diff --git a/assets/javascripts/bundle.c2b142ea.min.js b/assets/javascripts/bundle.c2b142ea.min.js index 36c87fe94..3a9ab3cd4 100644 --- a/assets/javascripts/bundle.c2b142ea.min.js +++ b/assets/javascripts/bundle.c2b142ea.min.js @@ -1,4 +1,4 @@ "use strict";(()=>{var xc=Object.create;var kn=Object.defineProperty,wc=Object.defineProperties,Ec=Object.getOwnPropertyDescriptor,Tc=Object.getOwnPropertyDescriptors,Sc=Object.getOwnPropertyNames,Dr=Object.getOwnPropertySymbols,Oc=Object.getPrototypeOf,An=Object.prototype.hasOwnProperty,Fo=Object.prototype.propertyIsEnumerable;var jo=(e,t,r)=>t in e?kn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,H=(e,t)=>{for(var r in t||(t={}))An.call(t,r)&&jo(e,r,t[r]);if(Dr)for(var r of Dr(t))Fo.call(t,r)&&jo(e,r,t[r]);return e},He=(e,t)=>wc(e,Tc(t));var gr=(e,t)=>{var r={};for(var n in e)An.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Dr)for(var n of Dr(e))t.indexOf(n)<0&&Fo.call(e,n)&&(r[n]=e[n]);return r};var Cn=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Lc=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Sc(t))!An.call(e,o)&&o!==r&&kn(e,o,{get:()=>t[o],enumerable:!(n=Ec(t,o))||n.enumerable});return e};var _r=(e,t,r)=>(r=e!=null?xc(Oc(e)):{},Lc(t||!e||!e.__esModule?kn(r,"default",{value:e,enumerable:!0}):r,e));var Uo=(e,t,r)=>new Promise((n,o)=>{var i=c=>{try{s(r.next(c))}catch(l){o(l)}},a=c=>{try{s(r.throw(c))}catch(l){o(l)}},s=c=>c.done?n(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var Do=Cn((Hn,No)=>{(function(e,t){typeof Hn=="object"&&typeof No!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Hn,(function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(_){return!!(_&&_!==document&&_.nodeName!=="HTML"&&_.nodeName!=="BODY"&&"classList"in _&&"contains"in _.classList)}function c(_){var de=_.type,be=_.tagName;return!!(be==="INPUT"&&a[de]&&!_.readOnly||be==="TEXTAREA"&&!_.readOnly||_.isContentEditable)}function l(_){_.classList.contains("focus-visible")||(_.classList.add("focus-visible"),_.setAttribute("data-focus-visible-added",""))}function u(_){_.hasAttribute("data-focus-visible-added")&&(_.classList.remove("focus-visible"),_.removeAttribute("data-focus-visible-added"))}function p(_){_.metaKey||_.altKey||_.ctrlKey||(s(r.activeElement)&&l(r.activeElement),n=!0)}function d(_){n=!1}function m(_){s(_.target)&&(n||c(_.target))&&l(_.target)}function h(_){s(_.target)&&(_.target.classList.contains("focus-visible")||_.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(_.target))}function v(_){document.visibilityState==="hidden"&&(o&&(n=!0),x())}function x(){document.addEventListener("mousemove",E),document.addEventListener("mousedown",E),document.addEventListener("mouseup",E),document.addEventListener("pointermove",E),document.addEventListener("pointerdown",E),document.addEventListener("pointerup",E),document.addEventListener("touchmove",E),document.addEventListener("touchstart",E),document.addEventListener("touchend",E)}function w(){document.removeEventListener("mousemove",E),document.removeEventListener("mousedown",E),document.removeEventListener("mouseup",E),document.removeEventListener("pointermove",E),document.removeEventListener("pointerdown",E),document.removeEventListener("pointerup",E),document.removeEventListener("touchmove",E),document.removeEventListener("touchstart",E),document.removeEventListener("touchend",E)}function E(_){_.target.nodeName&&_.target.nodeName.toLowerCase()==="html"||(n=!1,w())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",d,!0),document.addEventListener("pointerdown",d,!0),document.addEventListener("touchstart",d,!0),document.addEventListener("visibilitychange",v,!0),x(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var So=Cn((M0,vs)=>{"use strict";var Gu=/["'&<>]/;vs.exports=Ju;function Ju(e){var t=""+e,r=Gu.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i{(function(t,r){typeof jr=="object"&&typeof Lo=="object"?Lo.exports=r():typeof define=="function"&&define.amd?define([],r):typeof jr=="object"?jr.ClipboardJS=r():t.ClipboardJS=r()})(jr,function(){return(function(){var e={686:(function(n,o,i){"use strict";i.d(o,{default:function(){return vr}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),p=i.n(u);function d(B){try{return document.execCommand(B)}catch(C){return!1}}var m=function(C){var k=p()(C);return d("cut"),k},h=m;function v(B){var C=document.documentElement.getAttribute("dir")==="rtl",k=document.createElement("textarea");k.style.fontSize="12pt",k.style.border="0",k.style.padding="0",k.style.margin="0",k.style.position="absolute",k.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return k.style.top="".concat(D,"px"),k.setAttribute("readonly",""),k.value=B,k}var x=function(C,k){var D=v(C);k.container.appendChild(D);var W=p()(D);return d("copy"),D.remove(),W},w=function(C){var k=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=x(C,k):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=x(C.value,k):(D=p()(C),d("copy")),D},E=w;function _(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?_=function(k){return typeof k}:_=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},_(B)}var de=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},k=C.action,D=k===void 0?"copy":k,W=C.container,Z=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Z!==void 0)if(Z&&_(Z)==="object"&&Z.nodeType===1){if(D==="copy"&&Z.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(Z.hasAttribute("readonly")||Z.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return E(We,{container:W});if(Z)return D==="cut"?h(Z):E(Z,{container:W})},be=de;function M(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?M=function(k){return typeof k}:M=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},M(B)}function O(B,C){if(!(B instanceof C))throw new TypeError("Cannot call a class as a function")}function N(B,C){for(var k=0;k0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof W.action=="function"?W.action:this.defaultAction,this.target=typeof W.target=="function"?W.target:this.defaultTarget,this.text=typeof W.text=="function"?W.text:this.defaultText,this.container=M(W.container)==="object"?W.container:document.body}},{key:"listenClick",value:function(W){var Z=this;this.listener=l()(W,"click",function(We){return Z.onClick(We)})}},{key:"onClick",value:function(W){var Z=W.delegateTarget||W.currentTarget,We=this.action(Z)||"copy",Gt=be({action:We,container:this.container,target:this.target(Z),text:this.text(Z)});this.emit(Gt?"success":"error",{action:We,text:Gt,trigger:Z,clearSelection:function(){Z&&Z.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(W){return Yt("action",W)}},{key:"defaultTarget",value:function(W){var Z=Yt("target",W);if(Z)return document.querySelector(Z)}},{key:"defaultText",value:function(W){return Yt("text",W)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(W){var Z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return E(W,Z)}},{key:"cut",value:function(W){return h(W)}},{key:"isSupported",value:function(){var W=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Z=typeof W=="string"?[W]:W,We=!!document.queryCommandSupported;return Z.forEach(function(Gt){We=We&&!!document.queryCommandSupported(Gt)}),We}}]),k})(s()),vr=Mt}),828:(function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a}),438:(function(n,o,i){var a=i(828);function s(u,p,d,m,h){var v=l.apply(this,arguments);return u.addEventListener(d,v,h),{destroy:function(){u.removeEventListener(d,v,h)}}}function c(u,p,d,m,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof d=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,d,m,h)}))}function l(u,p,d,m){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&m.call(u,h)}}n.exports=c}),879:(function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}}),370:(function(n,o,i){var a=i(879),s=i(438);function c(d,m,h){if(!d&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(d))return l(d,m,h);if(a.nodeList(d))return u(d,m,h);if(a.string(d))return p(d,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(d,m,h){return d.addEventListener(m,h),{destroy:function(){d.removeEventListener(m,h)}}}function u(d,m,h){return Array.prototype.forEach.call(d,function(v){v.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(d,function(v){v.removeEventListener(m,h)})}}}function p(d,m,h){return s(document.body,d,m,h)}n.exports=c}),817:(function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}n.exports=o}),279:(function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c0&&i[i.length-1])&&(l[0]===6||l[0]===2)){r=0;continue}if(l[0]===3&&(!i||l[1]>i[0]&&l[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function te(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function ne(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||c(m,v)})},h&&(o[m]=h(o[m])))}function c(m,h){try{l(n[m](h))}catch(v){d(i[0][3],v)}}function l(m){m.value instanceof kt?Promise.resolve(m.value.v).then(u,p):d(i[0][2],m)}function u(m){c("next",m)}function p(m){c("throw",m)}function d(m,h){m(h),i.shift(),i.length&&c(i[0][0],i[0][1])}}function zo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof $e=="function"?$e(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),o(s,c,a.done,a.value)})}}function o(i,a,s,c){Promise.resolve(c).then(function(l){i({value:l,done:s})},a)}}function F(e){return typeof e=="function"}function Jt(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Vr=Jt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: `+r.map(function(n,o){return o+1+") "+n.toString()}).join(` `):"",this.name="UnsubscriptionError",this.errors=r}});function ct(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var rt=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=$e(a),c=s.next();!c.done;c=s.next()){var l=c.value;l.remove(this)}}catch(v){t={error:v}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(F(u))try{u()}catch(v){i=v instanceof Vr?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var d=$e(p),m=d.next();!m.done;m=d.next()){var h=m.value;try{qo(h)}catch(v){i=i!=null?i:[],v instanceof Vr?i=ne(ne([],te(i)),te(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{m&&!m.done&&(o=d.return)&&o.call(d)}finally{if(n)throw n.error}}}if(i)throw new Vr(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)qo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&ct(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&ct(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var Pn=rt.EMPTY;function zr(e){return e instanceof rt||e&&"closed"in e&&F(e.remove)&&F(e.add)&&F(e.unsubscribe)}function qo(e){F(e)?e():e.unsubscribe()}var Je={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Xt={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Pn:(this.currentObservers=null,s.push(r),new rt(function(){n.currentObservers=null,ct(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new Qo(r,n)},t})(U);var Qo=(function(e){ue(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Pn},t})(I);var Un=(function(e){ue(t,e);function t(r){var n=e.call(this)||this;return n._value=r,n}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var n=e.prototype._subscribe.call(this,r);return!n.closed&&r.next(this._value),n},t.prototype.getValue=function(){var r=this,n=r.hasError,o=r.thrownError,i=r._value;if(n)throw o;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(I);var xr={now:function(){return(xr.delegate||Date).now()},delegate:void 0};var wr=(function(e){ue(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=xr);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.schedule.call(this,r,n):(this.delay=n,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,n){return n>0||this.closed?e.prototype.execute.call(this,r,n):this._execute(r,n)},t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.flush(this),0)},t})(tr);var ri=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(rr);var Wn=new ri(ti);var ni=(function(e){ue(t,e);function t(r,n){var o=e.call(this,r,n)||this;return o.scheduler=r,o.work=n,o}return t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!==null&&o>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=er.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&n===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(er.cancelAnimationFrame(n),r._scheduled=void 0)},t})(tr);var oi=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n;r?n=r.id:(n=this._scheduled,this._scheduled=void 0);var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t})(rr);var je=new oi(ni);var y=new U(function(e){return e.complete()});function Br(e){return e&&F(e.schedule)}function Vn(e){return e[e.length-1]}function _t(e){return F(Vn(e))?e.pop():void 0}function qe(e){return Br(Vn(e))?e.pop():void 0}function Yr(e,t){return typeof Vn(e)=="number"?e.pop():t}var nr=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function Gr(e){return F(e==null?void 0:e.then)}function Jr(e){return F(e[Qt])}function Xr(e){return Symbol.asyncIterator&&F(e==null?void 0:e[Symbol.asyncIterator])}function Zr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Rc(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qr=Rc();function en(e){return F(e==null?void 0:e[Qr])}function tn(e){return Vo(this,arguments,function(){var r,n,o,i;return Wr(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,kt(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,kt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,kt(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function rn(e){return F(e==null?void 0:e.getReader)}function q(e){if(e instanceof U)return e;if(e!=null){if(Jr(e))return jc(e);if(nr(e))return Fc(e);if(Gr(e))return Uc(e);if(Xr(e))return ii(e);if(en(e))return Nc(e);if(rn(e))return Dc(e)}throw Zr(e)}function jc(e){return new U(function(t){var r=e[Qt]();if(F(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Fc(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?L(function(o,i){return e(o,i,n)}):Oe,Me(1),r?ot(t):wi(function(){return new on}))}}function Gn(e){return e<=0?function(){return y}:S(function(t,r){var n=[];t.subscribe(T(r,function(o){n.push(o),e=2,!0))}function xe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new I}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u,p,d,m=0,h=!1,v=!1,x=function(){p==null||p.unsubscribe(),p=void 0},w=function(){x(),u=d=void 0,h=v=!1},E=function(){var _=u;w(),_==null||_.unsubscribe()};return S(function(_,de){m++,!v&&!h&&x();var be=d=d!=null?d:r();de.add(function(){m--,m===0&&!v&&!h&&(p=Jn(E,c))}),be.subscribe(de),!u&&m>0&&(u=new Ct({next:function(M){return be.next(M)},error:function(M){v=!0,x(),p=Jn(w,o,M),be.error(M)},complete:function(){h=!0,x(),p=Jn(w,a),be.complete()}}),q(_).subscribe(u))})(l)}}function Jn(e,t){for(var r=[],n=2;ne.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function G(e,t=document){let r=Le(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function Le(e,t=document){return t.querySelector(e)||void 0}function xt(){var e,t,r,n;return(n=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?n:void 0}var il=R(b(document.body,"focusin"),b(document.body,"focusout")).pipe(Be(1),J(void 0),f(()=>xt()||document.body),se(1));function ir(e){return il.pipe(f(t=>e.contains(t)),ie())}function Ft(e,t){let{matches:r}=matchMedia("(hover)");return j(()=>(r?R(b(e,"mouseenter").pipe(f(()=>!0)),b(e,"mouseleave").pipe(f(()=>!1))):R(b(e,"touchstart").pipe(f(()=>!0)),b(e,"touchend").pipe(f(()=>!1)),b(e,"touchcancel").pipe(f(()=>!1)))).pipe(t?Tr(o=>Ve(+!o*t)):Oe,J(!0,e.matches(":hover"))))}function Oi(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Oi(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Oi(n,o);return n}function Li(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function ar(e){let t=A("script",{src:e});return j(()=>(document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(g(()=>zn(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(f(()=>{}),V(()=>document.head.removeChild(t)),Me(1))))}var Mi=new I,al=j(()=>typeof ResizeObserver=="undefined"?ar("https://unpkg.com/resize-observer-polyfill"):Y(void 0)).pipe(f(()=>new ResizeObserver(e=>e.forEach(t=>Mi.next(t)))),g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Ae(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Re(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return al.pipe($(r=>r.observe(t)),g(r=>Mi.pipe(L(n=>n.target===t),V(()=>r.unobserve(t)))),f(()=>Ae(e)),J(Ae(e)))}function Mr(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ki(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Ai(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function wt(e){return{x:e.offsetLeft,y:e.offsetTop}}function Ci(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Hi(e){return R(b(window,"load"),b(window,"resize")).pipe(Xe(0,je),f(()=>wt(e)),J(wt(e)))}function ln(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ut(e){return R(b(e,"scroll"),b(window,"scroll"),b(window,"resize")).pipe(Xe(0,je),f(()=>ln(e)),J(ln(e)))}var $i=new I,sl=j(()=>Y(new IntersectionObserver(e=>{for(let t of e)$i.next(t)},{threshold:0}))).pipe(g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Et(e){return sl.pipe($(t=>t.observe(e)),g(t=>$i.pipe(L(({target:r})=>r===e),V(()=>t.unobserve(e)),f(({isIntersecting:r})=>r))))}var cl=Object.create,la=Object.defineProperty,ll=Object.getOwnPropertyDescriptor,ul=Object.getOwnPropertyNames,pl=Object.getPrototypeOf,fl=Object.prototype.hasOwnProperty,ml=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),dl=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ul(t))!fl.call(e,o)&&o!==r&&la(e,o,{get:()=>t[o],enumerable:!(n=ll(t,o))||n.enumerable});return e},hl=(e,t,r)=>(r=e!=null?cl(pl(e)):{},dl(t||!e||!e.__esModule?la(r,"default",{value:e,enumerable:!0}):r,e)),vl=ml((e,t)=>{var r="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,c=/^0o[0-7]+$/i,l=parseInt,u=typeof global=="object"&&global&&global.Object===Object&&global,p=typeof self=="object"&&self&&self.Object===Object&&self,d=u||p||Function("return this")(),m=Object.prototype,h=m.toString,v=Math.max,x=Math.min,w=function(){return d.Date.now()};function E(O,N,ee){var le,ce,Ne,bt,De,st,tt=0,Yt=!1,Mt=!1,vr=!0;if(typeof O!="function")throw new TypeError(r);N=M(N)||0,_(ee)&&(Yt=!!ee.leading,Mt="maxWait"in ee,Ne=Mt?v(M(ee.maxWait)||0,N):Ne,vr="trailing"in ee?!!ee.trailing:vr);function B(Te){var gt=le,br=ce;return le=ce=void 0,tt=Te,bt=O.apply(br,gt),bt}function C(Te){return tt=Te,De=setTimeout(W,N),Yt?B(Te):bt}function k(Te){var gt=Te-st,br=Te-tt,Ro=N-gt;return Mt?x(Ro,Ne-br):Ro}function D(Te){var gt=Te-st,br=Te-tt;return st===void 0||gt>=N||gt<0||Mt&&br>=Ne}function W(){var Te=w();if(D(Te))return Z(Te);De=setTimeout(W,k(Te))}function Z(Te){return De=void 0,vr&&le?B(Te):(le=ce=void 0,bt)}function We(){De!==void 0&&clearTimeout(De),tt=0,le=st=ce=De=void 0}function Gt(){return De===void 0?bt:Z(w())}function Nr(){var Te=w(),gt=D(Te);if(le=arguments,ce=this,st=Te,gt){if(De===void 0)return C(st);if(Mt)return De=setTimeout(W,N),B(st)}return De===void 0&&(De=setTimeout(W,N)),bt}return Nr.cancel=We,Nr.flush=Gt,Nr}function _(O){var N=typeof O;return!!O&&(N=="object"||N=="function")}function de(O){return!!O&&typeof O=="object"}function be(O){return typeof O=="symbol"||de(O)&&h.call(O)==o}function M(O){if(typeof O=="number")return O;if(be(O))return n;if(_(O)){var N=typeof O.valueOf=="function"?O.valueOf():O;O=_(N)?N+"":N}if(typeof O!="string")return O===0?O:+O;O=O.replace(i,"");var ee=s.test(O);return ee||c.test(O)?l(O.slice(2),ee?2:8):a.test(O)?n:+O}t.exports=E}),yn,K,ua,pa,Nt,Pi,fa,ma,da,lo,to,ro,bl,Ar={},ha=[],gl=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Pr=Array.isArray;function pt(e,t){for(var r in t)e[r]=t[r];return e}function uo(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function Wt(e,t,r){var n,o,i,a={};for(i in t)i=="key"?n=t[i]:i=="ref"?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?yn.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return fn(e,a,n,o,null)}function fn(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o!=null?o:++ua,__i:-1,__u:0};return o==null&&K.vnode!=null&&K.vnode(i),i}function ft(e){return e.children}function at(e,t){this.props=e,this.context=t}function cr(e,t){if(t==null)return e.__?cr(e.__,e.__i+1):null;for(var r;ts&&Nt.sort(ma),e=Nt.shift(),s=Nt.length,e.__d&&(r=void 0,n=void 0,o=(n=(t=e).__v).__e,i=[],a=[],t.__P&&((r=pt({},n)).__v=n.__v+1,K.vnode&&K.vnode(r),po(t.__P,r,n,t.__n,t.__P.namespaceURI,32&n.__u?[o]:null,i,o!=null?o:cr(n),!!(32&n.__u),a),r.__v=n.__v,r.__.__k[r.__i]=r,_a(i,r,a),n.__e=n.__=null,r.__e!=o&&va(r)));vn.__r=0}function ba(e,t,r,n,o,i,a,s,c,l,u){var p,d,m,h,v,x,w,E=n&&n.__k||ha,_=t.length;for(c=_l(r,t,E,c,_),p=0;p<_;p++)(m=r.__k[p])!=null&&(d=m.__i==-1?Ar:E[m.__i]||Ar,m.__i=p,x=po(e,m,d,o,i,a,s,c,l,u),h=m.__e,m.ref&&d.ref!=m.ref&&(d.ref&&fo(d.ref,null,m),u.push(m.ref,m.__c||h,m)),v==null&&h!=null&&(v=h),(w=!!(4&m.__u))||d.__k===m.__k?c=ga(m,c,e,w):typeof m.type=="function"&&x!==void 0?c=x:h&&(c=h.nextSibling),m.__u&=-7);return r.__e=v,c}function _l(e,t,r,n,o){var i,a,s,c,l,u=r.length,p=u,d=0;for(e.__k=new Array(o),i=0;i0?fn(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):a).__=e,a.__b=e.__b+1,s=null,(l=a.__i=yl(a,r,c,p))!=-1&&(p--,(s=r[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(o>u?d--:oc?d--:d++,a.__u|=4))):e.__k[i]=null;if(p)for(i=0;i(u?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return a}return-1}function Ri(e,t,r){t[0]=="-"?e.setProperty(t,r!=null?r:""):e[t]=r==null?"":typeof r!="number"||gl.test(t)?r:r+"px"}function un(e,t,r,n,o){var i,a;e:if(t=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)r&&t in r||Ri(e.style,t,"");if(r)for(t in r)n&&r[t]==n[t]||Ri(e.style,t,r[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(da,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=r,r?n?r.u=n.u:(r.u=lo,e.addEventListener(t,i?ro:to,i)):e.removeEventListener(t,i?ro:to,i);else{if(o=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=r!=null?r:"";break e}catch(s){}typeof r=="function"||(r==null||r===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&r==1?"":r))}}function ji(e){return function(t){if(this.l){var r=this.l[t.type+e];if(t.t==null)t.t=lo++;else if(t.t0?e:Pr(e)?e.map(ya):pt({},e)}function xl(e,t,r,n,o,i,a,s,c){var l,u,p,d,m,h,v,x=r.props,w=t.props,E=t.type;if(E=="svg"?o="http://www.w3.org/2000/svg":E=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(l=0;l=r.__.length&&r.__.push({}),r.__[e]}function bn(e){return $r=1,Tl(Ta,e)}function Tl(e,t,r){var n=mo(Hr++,2);if(n.t=e,!n.__c&&(n.__=[r?r(t):Ta(void 0,t),function(s){var c=n.__N?n.__N[0]:n.__[0],l=n.t(c,s);c!==l&&(n.__N=[l,n.__[1]],n.__c.setState({}))}],n.__c=ve,!ve.__f)){var o=function(s,c,l){if(!n.__c.__H)return!0;var u=n.__c.__H.__.filter(function(d){return!!d.__c});if(u.every(function(d){return!d.__N}))return!i||i.call(this,s,c,l);var p=n.__c.props!==s;return u.forEach(function(d){if(d.__N){var m=d.__[0];d.__=d.__N,d.__N=void 0,m!==d.__[0]&&(p=!0)}}),i&&i.call(this,s,c,l)||p};ve.__f=!0;var i=ve.shouldComponentUpdate,a=ve.componentWillUpdate;ve.componentWillUpdate=function(s,c,l){if(this.__e){var u=i;i=void 0,o(s,c,l),i=u}a&&a.call(this,s,c,l)},ve.shouldComponentUpdate=o}return n.__N||n.__}function mt(e,t){var r=mo(Hr++,3);!we.__s&&Ea(r.__H,t)&&(r.__=e,r.u=t,ve.__H.__h.push(r))}function Vt(e){return $r=5,ur(function(){return{current:e}},[])}function ur(e,t){var r=mo(Hr++,7);return Ea(r.__H,t)&&(r.__=e(),r.__H=t,r.__h=e),r.__}function Sl(e,t){return $r=8,ur(function(){return e},t)}function Ol(){for(var e;e=wa.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(mn),e.__H.__h.forEach(oo),e.__H.__h=[]}catch(t){e.__H.__h=[],we.__e(t,e.__v)}}we.__b=function(e){ve=null,Ui&&Ui(e)},we.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),zi&&zi(e,t)},we.__r=function(e){Ni&&Ni(e),Hr=0;var t=(ve=e.__c).__H;t&&(Zn===ve?(t.__h=[],ve.__h=[],t.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(t.__h.forEach(mn),t.__h.forEach(oo),t.__h=[],Hr=0)),Zn=ve},we.diffed=function(e){Di&&Di(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(wa.push(t)!==1&&Fi===we.requestAnimationFrame||((Fi=we.requestAnimationFrame)||Ll)(Ol)),t.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),Zn=ve=null},we.__c=function(e,t){t.some(function(r){try{r.__h.forEach(mn),r.__h=r.__h.filter(function(n){return!n.__||oo(n)})}catch(n){t.some(function(o){o.__h&&(o.__h=[])}),t=[],we.__e(n,r.__v)}}),Wi&&Wi(e,t)},we.unmount=function(e){Vi&&Vi(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(n){try{mn(n)}catch(o){t=o}}),r.__H=void 0,t&&we.__e(t,r.__v))};var qi=typeof requestAnimationFrame=="function";function Ll(e){var t,r=function(){clearTimeout(n),qi&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,35);qi&&(t=requestAnimationFrame(r))}function mn(e){var t=ve,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),ve=t}function oo(e){var t=ve;e.__c=e.__(),ve=t}function Ea(e,t){return!e||e.length!==t.length||t.some(function(r,n){return r!==e[n]})}function Ta(e,t){return typeof t=="function"?t(e):t}function Ml(e,t){for(var r in t)e[r]=t[r];return e}function Ki(e,t){for(var r in e)if(r!=="__source"&&!(r in t))return!0;for(var n in t)if(n!=="__source"&&e[n]!==t[n])return!0;return!1}function Bi(e,t){this.props=e,this.context=t}(Bi.prototype=new at).isPureReactComponent=!0,Bi.prototype.shouldComponentUpdate=function(e,t){return Ki(this.props,e)||Ki(this.state,t)};var Yi=K.__b;K.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),Yi&&Yi(e)};var Yx=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,kl=K.__e;K.__e=function(e,t,r,n){if(e.then){for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return t.__e==null&&(t.__e=r.__e,t.__k=r.__k),o.__c(e,t)}kl(e,t,r,n)};var Gi=K.unmount;function Sa(e,t,r){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=Ml({},e)).__c!=null&&(e.__c.__P===r&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return Sa(n,t,r)})),e}function Oa(e,t,r){return e&&r&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return Oa(n,t,r)}),e.__c&&e.__c.__P===t&&(e.__e&&r.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=r)),e}function Qn(){this.__u=0,this.o=null,this.__b=null}function La(e){var t=e.__.__c;return t&&t.__a&&t.__a(e)}function pn(){this.i=null,this.l=null}K.unmount=function(e){var t=e.__c;t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),Gi&&Gi(e)},(Qn.prototype=new at).__c=function(e,t){var r=t.__c,n=this;n.o==null&&(n.o=[]),n.o.push(r);var o=La(n.__v),i=!1,a=function(){i||(i=!0,r.__R=null,o?o(s):s())};r.__R=a;var s=function(){if(!--n.__u){if(n.state.__a){var c=n.state.__a;n.__v.__k[0]=Oa(c,c.__c.__P,c.__c.__O)}var l;for(n.setState({__a:n.__b=null});l=n.o.pop();)l.forceUpdate()}};n.__u++||32&t.__u||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(a,a)},Qn.prototype.componentWillUnmount=function(){this.o=[]},Qn.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=Sa(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__a&&Wt(ft,null,e.fallback);return o&&(o.__u&=-33),[Wt(ft,null,t.__a?null:e.children),o]};var Ji=function(e,t,r){if(++r[1]===r[0]&&e.l.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.l.size))for(r=e.i;r;){for(;r.length>3;)r.pop()();if(r[1]Object.freeze({get current(){return t.current}}),[])}var Nl=typeof globalThis<"u"&&typeof navigator<"u"&&typeof document<"u";function Dl(e,...t){var r;(r=e==null?void 0:e.addEventListener)==null||r.call(e,...t)}function Wl(e,...t){var r;(r=e==null?void 0:e.removeEventListener)==null||r.call(e,...t)}var Vl=(e,t)=>Object.hasOwn(e,t),zl=()=>!0,ql=()=>!1;function Kl(e=!1){let t=Vt(e),r=Sl(()=>t.current,[]);return mt(()=>(t.current=!0,()=>{t.current=!1}),[]),r}function Bl(e,...t){let r=Kl(),n=ka(t[1]),o=ur(()=>function(...i){r()&&(typeof n.current=="function"?n.current.apply(this,i):typeof n.current.handleEvent=="function"&&n.current.handleEvent.apply(this,i))},[]);mt(()=>{let i=Yl(e)?e.current:e;if(!i)return;let a=t.slice(2);return Dl(i,t[0],o,...a),()=>{Wl(i,t[0],o,...a)}},[e,t[0]])}function Yl(e){return e!==null&&typeof e=="object"&&Vl(e,"current")}var Gl=e=>typeof e=="function"?e:typeof e=="string"?t=>t.key===e:e?zl:ql,Jl=Nl?globalThis:null;function Aa(e,t,r=[],n={}){let{event:o="keydown",target:i=Jl,eventOptions:a}=n,s=ka(t),c=ur(()=>{let l=Gl(e);return function(u){l(u)&&s.current.call(this,u)}},r);Bl(i,o,c,a)}function Ca(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t1)St--;else{for(var e,t=!1;kr!==void 0;){var r=kr;for(kr=void 0,io++;r!==void 0;){var n=r.o;if(r.o=void 0,r.f&=-3,!(8&r.f)&&Pa(r))try{r.c()}catch(o){t||(e=o,t=!0)}r=n}}if(io=0,St--,t)throw e}}function Ql(e){if(St>0)return e();St++;try{return e()}finally{xn()}}var ae=void 0;function Ha(e){var t=ae;ae=void 0;try{return e()}finally{ae=t}}var kr=void 0,St=0,io=0,gn=0;function $a(e){if(ae!==void 0){var t=e.n;if(t===void 0||t.t!==ae)return t={i:0,S:e,p:ae.s,n:void 0,t:ae,e:void 0,x:void 0,r:t},ae.s!==void 0&&(ae.s.n=t),ae.s=t,e.n=t,32&ae.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=ae.s,t.n=void 0,ae.s.n=t,ae.s=t),t}}function Ce(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Ce.prototype.brand=Zl;Ce.prototype.h=function(){return!0};Ce.prototype.S=function(e){var t=this,r=this.t;r!==e&&e.e===void 0&&(e.x=r,this.t=e,r!==void 0?r.e=e:Ha(function(){var n;(n=t.W)==null||n.call(t)}))};Ce.prototype.U=function(e){var t=this;if(this.t!==void 0){var r=e.e,n=e.x;r!==void 0&&(r.x=n,e.e=void 0),n!==void 0&&(n.e=r,e.x=void 0),e===this.t&&(this.t=n,n===void 0&&Ha(function(){var o;(o=t.Z)==null||o.call(t)}))}};Ce.prototype.subscribe=function(e){var t=this;return qt(function(){var r=t.value,n=ae;ae=void 0;try{e(r)}finally{ae=n}},{name:"sub"})};Ce.prototype.valueOf=function(){return this.value};Ce.prototype.toString=function(){return this.value+""};Ce.prototype.toJSON=function(){return this.value};Ce.prototype.peek=function(){var e=ae;ae=void 0;try{return this.value}finally{ae=e}};Object.defineProperty(Ce.prototype,"value",{get:function(){var e=$a(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(io>100)throw new Error("Cycle detected");this.v=e,this.i++,gn++,St++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{xn()}}}});function Ot(e,t){return new Ce(e,t)}function Pa(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function Ia(e){for(var t=e.s;t!==void 0;t=t.n){var r=t.S.n;if(r!==void 0&&(t.r=r),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function Ra(e){for(var t=e.s,r=void 0;t!==void 0;){var n=t.p;t.i===-1?(t.S.U(t),n!==void 0&&(n.n=t.n),t.n!==void 0&&(t.n.p=n)):r=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=n}e.s=r}function Kt(e,t){Ce.call(this,void 0),this.x=e,this.s=void 0,this.g=gn-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Kt.prototype=new Ce;Kt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===gn))return!0;if(this.g=gn,this.f|=1,this.i>0&&!Pa(this))return this.f&=-2,!0;var e=ae;try{Ia(this),ae=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(r){this.v=r,this.f|=16,this.i++}return ae=e,Ra(this),this.f&=-2,!0};Kt.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}Ce.prototype.S.call(this,e)};Kt.prototype.U=function(e){if(this.t!==void 0&&(Ce.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};Kt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(Kt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=$a(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function ta(e,t){return new Kt(e,t)}function ja(e){var t=e.u;if(e.u=void 0,typeof t=="function"){St++;var r=ae;ae=void 0;try{t()}catch(n){throw e.f&=-2,e.f|=8,ho(e),n}finally{ae=r,xn()}}}function ho(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,ja(e)}function eu(e){if(ae!==this)throw new Error("Out-of-order effect");Ra(this),ae=e,this.f&=-2,8&this.f&&ho(this),xn()}function pr(e,t){this.x=e,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32,this.name=t==null?void 0:t.name}pr.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.u=t)}finally{e()}};pr.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,ja(this),Ia(this),St++;var e=ae;return ae=this,eu.bind(this,e)};pr.prototype.N=function(){2&this.f||(this.f|=2,this.o=kr,kr=this)};pr.prototype.d=function(){this.f|=8,1&this.f||ho(this)};pr.prototype.dispose=function(){this.d()};function qt(e,t){var r=new pr(e,t);try{r.c()}catch(o){throw r.d(),o}var n=r.d.bind(r);return n[Symbol.dispose]=n,n}var Fa,vo,eo,Ua=[];qt(function(){Fa=this.N})();function fr(e,t){K[e]=t.bind(null,K[e]||function(){})}function _n(e){eo&&eo(),eo=e&&e.S()}function Na(e){var t=this,r=e.data,n=ru(r);n.value=r;var o=ur(function(){for(var s=t,c=t.__v;c=c.__;)if(c.__c){c.__c.__$f|=4;break}var l=ta(function(){var m=n.value.value;return m===0?0:m===!0?"":m||""}),u=ta(function(){return!Array.isArray(l.value)&&!pa(l.value)}),p=qt(function(){if(this.N=Da,u.value){var m=l.value;s.__v&&s.__v.__e&&s.__v.__e.nodeType===3&&(s.__v.__e.data=m)}}),d=t.__$u.d;return t.__$u.d=function(){p(),d.call(this)},[u,l]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}Na.displayName="ReactiveTextNode";Object.defineProperties(Ce.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Na},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});fr("__b",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),typeof t.type=="string"){var r,n=t.props;for(var o in n)if(o!=="children"){var i=n[o];i instanceof Ce&&(r||(t.__np=r={}),r[o]=i,n[o]=i.peek())}}e(t)});fr("__r",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(t),t.type!==ft){_n();var r,n=t.__c;n&&(n.__$f&=-2,(r=n.__$u)===void 0&&(n.__$u=r=(function(o){var i;return qt(function(){i=this}),i.c=function(){n.__$f|=1,n.setState({})},i})())),vo=n,_n(r)}e(t)});fr("__e",function(e,t,r,n){typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0,e(t,r,n)});fr("diffed",function(e,t){typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0;var r;if(typeof t.type=="string"&&(r=t.__e)){var n=t.__np,o=t.props;if(n){var i=r.U;if(i)for(var a in i){var s=i[a];s!==void 0&&!(a in n)&&(s.d(),i[a]=void 0)}else i={},r.U=i;for(var c in n){var l=i[c],u=n[c];l===void 0?(l=tu(r,c,u,o),i[c]=l):l.o(u,o)}}}e(t)});function tu(e,t,r,n){var o=t in e&&e.ownerSVGElement===void 0,i=Ot(r);return{o:function(a,s){i.value=a,n=s},d:qt(function(){this.N=Da;var a=i.value.value;n[t]!==a&&(n[t]=a,o?e[t]=a:a?e.setAttribute(t,a):e.removeAttribute(t))})}}fr("unmount",function(e,t){if(typeof t.type=="string"){var r=t.__e;if(r){var n=r.U;if(n){r.U=void 0;for(var o in n){var i=n[o];i&&i.d()}}}}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)});fr("__h",function(e,t,r,n){(n<3||n===9)&&(t.__$f|=2),e(t,r,n)});at.prototype.shouldComponentUpdate=function(e,t){var r=this.__$u,n=r&&r.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){var i=2&this.__$f;if(!(n||i||4&this.__$f)||1&this.__$f)return!0}else if(!(n||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};function ru(e,t){return bn(function(){return Ot(e,t)})[0]}var nu=function(e){queueMicrotask(function(){queueMicrotask(e)})};function ou(){Ql(function(){for(var e;e=Ua.shift();)Fa.call(e)})}function Da(){Ua.push(this)===1&&(K.requestAnimationFrame||nu)(ou)}var ao=[0];for(let e=0;e<32;e++)ao.push(ao[e]|1<>>5]>>>e&1}set(e){this.data[e>>>5]|=1<<(e&31)}forEach(e){let t=this.size&31;for(let r=0;r{var r;return(r=t.tags)==null?void 0:r.length})&&(matchMedia("(max-width: 768px)").matches||Wa())}function Dt(){Qe.value=He(H({},Qe.value),{hideSearch:!Qe.value.hideSearch})}function Wa(){Qe.value=He(H({},Qe.value),{hideFilters:!Qe.value.hideFilters})}function dn(){return Qe.value.selectedItem}function so(e){Qe.value=He(H({},Qe.value),{selectedItem:e})}function su(){var e,t;return(t=(e=lr.value)==null?void 0:e.items)!=null?t:[]}function wn(){return typeof Se.value.input=="string"?Se.value.input:""}function Va(e){let t=za();e.length&&!t.length?Se.value=He(H({},Se.value),{page:void 0,input:e}):!e.length&&t.length?Se.value=He(H({},Se.value),{page:void 0,input:{type:"operator",data:{operator:"not",operands:[]}}}):Se.value=He(H({},Se.value),{page:void 0,input:e})}function cu(){typeof it.value.pagination.next<"u"&&(Se.value=He(H({},Se.value),{page:it.value.pagination.next}))}function lu(e){let t=Se.value.filter.input;if("type"in t&&t.type==="operator"){for(let r of t.data.operands)if("type"in r&&r.type==="value"&&typeof r.data.value=="string"&&r.data.value===e)return!0}return!1}function za(){let e=Se.value.filter.input,t=[];if("type"in e&&e.type==="operator")for(let r of e.data.operands)"type"in r&&r.type==="value"&&typeof r.data.value=="string"&&t.push(r.data.value);return t}function uu(e){let t=Se.value.filter.input,r=[];if("type"in t&&t.type==="operator")for(let n of t.data.operands)"type"in n&&n.type==="value"&&typeof n.data.value=="string"&&r.push(n.data.value);if(r.includes(e)){let n=r.indexOf(e);n>-1&&r.splice(n,1)}else r.push(e);Se.value=He(H({},Se.value),{page:void 0,filter:He(H({},Se.value.filter),{input:{type:"operator",data:{operator:"and",operands:r.map(n=>({type:"value",data:{field:"tags",value:n}}))}}})}),Va(wn())}function pu(){return it.value.items}function fu(){return it.value.total}function mu(){var e;for(let t of(e=it.value.aggregations)!=null?e:[])if(t.type==="term")return t.data.value;return[]}function sr(){return Qe.value.hideSearch}function du(){return Qe.value.hideFilters}function qa(){var e;return(e=Ka.value.highlight)!=null?e:!1}var Qe=Ot({hideSearch:!0,hideFilters:!0,selectedItem:0}),Ka=Ot({}),lr=Ot(),na=Ot(),Se=Ot({input:"",filter:{input:{type:"operator",data:{operator:"and",operands:[]}},aggregation:{input:[{type:"term",data:{field:"tags"}}]}}}),it=Ot({items:[],query:{select:{documents:new ra(0),terms:new ra(0)},values:[]},pagination:{total:0}});function hu(e,t,r){for(let n=0;tr&&t(0,o,r,r=i);continue;case 62:e.charCodeAt(r+1)===47?t(2,--o,r,r=i+1):hu(e,r,n)?t(3,o,r,r=i+1):t(1,o++,r,r=i+1)}i>r&&t(0,o,r,i)}function bu(e,t=0,r=e.length){let n=++t;e:for(let l=0;n{let i=[],a=[],{onElement:s,onText:c=gu}=typeof r=="function"?{onElement:r}:r,l=0,u=0;return e(t,(p,d,m,h)=>{if(p===0)i[l++]=c(t,m,h),a[u++]={value:null,depth:d};else if(p&1&&(a[u++]={value:bu(t,m,h),depth:d}),p&2)for(let v=0;u>=0;v++){let{value:x,depth:w}=a[--u];if(w>d)continue;let E=i.slice(l-=v,l+v);i[l++]=s(x,E),u++;break}},n,o),i.slice(0,l)}}function yu(e){return e.replace(/[&<>]/g,t=>{switch(t.charCodeAt(0)){case 38:return"&";case 60:return"<";case 62:return">"}})}function hn(e){return e.replace(/&(amp|[lg]t);/g,t=>{switch(t.charCodeAt(1)){case 97:return"&";case 108:return"<";case 103:return">"}})}function xu(e,t){return{start:e.start+t,end:e.end+t,value:e.value}}function wu(e,t,r){return e.slice(t,r)}function Eu(e){let{onHighlight:t,onText:r=wu}=typeof e=="function"?{onHighlight:e}:e;return(n,o,i=0,a=n.length)=>{var l;let s=[],c=(l=o==null?void 0:o.ranges)!=null?l:[];for(let u=0,p=i;ua)break;let m=c[u].end;if(mi&&s.push(r(n,i,d));let{value:h}=c[u];s.push(t(n,{start:d,end:i=m,value:h}))}return i{let o=n.data;switch(o.type){case 1:na.value=!0;break;case 3:typeof o.data.pagination.prev<"u"?it.value=He(H({},it.value),{pagination:o.data.pagination,items:[...it.value.items,...o.data.items]}):(it.value=o.data,so(0));break}},qt(()=>{lr.value&&r.postMessage({type:0,data:lr.value})}),qt(()=>{na.value&&r.postMessage({type:2,data:Se.value})})}var oa={container:"p",hidden:"m"};function ku(e){return z("div",{class:zt(oa.container,{[oa.hidden]:e.hidden}),onClick:()=>Dt()})}var ia={container:"r",disabled:"c"};function co(e){return z("button",{class:zt(ia.container,{[ia.disabled]:!e.onClick}),onClick:e.onClick,children:e.children})}var aa=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Au=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),sa=e=>{let t=Au(e);return t.charAt(0).toUpperCase()+t.slice(1)},Cu=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),Hu={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},$u=c=>{var l=c,{color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,children:o,iconNode:i,class:a=""}=l,s=gr(l,["color","size","strokeWidth","absoluteStrokeWidth","children","iconNode","class"]);return Wt("svg",H(He(H({},Hu),{width:String(t),height:t,stroke:e,"stroke-width":n?Number(r)*24/Number(t):r,class:["lucide",a].join(" ")}),s),[...i.map(([u,p])=>Wt(u,p)),...Cr(o)])},bo=(e,t)=>{let r=a=>{var s=a,{class:n="",children:o}=s,i=gr(s,["class","children"]);return Wt($u,He(H({},i),{iconNode:t,class:Cu(`lucide-${aa(sa(e))}`,`lucide-${aa(e)}`,n)}),o)};return r.displayName=sa(e),r},Pu=bo("corner-down-left",[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4",key:"6o5b7l"}],["path",{d:"m9 10-5 5 5 5",key:"1kshq7"}]]),Iu=bo("list-filter",[["path",{d:"M2 5h20",key:"1fs1ex"}],["path",{d:"M6 12h12",key:"8npq4p"}],["path",{d:"M9 19h6",key:"456am0"}]]),Ru=bo("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Gx=hl(vl(),1);function ju({threshold:e=0,root:t=null,rootMargin:r="0%",freezeOnceVisible:n=!1,initialIsIntersecting:o=!1,onChange:i}={}){var a;let[s,c]=bn(null),[l,u]=bn(()=>({isIntersecting:o,entry:void 0})),p=Vt();p.current=i;let d=((a=l.entry)==null?void 0:a.isIntersecting)&&n;mt(()=>{if(!s||!("IntersectionObserver"in window)||d)return;let v,x=new IntersectionObserver(w=>{let E=Array.isArray(x.thresholds)?x.thresholds:[x.thresholds];w.forEach(_=>{let de=_.isIntersecting&&E.some(be=>_.intersectionRatio>=be);u({isIntersecting:de,entry:_}),p.current&&p.current(de,_),de&&n&&v&&(v(),v=void 0)})},{threshold:e,root:t,rootMargin:r});return x.observe(s),()=>{x.disconnect()}},[s,JSON.stringify(e),t,r,d,n]);let m=Vt(null);mt(()=>{var v;!s&&(v=l.entry)!=null&&v.target&&!n&&!d&&m.current!==l.entry.target&&(m.current=l.entry.target,u({isIntersecting:o,entry:void 0}))},[s,l.entry,n,d,o]);let h=[c,!!l.isIntersecting,l.entry];return h.ref=h[0],h.isIntersecting=h[1],h.entry=h[2],h}var lt={container:"n",hidden:"l",content:"u",pop:"d",badge:"y",sidebar:"i",controls:"w",results:"k",loadmore:"z"};function Fu(e){let{isIntersecting:t,ref:r}=ju({threshold:0});mt(()=>{t&&cu()},[t]);let n=Vt(null);mt(()=>{n.current&&typeof Se.value.page>"u"&&n.current.scrollTo({top:0,behavior:"smooth"})},[Se.value]);let o=za();return z("div",{class:zt(lt.container,{[lt.hidden]:e.hidden}),children:[z("div",{class:lt.content,children:[z("div",{class:lt.controls,children:[z(co,{onClick:Dt,children:z(Ru,{})}),z(Nu,{focus:!e.hidden}),z(co,{onClick:Wa,children:[z(Iu,{}),o.length>0&&z("span",{class:lt.badge,children:o.length})]})]}),z("div",{class:lt.results,ref:n,children:[z(Du,{keyboard:!e.hidden}),z("div",{class:lt.loadmore,ref:r})]})]}),z("div",{class:zt(lt.sidebar,{[lt.hidden]:du()}),children:z(Uu,{})})]})}var Tt={container:"X",list:"j",heading:"F",title:"I",item:"o",active:"g",value:"R",count:"q"};function Uu(e){let t=mu();return t.sort((r,n)=>n.node.count-r.node.count),z("div",{class:Tt.container,children:[z("h3",{class:Tt.heading,children:"Filters"}),z("h4",{class:Tt.title,children:"Tags"}),z("ol",{class:Tt.list,children:t.map(r=>z("li",{class:zt(Tt.item,{[Tt.active]:lu(r.node.value)}),onClick:()=>uu(r.node.value),children:[z("span",{class:Tt.value,children:r.node.value}),z("span",{class:Tt.count,children:r.node.count})]}))})]})}var ca={container:"f"};function Nu(e){let t=Vt(null);return mt(()=>{var r,n;e.focus?(r=t.current)==null||r.focus():(n=t.current)==null||n.blur()},[e.focus]),z("div",{class:ca.container,children:z("input",{ref:t,type:"text",class:ca.content,value:hn(wn()),onInput:r=>Va(yu(r.currentTarget.value)),autocapitalize:"off",autocomplete:"off",autocorrect:"off",placeholder:"Search",spellcheck:!1,role:"combobox"})})}var ut={container:"b",heading:"A",item:"a",active:"h",wrapper:"B",actions:"s",title:"x",path:"t"};function Ga(){let[e,t]=bn(!1);return mt(()=>{let r=()=>t(!0),n=()=>t(!1);return document.addEventListener("compositionstart",r),document.addEventListener("compositionend",n),()=>{document.removeEventListener("compositionstart",r),document.removeEventListener("compositionend",n)}},[]),e}function Du(e){var s;let t=su(),r=pu(),n=dn(),o=Vt([]),i=Ga();mt(()=>{let c=o.current[n];c&&c.scrollIntoView({block:"center",behavior:"smooth"})},[n]),Aa(e.keyboard,c=>{if(i)return;let l=dn();c.key==="ArrowDown"?(c.preventDefault(),so(Math.min(l+1,r.length-1))):c.key==="ArrowUp"&&(c.preventDefault(),so(Math.max(l-1,0)))},[e.keyboard,i]);let a=(s=fu())!=null?s:0;return z(ft,{children:[r.length>0&&z("h3",{class:ut.heading,children:[z("span",{class:ut.bubble,children:new Intl.NumberFormat("en-US").format(a)})," ","results"]}),z("ol",{class:ut.container,children:r.map((c,l)=>{var m;let u=Ba(t[c.id].title,c.matches.find(({field:h})=>h==="title")),p=Mu((m=t[c.id].path)!=null?m:[],c.matches.find(({field:h})=>h==="path")),d=t[c.id].location;if(qa()){let h=encodeURIComponent(wn()),[v,x]=d.split("#",2);d=`${v}?h=${h.replace(/%20/g,"+")}`,typeof x<"u"&&(d+=`#${x}`)}return z("li",{children:z("a",{ref:h=>{o.current[l]=h},href:d,onClick:()=>Dt(),class:zt(ut.item,{[ut.active]:l===dn()}),children:[z("div",{class:ut.wrapper,children:[z("h2",{class:ut.title,children:u}),z("menu",{class:ut.path,children:p.map(h=>z("li",{children:h}))})]}),z("nav",{class:ut.actions,children:z(co,{children:z(Pu,{})})})]})})})})]})}var Wu={container:"e"};function Vu(e){let t=Ga();return Aa(!0,r=>{var n,o,i,a,s;if(!t)if((r.metaKey||r.ctrlKey)&&r.key==="k")r.preventDefault(),Dt();else if((r.metaKey||r.ctrlKey)&&r.key==="j")document.body.classList.toggle("dark");else if(r.key==="Enter"&&!sr()){r.preventDefault();let c=dn(),l=(o=(n=it.value)==null?void 0:n.items[c])==null?void 0:o.id;if((a=(i=lr.value)==null?void 0:i.items[l])!=null&&a.location){Dt();let u=(s=lr.value)==null?void 0:s.items[l].location;if(qa()){let p=encodeURIComponent(wn()),[d,m]=u.split("#",2);u=`${d}?h=${p.replace(/%20/g,"+")}`,typeof m<"u"&&(u+=`#${m}`)}window.location.href=u}}else r.key==="Escape"&&!sr()&&(r.preventDefault(),Dt())},[t]),z("div",{class:Wu.container,children:[z(ku,{hidden:sr()}),z(Fu,{hidden:sr()})]})}function Ja(e,t){au(e),El(z(Vu,{}),t)}function go(){Dt()}function zu(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function qu(){return R(b(window,"compositionstart").pipe(f(()=>!0)),b(window,"compositionend").pipe(f(()=>!1))).pipe(J(!1))}function Xa(){let e=b(window,"keydown").pipe(f(t=>({mode:sr()?"global":"search",type:t.key,meta:t.ctrlKey||t.metaKey,claim(){t.preventDefault(),t.stopPropagation()}})),L(({mode:t,type:r})=>{if(t==="global"){let n=xt();if(typeof n!="undefined")return!zu(n,r)}return!0}),xe());return qu().pipe(g(t=>t?y:e))}function Ye(){return new URL(location.href)}function dt(e,t=!1){if(X("navigation.instant")&&!t){let r=A("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function Za(){return new I}function Qa(){return location.hash.slice(1)}function es(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function _o(e){return R(b(window,"hashchange"),e).pipe(f(Qa),J(Qa()),L(t=>t.length>0),se(1))}function ts(e){return _o(e).pipe(f(t=>Le(`[id="${t}"]`)),L(t=>typeof t!="undefined"))}function Ir(e){let t=matchMedia(e);return an(r=>t.addListener(()=>r(t.matches))).pipe(J(t.matches))}function rs(){let e=matchMedia("print");return R(b(window,"beforeprint").pipe(f(()=>!0)),b(window,"afterprint").pipe(f(()=>!1))).pipe(J(e.matches))}function yo(e,t){return e.pipe(g(r=>r?t():y))}function xo(e,t){return new U(r=>{let n=new XMLHttpRequest;return n.open("GET",`${e}`),n.responseType="blob",n.addEventListener("load",()=>{n.status>=200&&n.status<300?(r.next(n.response),r.complete()):r.error(new Error(n.statusText))}),n.addEventListener("error",()=>{r.error(new Error("Network error"))}),n.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(n.addEventListener("progress",o=>{var i;if(o.lengthComputable)t.progress$.next(o.loaded/o.total*100);else{let a=(i=n.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(o.loaded/+a*100)}}),t.progress$.next(5)),n.send(),()=>n.abort()})}function et(e,t){return xo(e,t).pipe(g(r=>r.text()),f(r=>JSON.parse(r)),se(1))}function En(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/html")),se(1))}function ns(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/xml")),se(1))}var wo={drawer:G("[data-md-toggle=drawer]"),search:G("[data-md-toggle=search]")};function Eo(e,t){wo[e].checked!==t&&wo[e].click()}function Tn(e){let t=wo[e];return b(t,"change").pipe(f(()=>t.checked),J(t.checked))}function os(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function is(){return R(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(f(os),J(os()))}function as(){return{width:innerWidth,height:innerHeight}}function ss(){return b(window,"resize",{passive:!0}).pipe(f(as),J(as()))}function cs(){return re([is(),ss()]).pipe(f(([e,t])=>({offset:e,size:t})),se(1))}function Sn(e,{viewport$:t,header$:r}){let n=t.pipe(fe("size")),o=re([n,r]).pipe(f(()=>wt(e)));return re([r,t,o]).pipe(f(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}var Ku=G("#__config"),mr=JSON.parse(Ku.textContent);mr.base=`${new URL(mr.base,Ye())}`;function Ue(){return mr}function X(e){return mr.features.includes(e)}function Bt(e,t){return typeof t!="undefined"?mr.translations[e].replace("#",t.toString()):mr.translations[e]}function ht(e,t=document){return G(`[data-md-component=${e}]`,t)}function Ee(e,t=document){return P(`[data-md-component=${e}]`,t)}function Bu(e){let t=G(".md-typeset > :first-child",e);return b(t,"click",{once:!0}).pipe(f(()=>G(".md-typeset",e)),f(r=>({hash:__md_hash(r.innerHTML)})))}function ls(e){if(!X("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=G(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return j(()=>{let t=new I;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Bu(e).pipe($(r=>t.next(r)),V(()=>t.complete()),f(r=>H({ref:e},r)))})}function Yu(e,{target$:t}){return t.pipe(f(r=>({hidden:r!==e})))}function us(e,t){let r=new I;return r.subscribe(({hidden:n})=>{e.hidden=n}),Yu(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))}function To(e,t){return t==="inline"?A("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})):A("div",{class:"md-tooltip",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"}))}function On(...e){return A("div",{class:"md-tooltip2",role:"dialog"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function ps(...e){return A("div",{class:"md-tooltip2",role:"tooltip"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function fs(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("a",{href:r,class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}else return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("span",{class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}function ms(e){return A("button",{class:"md-code__button",title:Bt("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function ds(){return A("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function hs(){return A("nav",{class:"md-code__nav"})}var Xu=_r(So());function bs(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Li(r):r)))}function Oo(e){let t=`tabbed-control tabbed-control--${e}`;return A("div",{class:t,hidden:!0},A("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function gs(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function Zu(e){var n;let t=Ue(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:`${r}`,class:"md-version__link"},e.title,((n=t.version)==null?void 0:n.alias)&&e.aliases.length>0&&A("span",{class:"md-version__alias"},e.aliases[0])))}function _s(e,t){var n;let r=Ue();return e=e.filter(o=>{var i;return!((i=o.properties)!=null&&i.hidden)}),A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":Bt("select.version")},t.title,((n=r.version)==null?void 0:n.alias)&&t.aliases.length>0&&A("span",{class:"md-version__alias"},t.aliases[0])),A("ul",{class:"md-version__list"},e.map(Zu)))}var Qu=0;function ep(e,t=250){let r=re([ir(e),Ft(e,t)]).pipe(f(([o,i])=>o||i),ie()),n=j(()=>Ai(e)).pipe(oe(Ut),Lr(1),Ze(r),f(()=>Ci(e)));return r.pipe(Sr(o=>o),g(()=>re([r,n])),f(([o,i])=>({active:o,offset:i})),xe())}function Rr(e,t,r=250){let{content$:n,viewport$:o}=t,i=`__tooltip2_${Qu++}`;return j(()=>{let a=new I,s=new Un(!1);a.pipe(he(),ye(!1)).subscribe(s);let c=s.pipe(Tr(u=>Ve(+!u*250,Wn)),ie(),g(u=>u?n:y),$(u=>u.id=i),xe());re([a.pipe(f(({active:u})=>u)),c.pipe(g(u=>Ft(u,250)),J(!1))]).pipe(f(u=>u.some(p=>p))).subscribe(s);let l=s.pipe(L(u=>u),pe(c,o),f(([u,p,{size:d}])=>{let m=e.getBoundingClientRect(),h=m.width/2;if(p.role==="tooltip")return{x:h,y:8+m.height};if(m.y>=d.height/2){let{height:v}=Ae(p);return{x:h,y:-16-v}}else return{x:h,y:16+m.height}}));return re([c,a,l]).subscribe(([u,{offset:p},d])=>{u.style.setProperty("--md-tooltip-host-x",`${p.x}px`),u.style.setProperty("--md-tooltip-host-y",`${p.y}px`),u.style.setProperty("--md-tooltip-x",`${d.x}px`),u.style.setProperty("--md-tooltip-y",`${d.y}px`),u.classList.toggle("md-tooltip2--top",d.y<0),u.classList.toggle("md-tooltip2--bottom",d.y>=0)}),s.pipe(L(u=>u),pe(c,(u,p)=>p),L(u=>u.role==="tooltip")).subscribe(u=>{let p=Ae(G(":scope > *",u));u.style.setProperty("--md-tooltip-width",`${p.width}px`),u.style.setProperty("--md-tooltip-tail","0px")}),s.pipe(ie(),Ie(je),pe(c)).subscribe(([u,p])=>{p.classList.toggle("md-tooltip2--active",u)}),re([s.pipe(L(u=>u)),c]).subscribe(([u,p])=>{p.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),s.pipe(L(u=>!u)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ep(e,r).pipe($(u=>a.next(u)),V(()=>a.complete()),f(u=>H({ref:e},u)))})}function Ge(e,{viewport$:t},r=document.body){return Rr(e,{content$:new U(n=>{let o=e.title,i=ps(o);return n.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",o)}}),viewport$:t},0)}function tp(e,t){let r=j(()=>re([Hi(e),Ut(t)])).pipe(f(([{x:n,y:o},i])=>{let{width:a,height:s}=Ae(e);return{x:n-i.x+a/2,y:o-i.y+s/2}}));return ir(e).pipe(g(n=>r.pipe(f(o=>({active:n,offset:o})),Me(+!n||1/0))))}function ys(e,t,{target$:r}){let[n,o]=Array.from(e.children);return j(()=>{let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),Et(e).pipe(Q(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),R(i.pipe(L(({active:s})=>s)),i.pipe(Be(250),L(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Xe(16,je)).subscribe(({active:s})=>{n.classList.toggle("md-tooltip--active",s)}),i.pipe(Lr(125,je),L(()=>!!e.offsetParent),f(()=>e.offsetParent.getBoundingClientRect()),f(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),b(o,"click").pipe(Q(a),L(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),b(o,"mousedown").pipe(Q(a),pe(i)).subscribe(([s,{active:c}])=>{var l;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(l=xt())==null||l.blur()}}),r.pipe(Q(a),L(s=>s===n),It(125)).subscribe(()=>e.focus()),tp(e,t).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function rp(e){let t=Ue();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate){let n=e.closest("[class|=language]");if(n)for(let o of Array.from(n.classList)){if(!o.startsWith("language-"))continue;let[,i]=o.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return P(r.join(", "),e)}function np(e){let t=[];for(let r of rp(e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let l=i.splitText(a.index);i=l.splitText(s.length),t.push(l)}else{i.textContent=s,t.push(i);break}}}}return t}function xs(e,t){t.append(...Array.from(e.childNodes))}function Ln(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let s of np(t)){let[,c]=s.textContent.match(/\((\d+)\)/);Le(`:scope > li:nth-child(${c})`,e)&&(a.set(c,fs(c,i)),s.replaceWith(a.get(c)))}return a.size===0?y:j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=[];for(let[u,p]of a)l.push([G(".md-typeset",p),G(`:scope > li:nth-child(${u})`,e)]);return n.pipe(Q(c)).subscribe(u=>{e.hidden=!u,e.classList.toggle("md-annotation-list",u);for(let[p,d]of l)u?xs(p,d):xs(d,p)}),R(...[...a].map(([,u])=>ys(u,t,{target$:r}))).pipe(V(()=>s.complete()),xe())})}function ws(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return ws(t)}}function Es(e,t){return j(()=>{let r=ws(e);return typeof r!="undefined"?Ln(r,e,t):y})}var Ss=_r(Mo());var op=0,Ts=R(b(window,"keydown").pipe(f(()=>!0)),R(b(window,"keyup"),b(window,"contextmenu")).pipe(f(()=>!1))).pipe(J(!1),se(1));function Os(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Os(t)}}function ip(e){return Re(e).pipe(f(({width:t})=>({scrollable:Mr(e).width>t})),fe("scrollable"))}function Ls(e,t){let{matches:r}=matchMedia("(hover)"),n=j(()=>{let o=new I,i=o.pipe(Gn(1));o.subscribe(({scrollable:m})=>{m&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[],s=e.closest("pre"),c=s.closest("[id]"),l=c?c.id:op++;s.id=`__code_${l}`;let u=[],p=e.closest(".highlight");if(p instanceof HTMLElement){let m=Os(p);if(typeof m!="undefined"&&(p.classList.contains("annotate")||X("content.code.annotate"))){let h=Ln(m,e,t);u.push(Re(p).pipe(Q(i),f(({width:v,height:x})=>v&&x),ie(),g(v=>v?h:y)))}}let d=P(":scope > span[id]",e);if(d.length&&(e.classList.add("md-code__content"),e.closest(".select")||X("content.code.select")&&!e.closest(".no-select"))){let m=+d[0].id.split("-").pop(),h=ds();a.push(h),X("content.tooltips")&&u.push(Ge(h,{viewport$}));let v=b(h,"click").pipe(Or(M=>!M,!1),$(()=>h.blur()),xe());v.subscribe(M=>{h.classList.toggle("md-code__button--active",M)});let x=me(d).pipe(oe(M=>Ft(M).pipe(f(O=>[M,O]))));v.pipe(g(M=>M?x:y)).subscribe(([M,O])=>{let N=Le(".hll.select",M);if(N&&!O)N.replaceWith(...Array.from(N.childNodes));else if(!N&&O){let ee=document.createElement("span");ee.className="hll select",ee.append(...Array.from(M.childNodes).slice(1)),M.append(ee)}});let w=me(d).pipe(oe(M=>b(M,"mousedown").pipe($(O=>O.preventDefault()),f(()=>M)))),E=v.pipe(g(M=>M?w:y),pe(Ts),f(([M,O])=>{var ee;let N=d.indexOf(M)+m;if(O===!1)return[N,N];{let le=P(".hll",e).map(ce=>d.indexOf(ce.parentElement)+m);return(ee=window.getSelection())==null||ee.removeAllRanges(),[Math.min(N,...le),Math.max(N,...le)]}})),_=_o(y).pipe(L(M=>M.startsWith(`__codelineno-${l}-`)));_.subscribe(M=>{let[,,O]=M.split("-"),N=O.split(":").map(le=>+le-m+1);N.length===1&&N.push(N[0]);for(let le of P(".hll:not(.select)",e))le.replaceWith(...Array.from(le.childNodes));let ee=d.slice(N[0]-1,N[1]);for(let le of ee){let ce=document.createElement("span");ce.className="hll",ce.append(...Array.from(le.childNodes).slice(1)),le.append(ce)}}),_.pipe(Me(1),Ie(ge)).subscribe(M=>{if(M.includes(":")){let O=document.getElementById(M.split(":")[0]);O&&setTimeout(()=>{let N=O,ee=-64;for(;N!==document.body;)ee+=N.offsetTop,N=N.offsetParent;window.scrollTo({top:ee})},1)}});let be=me(P('a[href^="#__codelineno"]',p)).pipe(oe(M=>b(M,"click").pipe($(O=>O.preventDefault()),f(()=>M)))).pipe(Q(i),pe(Ts),f(([M,O])=>{let ee=+G(`[id="${M.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(O===!1)return[ee,ee];{let le=P(".hll",e).map(ce=>+ce.parentElement.id.split("-").pop());return[Math.min(ee,...le),Math.max(ee,...le)]}}));R(E,be).subscribe(M=>{let O=`#__codelineno-${l}-`;M[0]===M[1]?O+=M[0]:O+=`${M[0]}:${M[1]}`,history.replaceState({},"",O),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+O,oldURL:window.location.href}))})}if(Ss.default.isSupported()&&(e.closest(".copy")||X("content.code.copy")&&!e.closest(".no-copy"))){let m=ms(s.id);a.push(m),X("content.tooltips")&&u.push(Ge(m,{viewport$}))}if(a.length){let m=hs();m.append(...a),s.insertBefore(m,e)}return ip(e).pipe($(m=>o.next(m)),V(()=>o.complete()),f(m=>H({ref:e},m)),Rt(R(...u).pipe(Q(i))))});return X("content.lazy")?Et(e).pipe(L(o=>o),Me(1),g(()=>n)):n}function ap(e,{target$:t,print$:r}){let n=!0;return R(t.pipe(f(o=>o.closest("details:not([open])")),L(o=>e===o),f(()=>({action:"open",reveal:!0}))),r.pipe(L(o=>o||!n),$(()=>n=e.open),f(o=>({action:o?"open":"close"}))))}function Ms(e,t){return j(()=>{let r=new I;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),ap(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}var ks=0,As=new Map;function sp(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],n=e.nextElementSibling;for(;n&&!(n instanceof HTMLHeadingElement);)r.push(n.cloneNode(!0)),n=n.nextElementSibling;return r}function cp(e,t){for(let r of P("[href], [src]",e))for(let n of["href","src"]){let o=r.getAttribute(n);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){r[n]=new URL(r.getAttribute(n),t).toString();break}}for(let r of P("[name^=__], [for]",e))for(let n of["id","for","name"]){let o=r.getAttribute(n);o&&r.setAttribute(n,`${o}$preview_${ks}`)}return ks++,Y(e)}function lp(e){let t=As.get(e.toString());return t?Y(t):En(e).pipe(g(r=>cp(r,e)),f(r=>(As.set(e.toString(),r),r)))}function Cs(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(X("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let n=re([ir(e),Ft(e).pipe(ke(1))]).pipe(f(([i,a])=>i||a),ie(),L(i=>i));return $t([r,n]).pipe(g(([i])=>{let a=new URL(e.href);return a.search=a.hash="",i.has(`${a}`)?Y(a):y}),g(i=>lp(i)),g(i=>{let a=e.hash?`article [id="${decodeURIComponent(e.hash.slice(1))}"]`:"article h1",s=Le(a,i);return typeof s=="undefined"?y:Y(sp(s))})).pipe(g(i=>{let a=new U(s=>{let c=On(...i);return s.next(c),document.body.append(c),()=>c.remove()});return Rr(e,H({content$:a},t))}))}var Hs=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var ko,pp=0;function fp(){return typeof mermaid=="undefined"||mermaid instanceof Element?ar("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):Y(void 0)}function $s(e){return e.classList.remove("mermaid"),ko||(ko=fp().pipe($(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Hs,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),f(()=>{}),se(1))),ko.subscribe(()=>Uo(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${pp++}`,r=A("div",{class:"mermaid"}),n=e.textContent,{svg:o,fn:i}=yield mermaid.render(t,n),a=r.attachShadow({mode:"closed"});a.innerHTML=o,e.replaceWith(r),i==null||i(a)})),ko.pipe(f(()=>({ref:e})))}var Ps=A("table");function Is(e){return e.replaceWith(Ps),Ps.replaceWith(gs(e)),Y({ref:e})}function mp(e){let t=e.find(r=>r.checked)||e[0];return R(...e.map(r=>b(r,"change").pipe(f(()=>G(`label[for="${r.id}"]`))))).pipe(J(G(`label[for="${t.id}"]`)),f(r=>({active:r})))}function Rs(e,{viewport$:t,target$:r}){let n=G(".tabbed-labels",e),o=P(":scope > input",e),i=Oo("prev");e.append(i);let a=Oo("next");return e.append(a),j(()=>{let s=new I,c=s.pipe(he(),ye(!0));re([s,Re(e),Et(e)]).pipe(Q(c),Xe(1,je)).subscribe({next([{active:l},u]){let p=wt(l),{width:d}=Ae(l);e.style.setProperty("--md-indicator-x",`${p.x}px`),e.style.setProperty("--md-indicator-width",`${d}px`);let m=ln(n);(p.xm.x+u.width)&&n.scrollTo({left:Math.max(0,p.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),re([Ut(n),Re(n)]).pipe(Q(c)).subscribe(([l,u])=>{let p=Mr(n);i.hidden=l.x<16,a.hidden=l.x>p.width-u.width-16}),R(b(i,"click").pipe(f(()=>-1)),b(a,"click").pipe(f(()=>1))).pipe(Q(c)).subscribe(l=>{let{width:u}=Ae(n);n.scrollBy({left:u*l,behavior:"smooth"})}),r.pipe(Q(c),L(l=>o.includes(l))).subscribe(l=>l.click()),n.classList.add("tabbed-labels--linked");for(let l of o){let u=G(`label[for="${l.id}"]`);u.replaceChildren(A("a",{href:`#${u.htmlFor}`,tabIndex:-1},...Array.from(u.childNodes))),b(u.firstElementChild,"click").pipe(Q(c),L(p=>!(p.metaKey||p.ctrlKey)),$(p=>{p.preventDefault(),p.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${u.htmlFor}`),u.click()})}return X("content.tabs.link")&&s.pipe(ke(1),pe(t)).subscribe(([{active:l},{offset:u}])=>{let p=l.innerText.trim();if(l.hasAttribute("data-md-switching"))l.removeAttribute("data-md-switching");else{let d=e.offsetTop-u.y;for(let h of P("[data-tabs]"))for(let v of P(":scope > input",h)){let x=G(`label[for="${v.id}"]`);if(x!==l&&x.innerText.trim()===p){x.setAttribute("data-md-switching",""),v.click();break}}window.scrollTo({top:e.offsetTop-d});let m=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([p,...m])])}}),s.pipe(Q(c)).subscribe(()=>{for(let l of P("audio, video",e))l.offsetWidth&&l.autoplay?l.play().catch(()=>{}):l.pause()}),mp(o).pipe($(l=>s.next(l)),V(()=>s.complete()),f(l=>H({ref:e},l)))}).pipe(Ht(ge))}function js(e,t){let{viewport$:r,target$:n,print$:o}=t;return R(...P(".annotate:not(.highlight)",e).map(i=>Es(i,{target$:n,print$:o})),...P("pre:not(.mermaid) > code",e).map(i=>Ls(i,{target$:n,print$:o})),...P("a",e).map(i=>Cs(i,t)),...P("pre.mermaid",e).map(i=>$s(i)),...P("table:not([class])",e).map(i=>Is(i)),...P("details",e).map(i=>Ms(i,{target$:n,print$:o})),...P("[data-tabs]",e).map(i=>Rs(i,{viewport$:r,target$:n})),...P("[title]:not([data-preview])",e).filter(()=>X("content.tooltips")).map(i=>Ge(i,{viewport$:r})),...P(".footnote-ref",e).filter(()=>X("content.footnote.tooltips")).map(i=>Rr(i,{content$:new U(a=>{let s=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(s).cloneNode(!0).children),l=On(...c);return a.next(l),document.body.append(l),()=>l.remove()}),viewport$:r})))}function dp(e,{alert$:t}){return t.pipe(g(r=>R(Y(!0),Y(!1).pipe(It(2e3))).pipe(f(n=>({message:r,active:n})))))}function Fs(e,t){let r=G(".md-typeset",e);return j(()=>{let n=new I;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),dp(e,t).pipe($(o=>n.next(o)),V(()=>n.complete()),f(o=>H({ref:e},o)))})}function hp({viewport$:e}){if(!X("header.autohide"))return Y(!1);let t=e.pipe(f(({offset:{y:o}})=>o),Pt(2,1),f(([o,i])=>[oMath.abs(i-o.y)>100),f(([,[o]])=>o),ie()),n=Tn("search");return re([e,n]).pipe(f(([{offset:o},i])=>o.y>400&&!i),ie(),g(o=>o?r:Y(!1)),J(!1))}function Us(e,t){return j(()=>re([Re(e),hp(t)])).pipe(f(([{height:r},n])=>({height:r,hidden:n})),ie((r,n)=>r.height===n.height&&r.hidden===n.hidden),se(1))}function Ns(e,{viewport$:t,header$:r,main$:n}){return j(()=>{let o=new I,i=o.pipe(he(),ye(!0));o.pipe(fe("active"),Ze(r)).subscribe(([{active:s},{hidden:c}])=>{e.classList.toggle("md-header--shadow",s&&!c),e.hidden=c});let a=me(P("[title]",e)).pipe(L(()=>X("content.tooltips")),oe(s=>Ge(s,{viewport$:t})));return n.subscribe(o),r.pipe(Q(i),f(s=>H({ref:e},s)),Rt(a.pipe(Q(i))))})}function vp(e,{viewport$:t,header$:r}){return Sn(e,{viewport$:t,header$:r}).pipe(f(({offset:{y:n}})=>{let{height:o}=Ae(e);return{active:o>0&&n>=o}}),fe("active"))}function Ds(e,t){return j(()=>{let r=new I;r.subscribe({next({active:o}){e.classList.toggle("md-header__title--active",o)},complete(){e.classList.remove("md-header__title--active")}});let n=Le(".md-content h1");return typeof n=="undefined"?y:vp(n,t).pipe($(o=>r.next(o)),V(()=>r.complete()),f(o=>H({ref:e},o)))})}function Ws(e,{viewport$:t,header$:r}){let n=r.pipe(f(({height:i})=>i),ie()),o=n.pipe(g(()=>Re(e).pipe(f(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),fe("bottom"))));return re([n,o,t]).pipe(f(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),ie((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function bp(e){let t=__md_get("__palette")||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return Y(...e).pipe(oe(n=>b(n,"change").pipe(f(()=>n))),J(e[r]),f(n=>({index:e.indexOf(n),color:{media:n.getAttribute("data-md-color-media"),scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),se(1))}function Vs(e){let t=P("input",e),r=A("meta",{name:"theme-color"});document.head.appendChild(r);let n=A("meta",{name:"color-scheme"});document.head.appendChild(n);let o=Ir("(prefers-color-scheme: light)");return j(()=>{let i=new I;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),pe(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(f(()=>{let a=ht("header"),s=window.getComputedStyle(a);return n.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Ie(ge)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),bp(t).pipe(Q(o.pipe(ke(1))),jt(),$(a=>i.next(a)),V(()=>i.complete()),f(a=>H({ref:e},a)))})}function zs(e,{progress$:t}){return j(()=>{let r=new I;return r.subscribe(({value:n})=>{e.style.setProperty("--md-progress-value",`${n}`)}),t.pipe($(n=>r.next({value:n})),V(()=>r.complete()),f(n=>({ref:e,value:n})))})}var qs='.v u{text-decoration:underline!important;text-decoration-style:wavy!important;text-decoration-thickness:1px!important}.p{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-backdrop)/var(--alpha-lighter));cursor:pointer;height:100%;pointer-events:auto;position:absolute;transition:opacity .25s;width:100%}.p.m{opacity:0;pointer-events:none;transition:opacity .35s}.r{align-items:center;background-color:initial;border:none;border-radius:var(--space-2);cursor:pointer;display:flex;flex-shrink:0;font-family:var(--font-family);height:36px;justify-content:center;outline:none;padding:0;position:relative;transition:background-color .25s,color .25s;width:36px;z-index:1}.r svg{stroke:rgb(var(--color-foreground));height:18px;opacity:.5;width:18px}.r:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.r:hover:before{opacity:1;transform:scale(1)}.r.c{cursor:auto}.r.c:before{display:none}.n{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background)/var(--alpha-light));border-radius:var(--space-3);box-shadow:0 0 60px #0000000d;display:flex;height:480px;overflow:hidden;pointer-events:auto;position:absolute;transition:transform .25s cubic-bezier(.16,1,.3,1),opacity .25s;width:640px}.n.l{opacity:0;pointer-events:none;transform:scale(1.1);transition:transform .25s .15s,opacity .15s}@media (max-width:680px){.n{border-radius:0;height:100%;width:100%}}.u{display:flex;flex-basis:min-content;flex-direction:column;flex-grow:1;flex-shrink:0}@keyframes d{0%{transform:scale(0)}50%{transform:scale(1.2)}to{transform:scale(1)}}.y{animation:d .25s ease-in-out;background:var(--color-highlight);border-radius:100%;color:#fff;font-size:8px;font-weight:700;height:12px;padding-top:1px;position:absolute;right:4px;top:4px;width:12px}.i{background-color:rgb(var(--color-background-subtle)/var(--alpha-lighter));flex-shrink:0;overflow:scroll;position:relative;transition:width .35s cubic-bezier(.16,1,.3,1),opacity .25s;width:200px}.i>*{transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.i.l{opacity:0;width:0}.i.l>*{transform:translate(-48px)}@media (max-width:680px){.i{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background-subtle)/var(--alpha-light));box-shadow:0 0 60px #00000026;height:100%;position:absolute;right:0;top:0}}.w{border-bottom:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));display:flex;gap:var(--space-1);padding:var(--space-2)}.k{-webkit-overflow-scrolling:touch;overflow:auto;overscroll-behavior:contain}.z{padding:8px 10px}.X{color:rgb(var(--color-foreground)/var(--alpha-light));padding:var(--space-2);position:absolute;width:200px}.X,.j{display:flex;flex-direction:column}.j{gap:2px;list-style:none;padding:0}.F,.j{margin:0}.F{font-size:16px;font-weight:400}.F,.I{padding:8px}.I{font-size:14px;margin:4px 0 0;opacity:.5}.I,.o{font-size:12px}.o{cursor:pointer;display:flex;padding:4px 8px;position:relative}.o:before{background-color:var(--color-highlight-transparent);border-radius:var(--space-1);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.o.g:before,.o:hover:before{opacity:1;transform:scale(1)}.o.g,.o:hover{color:var(--color-highlight)}.R{flex-grow:1}.R,.q{position:relative}.q{font-weight:700}.f{flex-grow:1}.f input{background:#0000;border:none;color:rgb(var(--color-foreground));font-family:var(--font-family);font-size:16px;height:100%;letter-spacing:-.25px;outline:none;width:100%}.b{color:rgb(var(--color-foreground)/var(--alpha-light));display:flex;flex-direction:column;gap:2px;line-height:1.3;list-style:none;margin:var(--space-2);margin-top:0;padding:0}.A,.b li{margin:0}.A{color:rgb(var(--color-foreground)/var(--alpha-lighter));font-size:12px;margin-top:var(--space-2);padding:0 18px}.a{border-radius:var(--space-2);color:inherit;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;padding:8px 10px;position:relative;text-decoration:none}.a:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";display:block;inset:0;opacity:0;position:absolute;transform:scale(.9);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.a.h:before,.a:hover:before{opacity:1;transform:scale(1)}}.a mark{background:#0000;color:var(--color-highlight)}.a u{background-color:var(--color-highlight-transparent);border-radius:2px;box-shadow:0 0 0 1px var(--color-highlight-transparent);text-decoration:none}.B{flex-grow:1}.s{margin-right:-8px;opacity:0;position:relative;transform:translate(-2px);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.h>.s,:hover>.s{opacity:1;transform:none}}.x{font-size:14px;margin:0;position:relative}.x code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);font-size:13px;padding:2px 4px}.t{color:rgb(var(--color-foreground)/var(--alpha-lighter));display:inline-flex;flex-wrap:wrap;font-size:12px;gap:var(--space-1);list-style:none;margin:0;padding:0;position:relative}.t li{white-space:nowrap}.t li:after{content:"/";display:inline;margin-left:var(--space-1)}.t li:last-child:after{content:"";display:none}.e{--space-1:4px;--space-2:calc(var(--space-1)*2);--space-3:calc(var(--space-2)*2);--space-4:calc(var(--space-3)*2);--space-5:calc(var(--space-4)*2);--alpha-light:.7;--alpha-lighter:.54;--alpha-lightest:.1;--color-highlight:var(--md-accent-fg-color,#526cfe);--color-highlight-transparent:var(--md-accent-fg-color--transparent,#526cfe1a);--border-radius-1:var(--space-1);--border-radius-2:var(--space-2);--border-radius-3:calc(var(--space-1) + var(--space-2));--font-family:var(--md-text-font-family,Inter,Roboto Flex,system-ui,sans-serif);--font-size:16px;--line-height:1.5;--letter-spacing:-.5px;-webkit-font-smoothing:antialiased;align-items:center;display:flex;font-family:var(--font-family);font-size:var(--font-size);height:100vh;justify-content:center;letter-spacing:var(--letter-spacing);line-height:var(--line-height);pointer-events:none;position:absolute;width:100vw}@media (pointer:coarse){.e{height:-webkit-fill-available}}.e *,.e :after,.e :before{box-sizing:border-box}';function Ks(e,{index$:t}){let r=Ue(),n=document.createElement("div");document.body.appendChild(n),n.style.position="fixed",n.style.height="100%",n.style.top="0",n.style.zIndex="4";let o=n.attachShadow({mode:"open"});o.appendChild(A("style",{},qs.toString()));try{Ya(r.search,{highlight:r.features.includes("search.highlight")}),me(t).subscribe(i=>{for(let a of i.items)a.location=new URL(a.location,r.base).toString();Ja(i,o)}),b(e,"click").subscribe(()=>{go()}),Tn("search").pipe(ke(1)).subscribe(()=>go())}catch(i){e.hidden=!0;let a=G("label[for=__search]");a.hidden=!0}return Ke}var Bs=_r(So());function Ys(e,{index$:t,location$:r}){return re([t,r.pipe(J(Ye()),L(n=>!!n.searchParams.get("h")))]).pipe(f(([n,o])=>_p(n.config)(o.searchParams.get("h"))),f(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,l=n(c);l.length>c.length&&o.set(s,l)}for(let[s,c]of o){let{childNodes:l}=A("span",null,c);s.replaceWith(...Array.from(l))}return{ref:e,nodes:o}}))}function _p(e){let t=e.separator.split("|").map(o=>o.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":o).join("|"),r=new RegExp(t,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/\s+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${o.split(r).map(a=>a.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&")).filter(a=>a.length>0).join("|")})`,"img");return a=>(0,Bs.default)(a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function yp(e,{viewport$:t,main$:r}){let n=e.closest(".md-grid"),o=n.offsetTop-n.parentElement.offsetTop;return re([r,t]).pipe(f(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),ie((i,a)=>i.height===a.height&&i.locked===a.locked))}function Ao(e,n){var o=n,{header$:t}=o,r=gr(o,["header$"]);let i=G(".md-sidebar__scrollwrap",e),{y:a}=wt(i);return j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=s.pipe(Xe(0,je));return l.pipe(pe(t)).subscribe({next([{height:u},{height:p}]){i.style.height=`${u-2*a}px`,e.style.top=`${p}px`},complete(){i.style.height="",e.style.top=""}}),l.pipe(Sr()).subscribe(()=>{for(let u of P(".md-nav__link--active[href]",e)){if(!u.clientHeight)continue;let p=u.closest(".md-sidebar__scrollwrap");if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2})}}}),me(P("label[tabindex]",e)).pipe(oe(u=>b(u,"click").pipe(Ie(ge),f(()=>u),Q(c)))).subscribe(u=>{let p=G(`[id="${u.htmlFor}"]`);G(`[aria-labelledby="${u.id}"]`).setAttribute("aria-expanded",`${p.checked}`)}),X("content.tooltips")&&me(P("abbr[title]",e)).pipe(oe(u=>Ge(u,{viewport$})),Q(c)).subscribe(),yp(e,r).pipe($(u=>s.next(u)),V(()=>s.complete()),f(u=>H({ref:e},u)))})}function Gs(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return $t(et(`${r}/releases/latest`).pipe(_e(()=>y),f(n=>({version:n.tag_name})),ot({})),et(r).pipe(_e(()=>y),f(n=>({stars:n.stargazers_count,forks:n.forks_count})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return et(r).pipe(f(n=>({repositories:n.public_repos})),ot({}))}}function Js(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return $t(et(`${r}/releases/permalink/latest`).pipe(_e(()=>y),f(({tag_name:n})=>({version:n})),ot({})),et(r).pipe(_e(()=>y),f(({star_count:n,forks_count:o})=>({stars:n,forks:o})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}function Xs(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,n]=t;return Gs(r,n)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,n]=t;return Js(r,n)}return y}var xp;function wp(e){return xp||(xp=j(()=>{let t=__md_get("__source",sessionStorage);if(t)return Y(t);if(Ee("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return y}return Xs(e.href).pipe($(n=>__md_set("__source",n,sessionStorage)))}).pipe(_e(()=>y),L(t=>Object.keys(t).length>0),f(t=>({facts:t})),se(1)))}function Zs(e){let t=G(":scope > :last-child",e);return j(()=>{let r=new I;return r.subscribe(({facts:n})=>{t.appendChild(bs(n)),t.classList.add("md-source__repository--active")}),wp(e).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Ep(e,{viewport$:t,header$:r}){return Re(document.body).pipe(g(()=>Sn(e,{header$:r,viewport$:t})),f(({offset:{y:n}})=>({hidden:n>=10})),fe("hidden"))}function Qs(e,t){return j(()=>{let r=new I;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(X("navigation.tabs.sticky")?Y({hidden:!1}):Ep(e,t)).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Tp(e,{viewport$:t,header$:r}){let n=new Map,o=P(".md-nav__link",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),l=Le(`[id="${c}"]`);typeof l!="undefined"&&n.set(s,l)}let i=r.pipe(fe("height"),f(({height:s})=>{let c=ht("main"),l=G(":scope > :first-child",c);return s+.9*(l.offsetTop-c.offsetTop)}),xe());return Re(document.body).pipe(fe("height"),g(s=>j(()=>{let c=[];return Y([...n].reduce((l,[u,p])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=p.tagName;)c.pop();let d=p.offsetTop;for(;!d&&p.parentElement;)p=p.parentElement,d=p.offsetTop;let m=p.offsetParent;for(;m;m=m.offsetParent)d+=m.offsetTop;return l.set([...c=[...c,u]].reverse(),d)},new Map))}).pipe(f(c=>new Map([...c].sort(([,l],[,u])=>l-u))),Ze(i),g(([c,l])=>t.pipe(Or(([u,p],{offset:{y:d},size:m})=>{let h=d+m.height>=Math.floor(s.height);for(;p.length;){let[,v]=p[0];if(v-l=d&&!h)p=[u.pop(),...p];else break}return[u,p]},[[],[...c]]),ie((u,p)=>u[0]===p[0]&&u[1]===p[1])))))).pipe(f(([s,c])=>({prev:s.map(([l])=>l),next:c.map(([l])=>l)})),J({prev:[],next:[]}),Pt(2,1),f(([s,c])=>s.prev.length{let i=new I,a=i.pipe(he(),ye(!0));if(i.subscribe(({prev:s,next:c})=>{for(let[l]of c)l.classList.remove("md-nav__link--passed"),l.classList.remove("md-nav__link--active");for(let[l,[u]]of s.entries())u.classList.add("md-nav__link--passed"),u.classList.toggle("md-nav__link--active",l===s.length-1)}),X("toc.follow")){let s=R(t.pipe(Be(1),f(()=>{})),t.pipe(Be(250),f(()=>"smooth")));i.pipe(L(({prev:c})=>c.length>0),Ze(n.pipe(Ie(ge))),pe(s)).subscribe(([[{prev:c}],l])=>{let[u]=c[c.length-1];if(u.offsetHeight){let p=ki(u);if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2,behavior:l})}}})}return X("navigation.tracking")&&t.pipe(Q(a),fe("offset"),Be(250),ke(1),Q(o.pipe(ke(1))),jt({delay:250}),pe(i)).subscribe(([,{prev:s}])=>{let c=Ye(),l=s[s.length-1];if(l&&l.length){let[u]=l,{hash:p}=new URL(u.href);c.hash!==p&&(c.hash=p,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),Tp(e,{viewport$:t,header$:r}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function Sp(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(f(({offset:{y:a}})=>a),Pt(2,1),f(([a,s])=>a>s&&s>0),ie()),i=r.pipe(f(({active:a})=>a));return re([i,o]).pipe(f(([a,s])=>!(a&&s)),ie(),Q(n.pipe(ke(1))),ye(!0),jt({delay:250}),f(a=>({hidden:a})))}function tc(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(Q(a),fe("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),b(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Sp(e,{viewport$:t,main$:n,target$:o}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))}function rc(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,t.port&&(e.port=t.port),e}function Op(e,t){let r=new Map;for(let n of P("url",e)){let o=G("loc",n),i=[rc(new URL(o.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",n)){let s=a.getAttribute("href");s!=null&&i.push(rc(new URL(s),t))}}return r}function dr(e){return ns(new URL("sitemap.xml",e)).pipe(f(t=>Op(t,new URL(e))),_e(()=>Y(new Map)),xe())}function nc({document$:e}){let t=new Map;e.pipe(g(()=>P("link[rel=alternate]")),f(r=>new URL(r.href)),L(r=>!t.has(r.toString())),oe(r=>dr(r).pipe(f(n=>[r,n]),_e(()=>y)))).subscribe(([r,n])=>{t.set(r.toString().replace(/\/$/,""),n)}),b(document.body,"click").pipe(L(r=>!r.metaKey&&!r.ctrlKey),g(r=>{if(r.target instanceof Element){let n=r.target.closest("a");if(n&&!n.target){let o=[...t].find(([p])=>n.href.startsWith(`${p}/`));if(typeof o=="undefined")return y;let[i,a]=o,s=Ye();if(s.href.startsWith(i))return y;let c=Ue(),l=s.href.replace(c.base,"");l=`${i}/${l}`;let u=a.has(l.split("#")[0])?new URL(l,c.base):new URL(i);return r.preventDefault(),Y(u)}}return y})).subscribe(r=>dt(r,!0))}var Co=_r(Mo());function Lp(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function oc({alert$:e}){Co.default.isSupported()&&new U(t=>{new Co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Lp(G(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe($(t=>{t.trigger.focus()}),f(()=>Bt("clipboard.copied"))).subscribe(e)}function ic(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let n=new URL(r.href);return n.search=n.hash="",t.has(`${n}`)?(e.preventDefault(),Y(r)):y}function ac(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function sc(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let n=t.getAttribute(r);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){t[r]=t[r];break}}return Y(e)}function Mp(e){for(let n of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...X("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let o=Le(n),i=Le(n,e);typeof o!="undefined"&&typeof i!="undefined"&&o.replaceWith(i)}let t=ac(document);for(let[n,o]of ac(e))t.has(n)?t.delete(n):document.head.appendChild(o);for(let n of t.values()){let o=n.getAttribute("name");o!=="theme-color"&&o!=="color-scheme"&&n.remove()}let r=ht("container");return nt(P("script",r)).pipe(g(n=>{let o=e.createElement("script");if(n.src){for(let i of n.getAttributeNames())o.setAttribute(i,n.getAttribute(i));return n.replaceWith(o),new U(i=>{o.onload=()=>i.complete()})}else return o.textContent=n.textContent,n.replaceWith(o),y}),he(),ye(document))}function cc({sitemap$:e,location$:t,viewport$:r,progress$:n}){if(location.protocol==="file:")return Ke;Y(document).subscribe(sc);let o=b(document.body,"click").pipe(Ze(e),g(([s,c])=>ic(s,c)),f(({href:s})=>new URL(s)),xe()),i=b(window,"popstate").pipe(f(Ye),xe());o.pipe(pe(r)).subscribe(([s,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",s)}),R(o,i).subscribe(t);let a=t.pipe(fe("pathname"),g(s=>En(s,{progress$:n}).pipe(_e(()=>(dt(s,!0),y)))),g(sc),g(Mp),xe());return R(a.pipe(pe(t,(s,c)=>c)),a.pipe(g(()=>t),fe("hash")),t.pipe(ie((s,c)=>s.pathname===c.pathname&&s.hash===c.hash),g(()=>o),$(()=>history.back()))).subscribe(s=>{var c,l;history.state!==null||!s.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",es(s.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),b(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(fe("offset"),Be(100)).subscribe(({offset:s})=>{history.replaceState(s,"")}),X("navigation.instant.prefetch")&&R(b(document.body,"mousemove"),b(document.body,"focusin")).pipe(Ze(e),g(([s,c])=>ic(s,c)),Be(25),Yn(({href:s})=>s),cn(s=>{let c=document.createElement("link");return c.rel="prefetch",c.href=s.toString(),document.head.appendChild(c),b(c,"load").pipe(f(()=>c),Me(1))})).subscribe(s=>s.remove()),a}function lc(e){var u;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:n,currentBaseURL:o}=e,i=(u=Ho(o))==null?void 0:u.pathname;if(i===void 0)return;let a=kp(n.pathname,i);if(a===void 0)return;let s=Cp(t.keys());if(!t.has(s))return;let c=Ho(a,s);if(!c||!t.has(c.href))return;let l=Ho(a,r);if(l)return l.hash=n.hash,l.search=n.search,l}function Ho(e,t){try{return new URL(e,t)}catch(r){return}}function kp(e,t){if(e.startsWith(t))return e.slice(t.length)}function Ap(e,t){let r=Math.min(e.length,t.length),n;for(n=0;ny)),n=r.pipe(f(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));r.pipe(f(o=>new Map(o.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),g(o=>b(document.body,"click").pipe(L(i=>!i.metaKey&&!i.ctrlKey),pe(n),g(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&o.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&o.get(c)===a?y:(i.preventDefault(),Y(new URL(c)))}}return y}),g(i=>dr(i).pipe(f(a=>{var s;return(s=lc({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:Ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(o=>dt(o,!0)),re([r,n]).subscribe(([o,i])=>{G(".md-header__topic").appendChild(_s(o,i))}),e.pipe(g(()=>n)).subscribe(o=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let c=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let l of c)for(let u of o.aliases.concat(o.version))if(new RegExp(l,"i").test(u)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let c of Ee("outdated"))c.hidden=!1})}function pc({document$:e,viewport$:t}){e.pipe(g(()=>P(".md-ellipsis")),oe(r=>Et(r).pipe(Q(e.pipe(ke(1))),L(n=>n),f(()=>r),Me(1))),L(r=>r.offsetWidth{let n=r.innerText,o=r.closest("a")||r;return o.title=n,X("content.tooltips")?Ge(o,{viewport$:t}).pipe(Q(e.pipe(ke(1))),V(()=>o.removeAttribute("title"))):y})).subscribe(),X("content.tooltips")&&e.pipe(g(()=>P(".md-status")),oe(r=>Ge(r,{viewport$:t}))).subscribe()}function fc({document$:e,tablet$:t}){e.pipe(g(()=>P(".md-toggle--indeterminate")),$(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>b(r,"change").pipe(Xn(()=>r.classList.contains("md-toggle--indeterminate")),f(()=>r))),pe(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function Hp(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function mc({document$:e}){e.pipe(g(()=>P("[data-md-scrollfix]")),$(t=>t.removeAttribute("data-md-scrollfix")),L(Hp),oe(t=>b(t,"touchstart").pipe(f(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));function $p(){return location.protocol==="file:"?ar(`${new URL("search.js",Mn.base)}`).pipe(f(()=>__index),_e(()=>Ke),se(1)):et(new URL("search.json",Mn.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var vt=Si(),Ur=Za(),hr=ts(Ur),hc=Xa(),ze=cs(),$o=Ir("(min-width: 60em)"),vc=Ir("(min-width: 76.25em)"),bc=rs(),Mn=Ue(),gc=Le(".md-search")?$p():Ke,Po=new I;oc({alert$:Po});nc({document$:vt});var Io=new I,_c=dr(Mn.base);X("navigation.instant")&&cc({sitemap$:_c,location$:Ur,viewport$:ze,progress$:Io}).subscribe(vt);var dc;((dc=Mn.version)==null?void 0:dc.provider)==="mike"&&uc({document$:vt});R(Ur,hr).pipe(It(125)).subscribe(()=>{Eo("drawer",!1),Eo("search",!1)});hc.pipe(L(({mode:e,meta:t})=>e==="global"&&!t)).subscribe(e=>{switch(e.type){case",":case"p":let t=document.querySelector("link[rel=prev]");t instanceof HTMLLinkElement&&dt(t);break;case".":case"n":let r=document.querySelector("link[rel=next]");r instanceof HTMLLinkElement&&dt(r);break;case"/":let n=document.querySelector("[data-md-component=search] button");n instanceof HTMLButtonElement&&n.click();break;case"Enter":let o=xt();o instanceof HTMLLabelElement&&o.click()}});pc({viewport$:ze,document$:vt});fc({document$:vt,tablet$:$o});mc({document$:vt});var Lt=Us(ht("header"),{viewport$:ze}),Fr=vt.pipe(f(()=>ht("main")),g(e=>Ws(e,{viewport$:ze,header$:Lt})),se(1)),Pp=R(...Ee("consent").map(e=>us(e,{target$:hr})),...Ee("dialog").map(e=>Fs(e,{alert$:Po})),...Ee("palette").map(e=>Vs(e)),...Ee("progress").map(e=>zs(e,{progress$:Io})),...Ee("search").map(e=>Ks(e,{index$:gc})),...Ee("source").map(e=>Zs(e))),Ip=j(()=>R(...Ee("announce").map(e=>ls(e)),...Ee("content").map(e=>js(e,{sitemap$:_c,viewport$:ze,target$:hr,print$:bc})),...Ee("content").map(e=>X("search.highlight")?Ys(e,{index$:gc,location$:Ur}):y),...Ee("header").map(e=>Ns(e,{viewport$:ze,header$:Lt,main$:Fr})),...Ee("header-title").map(e=>Ds(e,{viewport$:ze,header$:Lt})),...Ee("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?yo(vc,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr})):yo($o,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr}))),...Ee("tabs").map(e=>Qs(e,{viewport$:ze,header$:Lt})),...Ee("toc").map(e=>ec(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})),...Ee("top").map(e=>tc(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})))),yc=vt.pipe(g(()=>Ip),Rt(Pp),se(1));yc.subscribe();window.document$=vt;window.location$=Ur;window.target$=hr;window.keyboard$=hc;window.viewport$=ze;window.tablet$=$o;window.screen$=vc;window.print$=bc;window.alert$=Po;window.progress$=Io;window.component$=yc;})(); -/*! update cache: 20260331044451 */ +/*! update cache: 20260331053143 */ diff --git a/en/assets/javascripts/bundle.c2b142ea.min.js b/en/assets/javascripts/bundle.c2b142ea.min.js index c88793c85..7f19f1463 100644 --- a/en/assets/javascripts/bundle.c2b142ea.min.js +++ b/en/assets/javascripts/bundle.c2b142ea.min.js @@ -1,4 +1,4 @@ "use strict";(()=>{var xc=Object.create;var kn=Object.defineProperty,wc=Object.defineProperties,Ec=Object.getOwnPropertyDescriptor,Tc=Object.getOwnPropertyDescriptors,Sc=Object.getOwnPropertyNames,Dr=Object.getOwnPropertySymbols,Oc=Object.getPrototypeOf,An=Object.prototype.hasOwnProperty,Fo=Object.prototype.propertyIsEnumerable;var jo=(e,t,r)=>t in e?kn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,H=(e,t)=>{for(var r in t||(t={}))An.call(t,r)&&jo(e,r,t[r]);if(Dr)for(var r of Dr(t))Fo.call(t,r)&&jo(e,r,t[r]);return e},He=(e,t)=>wc(e,Tc(t));var gr=(e,t)=>{var r={};for(var n in e)An.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Dr)for(var n of Dr(e))t.indexOf(n)<0&&Fo.call(e,n)&&(r[n]=e[n]);return r};var Cn=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Lc=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Sc(t))!An.call(e,o)&&o!==r&&kn(e,o,{get:()=>t[o],enumerable:!(n=Ec(t,o))||n.enumerable});return e};var _r=(e,t,r)=>(r=e!=null?xc(Oc(e)):{},Lc(t||!e||!e.__esModule?kn(r,"default",{value:e,enumerable:!0}):r,e));var Uo=(e,t,r)=>new Promise((n,o)=>{var i=c=>{try{s(r.next(c))}catch(l){o(l)}},a=c=>{try{s(r.throw(c))}catch(l){o(l)}},s=c=>c.done?n(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var Do=Cn((Hn,No)=>{(function(e,t){typeof Hn=="object"&&typeof No!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Hn,(function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(_){return!!(_&&_!==document&&_.nodeName!=="HTML"&&_.nodeName!=="BODY"&&"classList"in _&&"contains"in _.classList)}function c(_){var de=_.type,be=_.tagName;return!!(be==="INPUT"&&a[de]&&!_.readOnly||be==="TEXTAREA"&&!_.readOnly||_.isContentEditable)}function l(_){_.classList.contains("focus-visible")||(_.classList.add("focus-visible"),_.setAttribute("data-focus-visible-added",""))}function u(_){_.hasAttribute("data-focus-visible-added")&&(_.classList.remove("focus-visible"),_.removeAttribute("data-focus-visible-added"))}function p(_){_.metaKey||_.altKey||_.ctrlKey||(s(r.activeElement)&&l(r.activeElement),n=!0)}function d(_){n=!1}function m(_){s(_.target)&&(n||c(_.target))&&l(_.target)}function h(_){s(_.target)&&(_.target.classList.contains("focus-visible")||_.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(_.target))}function v(_){document.visibilityState==="hidden"&&(o&&(n=!0),x())}function x(){document.addEventListener("mousemove",E),document.addEventListener("mousedown",E),document.addEventListener("mouseup",E),document.addEventListener("pointermove",E),document.addEventListener("pointerdown",E),document.addEventListener("pointerup",E),document.addEventListener("touchmove",E),document.addEventListener("touchstart",E),document.addEventListener("touchend",E)}function w(){document.removeEventListener("mousemove",E),document.removeEventListener("mousedown",E),document.removeEventListener("mouseup",E),document.removeEventListener("pointermove",E),document.removeEventListener("pointerdown",E),document.removeEventListener("pointerup",E),document.removeEventListener("touchmove",E),document.removeEventListener("touchstart",E),document.removeEventListener("touchend",E)}function E(_){_.target.nodeName&&_.target.nodeName.toLowerCase()==="html"||(n=!1,w())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",d,!0),document.addEventListener("pointerdown",d,!0),document.addEventListener("touchstart",d,!0),document.addEventListener("visibilitychange",v,!0),x(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var So=Cn((M0,vs)=>{"use strict";var Gu=/["'&<>]/;vs.exports=Ju;function Ju(e){var t=""+e,r=Gu.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i{(function(t,r){typeof jr=="object"&&typeof Lo=="object"?Lo.exports=r():typeof define=="function"&&define.amd?define([],r):typeof jr=="object"?jr.ClipboardJS=r():t.ClipboardJS=r()})(jr,function(){return(function(){var e={686:(function(n,o,i){"use strict";i.d(o,{default:function(){return vr}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),p=i.n(u);function d(B){try{return document.execCommand(B)}catch(C){return!1}}var m=function(C){var k=p()(C);return d("cut"),k},h=m;function v(B){var C=document.documentElement.getAttribute("dir")==="rtl",k=document.createElement("textarea");k.style.fontSize="12pt",k.style.border="0",k.style.padding="0",k.style.margin="0",k.style.position="absolute",k.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return k.style.top="".concat(D,"px"),k.setAttribute("readonly",""),k.value=B,k}var x=function(C,k){var D=v(C);k.container.appendChild(D);var W=p()(D);return d("copy"),D.remove(),W},w=function(C){var k=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=x(C,k):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=x(C.value,k):(D=p()(C),d("copy")),D},E=w;function _(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?_=function(k){return typeof k}:_=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},_(B)}var de=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},k=C.action,D=k===void 0?"copy":k,W=C.container,Z=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Z!==void 0)if(Z&&_(Z)==="object"&&Z.nodeType===1){if(D==="copy"&&Z.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(Z.hasAttribute("readonly")||Z.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return E(We,{container:W});if(Z)return D==="cut"?h(Z):E(Z,{container:W})},be=de;function M(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?M=function(k){return typeof k}:M=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},M(B)}function O(B,C){if(!(B instanceof C))throw new TypeError("Cannot call a class as a function")}function N(B,C){for(var k=0;k0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof W.action=="function"?W.action:this.defaultAction,this.target=typeof W.target=="function"?W.target:this.defaultTarget,this.text=typeof W.text=="function"?W.text:this.defaultText,this.container=M(W.container)==="object"?W.container:document.body}},{key:"listenClick",value:function(W){var Z=this;this.listener=l()(W,"click",function(We){return Z.onClick(We)})}},{key:"onClick",value:function(W){var Z=W.delegateTarget||W.currentTarget,We=this.action(Z)||"copy",Gt=be({action:We,container:this.container,target:this.target(Z),text:this.text(Z)});this.emit(Gt?"success":"error",{action:We,text:Gt,trigger:Z,clearSelection:function(){Z&&Z.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(W){return Yt("action",W)}},{key:"defaultTarget",value:function(W){var Z=Yt("target",W);if(Z)return document.querySelector(Z)}},{key:"defaultText",value:function(W){return Yt("text",W)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(W){var Z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return E(W,Z)}},{key:"cut",value:function(W){return h(W)}},{key:"isSupported",value:function(){var W=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Z=typeof W=="string"?[W]:W,We=!!document.queryCommandSupported;return Z.forEach(function(Gt){We=We&&!!document.queryCommandSupported(Gt)}),We}}]),k})(s()),vr=Mt}),828:(function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a}),438:(function(n,o,i){var a=i(828);function s(u,p,d,m,h){var v=l.apply(this,arguments);return u.addEventListener(d,v,h),{destroy:function(){u.removeEventListener(d,v,h)}}}function c(u,p,d,m,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof d=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,d,m,h)}))}function l(u,p,d,m){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&m.call(u,h)}}n.exports=c}),879:(function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}}),370:(function(n,o,i){var a=i(879),s=i(438);function c(d,m,h){if(!d&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(d))return l(d,m,h);if(a.nodeList(d))return u(d,m,h);if(a.string(d))return p(d,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(d,m,h){return d.addEventListener(m,h),{destroy:function(){d.removeEventListener(m,h)}}}function u(d,m,h){return Array.prototype.forEach.call(d,function(v){v.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(d,function(v){v.removeEventListener(m,h)})}}}function p(d,m,h){return s(document.body,d,m,h)}n.exports=c}),817:(function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}n.exports=o}),279:(function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c0&&i[i.length-1])&&(l[0]===6||l[0]===2)){r=0;continue}if(l[0]===3&&(!i||l[1]>i[0]&&l[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function te(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function ne(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||c(m,v)})},h&&(o[m]=h(o[m])))}function c(m,h){try{l(n[m](h))}catch(v){d(i[0][3],v)}}function l(m){m.value instanceof kt?Promise.resolve(m.value.v).then(u,p):d(i[0][2],m)}function u(m){c("next",m)}function p(m){c("throw",m)}function d(m,h){m(h),i.shift(),i.length&&c(i[0][0],i[0][1])}}function zo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof $e=="function"?$e(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),o(s,c,a.done,a.value)})}}function o(i,a,s,c){Promise.resolve(c).then(function(l){i({value:l,done:s})},a)}}function F(e){return typeof e=="function"}function Jt(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Vr=Jt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: `+r.map(function(n,o){return o+1+") "+n.toString()}).join(` `):"",this.name="UnsubscriptionError",this.errors=r}});function ct(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var rt=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=$e(a),c=s.next();!c.done;c=s.next()){var l=c.value;l.remove(this)}}catch(v){t={error:v}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(F(u))try{u()}catch(v){i=v instanceof Vr?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var d=$e(p),m=d.next();!m.done;m=d.next()){var h=m.value;try{qo(h)}catch(v){i=i!=null?i:[],v instanceof Vr?i=ne(ne([],te(i)),te(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{m&&!m.done&&(o=d.return)&&o.call(d)}finally{if(n)throw n.error}}}if(i)throw new Vr(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)qo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&ct(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&ct(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var Pn=rt.EMPTY;function zr(e){return e instanceof rt||e&&"closed"in e&&F(e.remove)&&F(e.add)&&F(e.unsubscribe)}function qo(e){F(e)?e():e.unsubscribe()}var Je={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Xt={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Pn:(this.currentObservers=null,s.push(r),new rt(function(){n.currentObservers=null,ct(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new Qo(r,n)},t})(U);var Qo=(function(e){ue(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Pn},t})(I);var Un=(function(e){ue(t,e);function t(r){var n=e.call(this)||this;return n._value=r,n}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var n=e.prototype._subscribe.call(this,r);return!n.closed&&r.next(this._value),n},t.prototype.getValue=function(){var r=this,n=r.hasError,o=r.thrownError,i=r._value;if(n)throw o;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(I);var xr={now:function(){return(xr.delegate||Date).now()},delegate:void 0};var wr=(function(e){ue(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=xr);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.schedule.call(this,r,n):(this.delay=n,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,n){return n>0||this.closed?e.prototype.execute.call(this,r,n):this._execute(r,n)},t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.flush(this),0)},t})(tr);var ri=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(rr);var Wn=new ri(ti);var ni=(function(e){ue(t,e);function t(r,n){var o=e.call(this,r,n)||this;return o.scheduler=r,o.work=n,o}return t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!==null&&o>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=er.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&n===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(er.cancelAnimationFrame(n),r._scheduled=void 0)},t})(tr);var oi=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n;r?n=r.id:(n=this._scheduled,this._scheduled=void 0);var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t})(rr);var je=new oi(ni);var y=new U(function(e){return e.complete()});function Br(e){return e&&F(e.schedule)}function Vn(e){return e[e.length-1]}function _t(e){return F(Vn(e))?e.pop():void 0}function qe(e){return Br(Vn(e))?e.pop():void 0}function Yr(e,t){return typeof Vn(e)=="number"?e.pop():t}var nr=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function Gr(e){return F(e==null?void 0:e.then)}function Jr(e){return F(e[Qt])}function Xr(e){return Symbol.asyncIterator&&F(e==null?void 0:e[Symbol.asyncIterator])}function Zr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Rc(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qr=Rc();function en(e){return F(e==null?void 0:e[Qr])}function tn(e){return Vo(this,arguments,function(){var r,n,o,i;return Wr(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,kt(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,kt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,kt(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function rn(e){return F(e==null?void 0:e.getReader)}function q(e){if(e instanceof U)return e;if(e!=null){if(Jr(e))return jc(e);if(nr(e))return Fc(e);if(Gr(e))return Uc(e);if(Xr(e))return ii(e);if(en(e))return Nc(e);if(rn(e))return Dc(e)}throw Zr(e)}function jc(e){return new U(function(t){var r=e[Qt]();if(F(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Fc(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?L(function(o,i){return e(o,i,n)}):Oe,Me(1),r?ot(t):wi(function(){return new on}))}}function Gn(e){return e<=0?function(){return y}:S(function(t,r){var n=[];t.subscribe(T(r,function(o){n.push(o),e=2,!0))}function xe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new I}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u,p,d,m=0,h=!1,v=!1,x=function(){p==null||p.unsubscribe(),p=void 0},w=function(){x(),u=d=void 0,h=v=!1},E=function(){var _=u;w(),_==null||_.unsubscribe()};return S(function(_,de){m++,!v&&!h&&x();var be=d=d!=null?d:r();de.add(function(){m--,m===0&&!v&&!h&&(p=Jn(E,c))}),be.subscribe(de),!u&&m>0&&(u=new Ct({next:function(M){return be.next(M)},error:function(M){v=!0,x(),p=Jn(w,o,M),be.error(M)},complete:function(){h=!0,x(),p=Jn(w,a),be.complete()}}),q(_).subscribe(u))})(l)}}function Jn(e,t){for(var r=[],n=2;ne.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function G(e,t=document){let r=Le(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function Le(e,t=document){return t.querySelector(e)||void 0}function xt(){var e,t,r,n;return(n=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?n:void 0}var il=R(b(document.body,"focusin"),b(document.body,"focusout")).pipe(Be(1),J(void 0),f(()=>xt()||document.body),se(1));function ir(e){return il.pipe(f(t=>e.contains(t)),ie())}function Ft(e,t){let{matches:r}=matchMedia("(hover)");return j(()=>(r?R(b(e,"mouseenter").pipe(f(()=>!0)),b(e,"mouseleave").pipe(f(()=>!1))):R(b(e,"touchstart").pipe(f(()=>!0)),b(e,"touchend").pipe(f(()=>!1)),b(e,"touchcancel").pipe(f(()=>!1)))).pipe(t?Tr(o=>Ve(+!o*t)):Oe,J(!0,e.matches(":hover"))))}function Oi(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Oi(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Oi(n,o);return n}function Li(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function ar(e){let t=A("script",{src:e});return j(()=>(document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(g(()=>zn(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(f(()=>{}),V(()=>document.head.removeChild(t)),Me(1))))}var Mi=new I,al=j(()=>typeof ResizeObserver=="undefined"?ar("https://unpkg.com/resize-observer-polyfill"):Y(void 0)).pipe(f(()=>new ResizeObserver(e=>e.forEach(t=>Mi.next(t)))),g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Ae(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Re(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return al.pipe($(r=>r.observe(t)),g(r=>Mi.pipe(L(n=>n.target===t),V(()=>r.unobserve(t)))),f(()=>Ae(e)),J(Ae(e)))}function Mr(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ki(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Ai(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function wt(e){return{x:e.offsetLeft,y:e.offsetTop}}function Ci(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Hi(e){return R(b(window,"load"),b(window,"resize")).pipe(Xe(0,je),f(()=>wt(e)),J(wt(e)))}function ln(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ut(e){return R(b(e,"scroll"),b(window,"scroll"),b(window,"resize")).pipe(Xe(0,je),f(()=>ln(e)),J(ln(e)))}var $i=new I,sl=j(()=>Y(new IntersectionObserver(e=>{for(let t of e)$i.next(t)},{threshold:0}))).pipe(g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Et(e){return sl.pipe($(t=>t.observe(e)),g(t=>$i.pipe(L(({target:r})=>r===e),V(()=>t.unobserve(e)),f(({isIntersecting:r})=>r))))}var cl=Object.create,la=Object.defineProperty,ll=Object.getOwnPropertyDescriptor,ul=Object.getOwnPropertyNames,pl=Object.getPrototypeOf,fl=Object.prototype.hasOwnProperty,ml=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),dl=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ul(t))!fl.call(e,o)&&o!==r&&la(e,o,{get:()=>t[o],enumerable:!(n=ll(t,o))||n.enumerable});return e},hl=(e,t,r)=>(r=e!=null?cl(pl(e)):{},dl(t||!e||!e.__esModule?la(r,"default",{value:e,enumerable:!0}):r,e)),vl=ml((e,t)=>{var r="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,c=/^0o[0-7]+$/i,l=parseInt,u=typeof global=="object"&&global&&global.Object===Object&&global,p=typeof self=="object"&&self&&self.Object===Object&&self,d=u||p||Function("return this")(),m=Object.prototype,h=m.toString,v=Math.max,x=Math.min,w=function(){return d.Date.now()};function E(O,N,ee){var le,ce,Ne,bt,De,st,tt=0,Yt=!1,Mt=!1,vr=!0;if(typeof O!="function")throw new TypeError(r);N=M(N)||0,_(ee)&&(Yt=!!ee.leading,Mt="maxWait"in ee,Ne=Mt?v(M(ee.maxWait)||0,N):Ne,vr="trailing"in ee?!!ee.trailing:vr);function B(Te){var gt=le,br=ce;return le=ce=void 0,tt=Te,bt=O.apply(br,gt),bt}function C(Te){return tt=Te,De=setTimeout(W,N),Yt?B(Te):bt}function k(Te){var gt=Te-st,br=Te-tt,Ro=N-gt;return Mt?x(Ro,Ne-br):Ro}function D(Te){var gt=Te-st,br=Te-tt;return st===void 0||gt>=N||gt<0||Mt&&br>=Ne}function W(){var Te=w();if(D(Te))return Z(Te);De=setTimeout(W,k(Te))}function Z(Te){return De=void 0,vr&&le?B(Te):(le=ce=void 0,bt)}function We(){De!==void 0&&clearTimeout(De),tt=0,le=st=ce=De=void 0}function Gt(){return De===void 0?bt:Z(w())}function Nr(){var Te=w(),gt=D(Te);if(le=arguments,ce=this,st=Te,gt){if(De===void 0)return C(st);if(Mt)return De=setTimeout(W,N),B(st)}return De===void 0&&(De=setTimeout(W,N)),bt}return Nr.cancel=We,Nr.flush=Gt,Nr}function _(O){var N=typeof O;return!!O&&(N=="object"||N=="function")}function de(O){return!!O&&typeof O=="object"}function be(O){return typeof O=="symbol"||de(O)&&h.call(O)==o}function M(O){if(typeof O=="number")return O;if(be(O))return n;if(_(O)){var N=typeof O.valueOf=="function"?O.valueOf():O;O=_(N)?N+"":N}if(typeof O!="string")return O===0?O:+O;O=O.replace(i,"");var ee=s.test(O);return ee||c.test(O)?l(O.slice(2),ee?2:8):a.test(O)?n:+O}t.exports=E}),yn,K,ua,pa,Nt,Pi,fa,ma,da,lo,to,ro,bl,Ar={},ha=[],gl=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Pr=Array.isArray;function pt(e,t){for(var r in t)e[r]=t[r];return e}function uo(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function Wt(e,t,r){var n,o,i,a={};for(i in t)i=="key"?n=t[i]:i=="ref"?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?yn.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return fn(e,a,n,o,null)}function fn(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o!=null?o:++ua,__i:-1,__u:0};return o==null&&K.vnode!=null&&K.vnode(i),i}function ft(e){return e.children}function at(e,t){this.props=e,this.context=t}function cr(e,t){if(t==null)return e.__?cr(e.__,e.__i+1):null;for(var r;ts&&Nt.sort(ma),e=Nt.shift(),s=Nt.length,e.__d&&(r=void 0,n=void 0,o=(n=(t=e).__v).__e,i=[],a=[],t.__P&&((r=pt({},n)).__v=n.__v+1,K.vnode&&K.vnode(r),po(t.__P,r,n,t.__n,t.__P.namespaceURI,32&n.__u?[o]:null,i,o!=null?o:cr(n),!!(32&n.__u),a),r.__v=n.__v,r.__.__k[r.__i]=r,_a(i,r,a),n.__e=n.__=null,r.__e!=o&&va(r)));vn.__r=0}function ba(e,t,r,n,o,i,a,s,c,l,u){var p,d,m,h,v,x,w,E=n&&n.__k||ha,_=t.length;for(c=_l(r,t,E,c,_),p=0;p<_;p++)(m=r.__k[p])!=null&&(d=m.__i==-1?Ar:E[m.__i]||Ar,m.__i=p,x=po(e,m,d,o,i,a,s,c,l,u),h=m.__e,m.ref&&d.ref!=m.ref&&(d.ref&&fo(d.ref,null,m),u.push(m.ref,m.__c||h,m)),v==null&&h!=null&&(v=h),(w=!!(4&m.__u))||d.__k===m.__k?c=ga(m,c,e,w):typeof m.type=="function"&&x!==void 0?c=x:h&&(c=h.nextSibling),m.__u&=-7);return r.__e=v,c}function _l(e,t,r,n,o){var i,a,s,c,l,u=r.length,p=u,d=0;for(e.__k=new Array(o),i=0;i0?fn(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):a).__=e,a.__b=e.__b+1,s=null,(l=a.__i=yl(a,r,c,p))!=-1&&(p--,(s=r[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(o>u?d--:oc?d--:d++,a.__u|=4))):e.__k[i]=null;if(p)for(i=0;i(u?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return a}return-1}function Ri(e,t,r){t[0]=="-"?e.setProperty(t,r!=null?r:""):e[t]=r==null?"":typeof r!="number"||gl.test(t)?r:r+"px"}function un(e,t,r,n,o){var i,a;e:if(t=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)r&&t in r||Ri(e.style,t,"");if(r)for(t in r)n&&r[t]==n[t]||Ri(e.style,t,r[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(da,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=r,r?n?r.u=n.u:(r.u=lo,e.addEventListener(t,i?ro:to,i)):e.removeEventListener(t,i?ro:to,i);else{if(o=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=r!=null?r:"";break e}catch(s){}typeof r=="function"||(r==null||r===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&r==1?"":r))}}function ji(e){return function(t){if(this.l){var r=this.l[t.type+e];if(t.t==null)t.t=lo++;else if(t.t0?e:Pr(e)?e.map(ya):pt({},e)}function xl(e,t,r,n,o,i,a,s,c){var l,u,p,d,m,h,v,x=r.props,w=t.props,E=t.type;if(E=="svg"?o="http://www.w3.org/2000/svg":E=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(l=0;l=r.__.length&&r.__.push({}),r.__[e]}function bn(e){return $r=1,Tl(Ta,e)}function Tl(e,t,r){var n=mo(Hr++,2);if(n.t=e,!n.__c&&(n.__=[r?r(t):Ta(void 0,t),function(s){var c=n.__N?n.__N[0]:n.__[0],l=n.t(c,s);c!==l&&(n.__N=[l,n.__[1]],n.__c.setState({}))}],n.__c=ve,!ve.__f)){var o=function(s,c,l){if(!n.__c.__H)return!0;var u=n.__c.__H.__.filter(function(d){return!!d.__c});if(u.every(function(d){return!d.__N}))return!i||i.call(this,s,c,l);var p=n.__c.props!==s;return u.forEach(function(d){if(d.__N){var m=d.__[0];d.__=d.__N,d.__N=void 0,m!==d.__[0]&&(p=!0)}}),i&&i.call(this,s,c,l)||p};ve.__f=!0;var i=ve.shouldComponentUpdate,a=ve.componentWillUpdate;ve.componentWillUpdate=function(s,c,l){if(this.__e){var u=i;i=void 0,o(s,c,l),i=u}a&&a.call(this,s,c,l)},ve.shouldComponentUpdate=o}return n.__N||n.__}function mt(e,t){var r=mo(Hr++,3);!we.__s&&Ea(r.__H,t)&&(r.__=e,r.u=t,ve.__H.__h.push(r))}function Vt(e){return $r=5,ur(function(){return{current:e}},[])}function ur(e,t){var r=mo(Hr++,7);return Ea(r.__H,t)&&(r.__=e(),r.__H=t,r.__h=e),r.__}function Sl(e,t){return $r=8,ur(function(){return e},t)}function Ol(){for(var e;e=wa.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(mn),e.__H.__h.forEach(oo),e.__H.__h=[]}catch(t){e.__H.__h=[],we.__e(t,e.__v)}}we.__b=function(e){ve=null,Ui&&Ui(e)},we.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),zi&&zi(e,t)},we.__r=function(e){Ni&&Ni(e),Hr=0;var t=(ve=e.__c).__H;t&&(Zn===ve?(t.__h=[],ve.__h=[],t.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(t.__h.forEach(mn),t.__h.forEach(oo),t.__h=[],Hr=0)),Zn=ve},we.diffed=function(e){Di&&Di(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(wa.push(t)!==1&&Fi===we.requestAnimationFrame||((Fi=we.requestAnimationFrame)||Ll)(Ol)),t.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),Zn=ve=null},we.__c=function(e,t){t.some(function(r){try{r.__h.forEach(mn),r.__h=r.__h.filter(function(n){return!n.__||oo(n)})}catch(n){t.some(function(o){o.__h&&(o.__h=[])}),t=[],we.__e(n,r.__v)}}),Wi&&Wi(e,t)},we.unmount=function(e){Vi&&Vi(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(n){try{mn(n)}catch(o){t=o}}),r.__H=void 0,t&&we.__e(t,r.__v))};var qi=typeof requestAnimationFrame=="function";function Ll(e){var t,r=function(){clearTimeout(n),qi&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,35);qi&&(t=requestAnimationFrame(r))}function mn(e){var t=ve,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),ve=t}function oo(e){var t=ve;e.__c=e.__(),ve=t}function Ea(e,t){return!e||e.length!==t.length||t.some(function(r,n){return r!==e[n]})}function Ta(e,t){return typeof t=="function"?t(e):t}function Ml(e,t){for(var r in t)e[r]=t[r];return e}function Ki(e,t){for(var r in e)if(r!=="__source"&&!(r in t))return!0;for(var n in t)if(n!=="__source"&&e[n]!==t[n])return!0;return!1}function Bi(e,t){this.props=e,this.context=t}(Bi.prototype=new at).isPureReactComponent=!0,Bi.prototype.shouldComponentUpdate=function(e,t){return Ki(this.props,e)||Ki(this.state,t)};var Yi=K.__b;K.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),Yi&&Yi(e)};var Yx=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,kl=K.__e;K.__e=function(e,t,r,n){if(e.then){for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return t.__e==null&&(t.__e=r.__e,t.__k=r.__k),o.__c(e,t)}kl(e,t,r,n)};var Gi=K.unmount;function Sa(e,t,r){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=Ml({},e)).__c!=null&&(e.__c.__P===r&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return Sa(n,t,r)})),e}function Oa(e,t,r){return e&&r&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return Oa(n,t,r)}),e.__c&&e.__c.__P===t&&(e.__e&&r.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=r)),e}function Qn(){this.__u=0,this.o=null,this.__b=null}function La(e){var t=e.__.__c;return t&&t.__a&&t.__a(e)}function pn(){this.i=null,this.l=null}K.unmount=function(e){var t=e.__c;t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),Gi&&Gi(e)},(Qn.prototype=new at).__c=function(e,t){var r=t.__c,n=this;n.o==null&&(n.o=[]),n.o.push(r);var o=La(n.__v),i=!1,a=function(){i||(i=!0,r.__R=null,o?o(s):s())};r.__R=a;var s=function(){if(!--n.__u){if(n.state.__a){var c=n.state.__a;n.__v.__k[0]=Oa(c,c.__c.__P,c.__c.__O)}var l;for(n.setState({__a:n.__b=null});l=n.o.pop();)l.forceUpdate()}};n.__u++||32&t.__u||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(a,a)},Qn.prototype.componentWillUnmount=function(){this.o=[]},Qn.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=Sa(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__a&&Wt(ft,null,e.fallback);return o&&(o.__u&=-33),[Wt(ft,null,t.__a?null:e.children),o]};var Ji=function(e,t,r){if(++r[1]===r[0]&&e.l.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.l.size))for(r=e.i;r;){for(;r.length>3;)r.pop()();if(r[1]Object.freeze({get current(){return t.current}}),[])}var Nl=typeof globalThis<"u"&&typeof navigator<"u"&&typeof document<"u";function Dl(e,...t){var r;(r=e==null?void 0:e.addEventListener)==null||r.call(e,...t)}function Wl(e,...t){var r;(r=e==null?void 0:e.removeEventListener)==null||r.call(e,...t)}var Vl=(e,t)=>Object.hasOwn(e,t),zl=()=>!0,ql=()=>!1;function Kl(e=!1){let t=Vt(e),r=Sl(()=>t.current,[]);return mt(()=>(t.current=!0,()=>{t.current=!1}),[]),r}function Bl(e,...t){let r=Kl(),n=ka(t[1]),o=ur(()=>function(...i){r()&&(typeof n.current=="function"?n.current.apply(this,i):typeof n.current.handleEvent=="function"&&n.current.handleEvent.apply(this,i))},[]);mt(()=>{let i=Yl(e)?e.current:e;if(!i)return;let a=t.slice(2);return Dl(i,t[0],o,...a),()=>{Wl(i,t[0],o,...a)}},[e,t[0]])}function Yl(e){return e!==null&&typeof e=="object"&&Vl(e,"current")}var Gl=e=>typeof e=="function"?e:typeof e=="string"?t=>t.key===e:e?zl:ql,Jl=Nl?globalThis:null;function Aa(e,t,r=[],n={}){let{event:o="keydown",target:i=Jl,eventOptions:a}=n,s=ka(t),c=ur(()=>{let l=Gl(e);return function(u){l(u)&&s.current.call(this,u)}},r);Bl(i,o,c,a)}function Ca(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t1)St--;else{for(var e,t=!1;kr!==void 0;){var r=kr;for(kr=void 0,io++;r!==void 0;){var n=r.o;if(r.o=void 0,r.f&=-3,!(8&r.f)&&Pa(r))try{r.c()}catch(o){t||(e=o,t=!0)}r=n}}if(io=0,St--,t)throw e}}function Ql(e){if(St>0)return e();St++;try{return e()}finally{xn()}}var ae=void 0;function Ha(e){var t=ae;ae=void 0;try{return e()}finally{ae=t}}var kr=void 0,St=0,io=0,gn=0;function $a(e){if(ae!==void 0){var t=e.n;if(t===void 0||t.t!==ae)return t={i:0,S:e,p:ae.s,n:void 0,t:ae,e:void 0,x:void 0,r:t},ae.s!==void 0&&(ae.s.n=t),ae.s=t,e.n=t,32&ae.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=ae.s,t.n=void 0,ae.s.n=t,ae.s=t),t}}function Ce(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Ce.prototype.brand=Zl;Ce.prototype.h=function(){return!0};Ce.prototype.S=function(e){var t=this,r=this.t;r!==e&&e.e===void 0&&(e.x=r,this.t=e,r!==void 0?r.e=e:Ha(function(){var n;(n=t.W)==null||n.call(t)}))};Ce.prototype.U=function(e){var t=this;if(this.t!==void 0){var r=e.e,n=e.x;r!==void 0&&(r.x=n,e.e=void 0),n!==void 0&&(n.e=r,e.x=void 0),e===this.t&&(this.t=n,n===void 0&&Ha(function(){var o;(o=t.Z)==null||o.call(t)}))}};Ce.prototype.subscribe=function(e){var t=this;return qt(function(){var r=t.value,n=ae;ae=void 0;try{e(r)}finally{ae=n}},{name:"sub"})};Ce.prototype.valueOf=function(){return this.value};Ce.prototype.toString=function(){return this.value+""};Ce.prototype.toJSON=function(){return this.value};Ce.prototype.peek=function(){var e=ae;ae=void 0;try{return this.value}finally{ae=e}};Object.defineProperty(Ce.prototype,"value",{get:function(){var e=$a(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(io>100)throw new Error("Cycle detected");this.v=e,this.i++,gn++,St++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{xn()}}}});function Ot(e,t){return new Ce(e,t)}function Pa(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function Ia(e){for(var t=e.s;t!==void 0;t=t.n){var r=t.S.n;if(r!==void 0&&(t.r=r),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function Ra(e){for(var t=e.s,r=void 0;t!==void 0;){var n=t.p;t.i===-1?(t.S.U(t),n!==void 0&&(n.n=t.n),t.n!==void 0&&(t.n.p=n)):r=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=n}e.s=r}function Kt(e,t){Ce.call(this,void 0),this.x=e,this.s=void 0,this.g=gn-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Kt.prototype=new Ce;Kt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===gn))return!0;if(this.g=gn,this.f|=1,this.i>0&&!Pa(this))return this.f&=-2,!0;var e=ae;try{Ia(this),ae=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(r){this.v=r,this.f|=16,this.i++}return ae=e,Ra(this),this.f&=-2,!0};Kt.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}Ce.prototype.S.call(this,e)};Kt.prototype.U=function(e){if(this.t!==void 0&&(Ce.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};Kt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(Kt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=$a(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function ta(e,t){return new Kt(e,t)}function ja(e){var t=e.u;if(e.u=void 0,typeof t=="function"){St++;var r=ae;ae=void 0;try{t()}catch(n){throw e.f&=-2,e.f|=8,ho(e),n}finally{ae=r,xn()}}}function ho(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,ja(e)}function eu(e){if(ae!==this)throw new Error("Out-of-order effect");Ra(this),ae=e,this.f&=-2,8&this.f&&ho(this),xn()}function pr(e,t){this.x=e,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32,this.name=t==null?void 0:t.name}pr.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.u=t)}finally{e()}};pr.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,ja(this),Ia(this),St++;var e=ae;return ae=this,eu.bind(this,e)};pr.prototype.N=function(){2&this.f||(this.f|=2,this.o=kr,kr=this)};pr.prototype.d=function(){this.f|=8,1&this.f||ho(this)};pr.prototype.dispose=function(){this.d()};function qt(e,t){var r=new pr(e,t);try{r.c()}catch(o){throw r.d(),o}var n=r.d.bind(r);return n[Symbol.dispose]=n,n}var Fa,vo,eo,Ua=[];qt(function(){Fa=this.N})();function fr(e,t){K[e]=t.bind(null,K[e]||function(){})}function _n(e){eo&&eo(),eo=e&&e.S()}function Na(e){var t=this,r=e.data,n=ru(r);n.value=r;var o=ur(function(){for(var s=t,c=t.__v;c=c.__;)if(c.__c){c.__c.__$f|=4;break}var l=ta(function(){var m=n.value.value;return m===0?0:m===!0?"":m||""}),u=ta(function(){return!Array.isArray(l.value)&&!pa(l.value)}),p=qt(function(){if(this.N=Da,u.value){var m=l.value;s.__v&&s.__v.__e&&s.__v.__e.nodeType===3&&(s.__v.__e.data=m)}}),d=t.__$u.d;return t.__$u.d=function(){p(),d.call(this)},[u,l]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}Na.displayName="ReactiveTextNode";Object.defineProperties(Ce.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Na},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});fr("__b",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),typeof t.type=="string"){var r,n=t.props;for(var o in n)if(o!=="children"){var i=n[o];i instanceof Ce&&(r||(t.__np=r={}),r[o]=i,n[o]=i.peek())}}e(t)});fr("__r",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(t),t.type!==ft){_n();var r,n=t.__c;n&&(n.__$f&=-2,(r=n.__$u)===void 0&&(n.__$u=r=(function(o){var i;return qt(function(){i=this}),i.c=function(){n.__$f|=1,n.setState({})},i})())),vo=n,_n(r)}e(t)});fr("__e",function(e,t,r,n){typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0,e(t,r,n)});fr("diffed",function(e,t){typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0;var r;if(typeof t.type=="string"&&(r=t.__e)){var n=t.__np,o=t.props;if(n){var i=r.U;if(i)for(var a in i){var s=i[a];s!==void 0&&!(a in n)&&(s.d(),i[a]=void 0)}else i={},r.U=i;for(var c in n){var l=i[c],u=n[c];l===void 0?(l=tu(r,c,u,o),i[c]=l):l.o(u,o)}}}e(t)});function tu(e,t,r,n){var o=t in e&&e.ownerSVGElement===void 0,i=Ot(r);return{o:function(a,s){i.value=a,n=s},d:qt(function(){this.N=Da;var a=i.value.value;n[t]!==a&&(n[t]=a,o?e[t]=a:a?e.setAttribute(t,a):e.removeAttribute(t))})}}fr("unmount",function(e,t){if(typeof t.type=="string"){var r=t.__e;if(r){var n=r.U;if(n){r.U=void 0;for(var o in n){var i=n[o];i&&i.d()}}}}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)});fr("__h",function(e,t,r,n){(n<3||n===9)&&(t.__$f|=2),e(t,r,n)});at.prototype.shouldComponentUpdate=function(e,t){var r=this.__$u,n=r&&r.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){var i=2&this.__$f;if(!(n||i||4&this.__$f)||1&this.__$f)return!0}else if(!(n||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};function ru(e,t){return bn(function(){return Ot(e,t)})[0]}var nu=function(e){queueMicrotask(function(){queueMicrotask(e)})};function ou(){Ql(function(){for(var e;e=Ua.shift();)Fa.call(e)})}function Da(){Ua.push(this)===1&&(K.requestAnimationFrame||nu)(ou)}var ao=[0];for(let e=0;e<32;e++)ao.push(ao[e]|1<>>5]>>>e&1}set(e){this.data[e>>>5]|=1<<(e&31)}forEach(e){let t=this.size&31;for(let r=0;r{var r;return(r=t.tags)==null?void 0:r.length})&&(matchMedia("(max-width: 768px)").matches||Wa())}function Dt(){Qe.value=He(H({},Qe.value),{hideSearch:!Qe.value.hideSearch})}function Wa(){Qe.value=He(H({},Qe.value),{hideFilters:!Qe.value.hideFilters})}function dn(){return Qe.value.selectedItem}function so(e){Qe.value=He(H({},Qe.value),{selectedItem:e})}function su(){var e,t;return(t=(e=lr.value)==null?void 0:e.items)!=null?t:[]}function wn(){return typeof Se.value.input=="string"?Se.value.input:""}function Va(e){let t=za();e.length&&!t.length?Se.value=He(H({},Se.value),{page:void 0,input:e}):!e.length&&t.length?Se.value=He(H({},Se.value),{page:void 0,input:{type:"operator",data:{operator:"not",operands:[]}}}):Se.value=He(H({},Se.value),{page:void 0,input:e})}function cu(){typeof it.value.pagination.next<"u"&&(Se.value=He(H({},Se.value),{page:it.value.pagination.next}))}function lu(e){let t=Se.value.filter.input;if("type"in t&&t.type==="operator"){for(let r of t.data.operands)if("type"in r&&r.type==="value"&&typeof r.data.value=="string"&&r.data.value===e)return!0}return!1}function za(){let e=Se.value.filter.input,t=[];if("type"in e&&e.type==="operator")for(let r of e.data.operands)"type"in r&&r.type==="value"&&typeof r.data.value=="string"&&t.push(r.data.value);return t}function uu(e){let t=Se.value.filter.input,r=[];if("type"in t&&t.type==="operator")for(let n of t.data.operands)"type"in n&&n.type==="value"&&typeof n.data.value=="string"&&r.push(n.data.value);if(r.includes(e)){let n=r.indexOf(e);n>-1&&r.splice(n,1)}else r.push(e);Se.value=He(H({},Se.value),{page:void 0,filter:He(H({},Se.value.filter),{input:{type:"operator",data:{operator:"and",operands:r.map(n=>({type:"value",data:{field:"tags",value:n}}))}}})}),Va(wn())}function pu(){return it.value.items}function fu(){return it.value.total}function mu(){var e;for(let t of(e=it.value.aggregations)!=null?e:[])if(t.type==="term")return t.data.value;return[]}function sr(){return Qe.value.hideSearch}function du(){return Qe.value.hideFilters}function qa(){var e;return(e=Ka.value.highlight)!=null?e:!1}var Qe=Ot({hideSearch:!0,hideFilters:!0,selectedItem:0}),Ka=Ot({}),lr=Ot(),na=Ot(),Se=Ot({input:"",filter:{input:{type:"operator",data:{operator:"and",operands:[]}},aggregation:{input:[{type:"term",data:{field:"tags"}}]}}}),it=Ot({items:[],query:{select:{documents:new ra(0),terms:new ra(0)},values:[]},pagination:{total:0}});function hu(e,t,r){for(let n=0;tr&&t(0,o,r,r=i);continue;case 62:e.charCodeAt(r+1)===47?t(2,--o,r,r=i+1):hu(e,r,n)?t(3,o,r,r=i+1):t(1,o++,r,r=i+1)}i>r&&t(0,o,r,i)}function bu(e,t=0,r=e.length){let n=++t;e:for(let l=0;n{let i=[],a=[],{onElement:s,onText:c=gu}=typeof r=="function"?{onElement:r}:r,l=0,u=0;return e(t,(p,d,m,h)=>{if(p===0)i[l++]=c(t,m,h),a[u++]={value:null,depth:d};else if(p&1&&(a[u++]={value:bu(t,m,h),depth:d}),p&2)for(let v=0;u>=0;v++){let{value:x,depth:w}=a[--u];if(w>d)continue;let E=i.slice(l-=v,l+v);i[l++]=s(x,E),u++;break}},n,o),i.slice(0,l)}}function yu(e){return e.replace(/[&<>]/g,t=>{switch(t.charCodeAt(0)){case 38:return"&";case 60:return"<";case 62:return">"}})}function hn(e){return e.replace(/&(amp|[lg]t);/g,t=>{switch(t.charCodeAt(1)){case 97:return"&";case 108:return"<";case 103:return">"}})}function xu(e,t){return{start:e.start+t,end:e.end+t,value:e.value}}function wu(e,t,r){return e.slice(t,r)}function Eu(e){let{onHighlight:t,onText:r=wu}=typeof e=="function"?{onHighlight:e}:e;return(n,o,i=0,a=n.length)=>{var l;let s=[],c=(l=o==null?void 0:o.ranges)!=null?l:[];for(let u=0,p=i;ua)break;let m=c[u].end;if(mi&&s.push(r(n,i,d));let{value:h}=c[u];s.push(t(n,{start:d,end:i=m,value:h}))}return i{let o=n.data;switch(o.type){case 1:na.value=!0;break;case 3:typeof o.data.pagination.prev<"u"?it.value=He(H({},it.value),{pagination:o.data.pagination,items:[...it.value.items,...o.data.items]}):(it.value=o.data,so(0));break}},qt(()=>{lr.value&&r.postMessage({type:0,data:lr.value})}),qt(()=>{na.value&&r.postMessage({type:2,data:Se.value})})}var oa={container:"p",hidden:"m"};function ku(e){return z("div",{class:zt(oa.container,{[oa.hidden]:e.hidden}),onClick:()=>Dt()})}var ia={container:"r",disabled:"c"};function co(e){return z("button",{class:zt(ia.container,{[ia.disabled]:!e.onClick}),onClick:e.onClick,children:e.children})}var aa=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Au=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),sa=e=>{let t=Au(e);return t.charAt(0).toUpperCase()+t.slice(1)},Cu=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),Hu={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},$u=c=>{var l=c,{color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,children:o,iconNode:i,class:a=""}=l,s=gr(l,["color","size","strokeWidth","absoluteStrokeWidth","children","iconNode","class"]);return Wt("svg",H(He(H({},Hu),{width:String(t),height:t,stroke:e,"stroke-width":n?Number(r)*24/Number(t):r,class:["lucide",a].join(" ")}),s),[...i.map(([u,p])=>Wt(u,p)),...Cr(o)])},bo=(e,t)=>{let r=a=>{var s=a,{class:n="",children:o}=s,i=gr(s,["class","children"]);return Wt($u,He(H({},i),{iconNode:t,class:Cu(`lucide-${aa(sa(e))}`,`lucide-${aa(e)}`,n)}),o)};return r.displayName=sa(e),r},Pu=bo("corner-down-left",[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4",key:"6o5b7l"}],["path",{d:"m9 10-5 5 5 5",key:"1kshq7"}]]),Iu=bo("list-filter",[["path",{d:"M2 5h20",key:"1fs1ex"}],["path",{d:"M6 12h12",key:"8npq4p"}],["path",{d:"M9 19h6",key:"456am0"}]]),Ru=bo("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Gx=hl(vl(),1);function ju({threshold:e=0,root:t=null,rootMargin:r="0%",freezeOnceVisible:n=!1,initialIsIntersecting:o=!1,onChange:i}={}){var a;let[s,c]=bn(null),[l,u]=bn(()=>({isIntersecting:o,entry:void 0})),p=Vt();p.current=i;let d=((a=l.entry)==null?void 0:a.isIntersecting)&&n;mt(()=>{if(!s||!("IntersectionObserver"in window)||d)return;let v,x=new IntersectionObserver(w=>{let E=Array.isArray(x.thresholds)?x.thresholds:[x.thresholds];w.forEach(_=>{let de=_.isIntersecting&&E.some(be=>_.intersectionRatio>=be);u({isIntersecting:de,entry:_}),p.current&&p.current(de,_),de&&n&&v&&(v(),v=void 0)})},{threshold:e,root:t,rootMargin:r});return x.observe(s),()=>{x.disconnect()}},[s,JSON.stringify(e),t,r,d,n]);let m=Vt(null);mt(()=>{var v;!s&&(v=l.entry)!=null&&v.target&&!n&&!d&&m.current!==l.entry.target&&(m.current=l.entry.target,u({isIntersecting:o,entry:void 0}))},[s,l.entry,n,d,o]);let h=[c,!!l.isIntersecting,l.entry];return h.ref=h[0],h.isIntersecting=h[1],h.entry=h[2],h}var lt={container:"n",hidden:"l",content:"u",pop:"d",badge:"y",sidebar:"i",controls:"w",results:"k",loadmore:"z"};function Fu(e){let{isIntersecting:t,ref:r}=ju({threshold:0});mt(()=>{t&&cu()},[t]);let n=Vt(null);mt(()=>{n.current&&typeof Se.value.page>"u"&&n.current.scrollTo({top:0,behavior:"smooth"})},[Se.value]);let o=za();return z("div",{class:zt(lt.container,{[lt.hidden]:e.hidden}),children:[z("div",{class:lt.content,children:[z("div",{class:lt.controls,children:[z(co,{onClick:Dt,children:z(Ru,{})}),z(Nu,{focus:!e.hidden}),z(co,{onClick:Wa,children:[z(Iu,{}),o.length>0&&z("span",{class:lt.badge,children:o.length})]})]}),z("div",{class:lt.results,ref:n,children:[z(Du,{keyboard:!e.hidden}),z("div",{class:lt.loadmore,ref:r})]})]}),z("div",{class:zt(lt.sidebar,{[lt.hidden]:du()}),children:z(Uu,{})})]})}var Tt={container:"X",list:"j",heading:"F",title:"I",item:"o",active:"g",value:"R",count:"q"};function Uu(e){let t=mu();return t.sort((r,n)=>n.node.count-r.node.count),z("div",{class:Tt.container,children:[z("h3",{class:Tt.heading,children:"Filters"}),z("h4",{class:Tt.title,children:"Tags"}),z("ol",{class:Tt.list,children:t.map(r=>z("li",{class:zt(Tt.item,{[Tt.active]:lu(r.node.value)}),onClick:()=>uu(r.node.value),children:[z("span",{class:Tt.value,children:r.node.value}),z("span",{class:Tt.count,children:r.node.count})]}))})]})}var ca={container:"f"};function Nu(e){let t=Vt(null);return mt(()=>{var r,n;e.focus?(r=t.current)==null||r.focus():(n=t.current)==null||n.blur()},[e.focus]),z("div",{class:ca.container,children:z("input",{ref:t,type:"text",class:ca.content,value:hn(wn()),onInput:r=>Va(yu(r.currentTarget.value)),autocapitalize:"off",autocomplete:"off",autocorrect:"off",placeholder:"Search",spellcheck:!1,role:"combobox"})})}var ut={container:"b",heading:"A",item:"a",active:"h",wrapper:"B",actions:"s",title:"x",path:"t"};function Ga(){let[e,t]=bn(!1);return mt(()=>{let r=()=>t(!0),n=()=>t(!1);return document.addEventListener("compositionstart",r),document.addEventListener("compositionend",n),()=>{document.removeEventListener("compositionstart",r),document.removeEventListener("compositionend",n)}},[]),e}function Du(e){var s;let t=su(),r=pu(),n=dn(),o=Vt([]),i=Ga();mt(()=>{let c=o.current[n];c&&c.scrollIntoView({block:"center",behavior:"smooth"})},[n]),Aa(e.keyboard,c=>{if(i)return;let l=dn();c.key==="ArrowDown"?(c.preventDefault(),so(Math.min(l+1,r.length-1))):c.key==="ArrowUp"&&(c.preventDefault(),so(Math.max(l-1,0)))},[e.keyboard,i]);let a=(s=fu())!=null?s:0;return z(ft,{children:[r.length>0&&z("h3",{class:ut.heading,children:[z("span",{class:ut.bubble,children:new Intl.NumberFormat("en-US").format(a)})," ","results"]}),z("ol",{class:ut.container,children:r.map((c,l)=>{var m;let u=Ba(t[c.id].title,c.matches.find(({field:h})=>h==="title")),p=Mu((m=t[c.id].path)!=null?m:[],c.matches.find(({field:h})=>h==="path")),d=t[c.id].location;if(qa()){let h=encodeURIComponent(wn()),[v,x]=d.split("#",2);d=`${v}?h=${h.replace(/%20/g,"+")}`,typeof x<"u"&&(d+=`#${x}`)}return z("li",{children:z("a",{ref:h=>{o.current[l]=h},href:d,onClick:()=>Dt(),class:zt(ut.item,{[ut.active]:l===dn()}),children:[z("div",{class:ut.wrapper,children:[z("h2",{class:ut.title,children:u}),z("menu",{class:ut.path,children:p.map(h=>z("li",{children:h}))})]}),z("nav",{class:ut.actions,children:z(co,{children:z(Pu,{})})})]})})})})]})}var Wu={container:"e"};function Vu(e){let t=Ga();return Aa(!0,r=>{var n,o,i,a,s;if(!t)if((r.metaKey||r.ctrlKey)&&r.key==="k")r.preventDefault(),Dt();else if((r.metaKey||r.ctrlKey)&&r.key==="j")document.body.classList.toggle("dark");else if(r.key==="Enter"&&!sr()){r.preventDefault();let c=dn(),l=(o=(n=it.value)==null?void 0:n.items[c])==null?void 0:o.id;if((a=(i=lr.value)==null?void 0:i.items[l])!=null&&a.location){Dt();let u=(s=lr.value)==null?void 0:s.items[l].location;if(qa()){let p=encodeURIComponent(wn()),[d,m]=u.split("#",2);u=`${d}?h=${p.replace(/%20/g,"+")}`,typeof m<"u"&&(u+=`#${m}`)}window.location.href=u}}else r.key==="Escape"&&!sr()&&(r.preventDefault(),Dt())},[t]),z("div",{class:Wu.container,children:[z(ku,{hidden:sr()}),z(Fu,{hidden:sr()})]})}function Ja(e,t){au(e),El(z(Vu,{}),t)}function go(){Dt()}function zu(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function qu(){return R(b(window,"compositionstart").pipe(f(()=>!0)),b(window,"compositionend").pipe(f(()=>!1))).pipe(J(!1))}function Xa(){let e=b(window,"keydown").pipe(f(t=>({mode:sr()?"global":"search",type:t.key,meta:t.ctrlKey||t.metaKey,claim(){t.preventDefault(),t.stopPropagation()}})),L(({mode:t,type:r})=>{if(t==="global"){let n=xt();if(typeof n!="undefined")return!zu(n,r)}return!0}),xe());return qu().pipe(g(t=>t?y:e))}function Ye(){return new URL(location.href)}function dt(e,t=!1){if(X("navigation.instant")&&!t){let r=A("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function Za(){return new I}function Qa(){return location.hash.slice(1)}function es(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function _o(e){return R(b(window,"hashchange"),e).pipe(f(Qa),J(Qa()),L(t=>t.length>0),se(1))}function ts(e){return _o(e).pipe(f(t=>Le(`[id="${t}"]`)),L(t=>typeof t!="undefined"))}function Ir(e){let t=matchMedia(e);return an(r=>t.addListener(()=>r(t.matches))).pipe(J(t.matches))}function rs(){let e=matchMedia("print");return R(b(window,"beforeprint").pipe(f(()=>!0)),b(window,"afterprint").pipe(f(()=>!1))).pipe(J(e.matches))}function yo(e,t){return e.pipe(g(r=>r?t():y))}function xo(e,t){return new U(r=>{let n=new XMLHttpRequest;return n.open("GET",`${e}`),n.responseType="blob",n.addEventListener("load",()=>{n.status>=200&&n.status<300?(r.next(n.response),r.complete()):r.error(new Error(n.statusText))}),n.addEventListener("error",()=>{r.error(new Error("Network error"))}),n.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(n.addEventListener("progress",o=>{var i;if(o.lengthComputable)t.progress$.next(o.loaded/o.total*100);else{let a=(i=n.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(o.loaded/+a*100)}}),t.progress$.next(5)),n.send(),()=>n.abort()})}function et(e,t){return xo(e,t).pipe(g(r=>r.text()),f(r=>JSON.parse(r)),se(1))}function En(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/html")),se(1))}function ns(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/xml")),se(1))}var wo={drawer:G("[data-md-toggle=drawer]"),search:G("[data-md-toggle=search]")};function Eo(e,t){wo[e].checked!==t&&wo[e].click()}function Tn(e){let t=wo[e];return b(t,"change").pipe(f(()=>t.checked),J(t.checked))}function os(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function is(){return R(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(f(os),J(os()))}function as(){return{width:innerWidth,height:innerHeight}}function ss(){return b(window,"resize",{passive:!0}).pipe(f(as),J(as()))}function cs(){return re([is(),ss()]).pipe(f(([e,t])=>({offset:e,size:t})),se(1))}function Sn(e,{viewport$:t,header$:r}){let n=t.pipe(fe("size")),o=re([n,r]).pipe(f(()=>wt(e)));return re([r,t,o]).pipe(f(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}var Ku=G("#__config"),mr=JSON.parse(Ku.textContent);mr.base=`${new URL(mr.base,Ye())}`;function Ue(){return mr}function X(e){return mr.features.includes(e)}function Bt(e,t){return typeof t!="undefined"?mr.translations[e].replace("#",t.toString()):mr.translations[e]}function ht(e,t=document){return G(`[data-md-component=${e}]`,t)}function Ee(e,t=document){return P(`[data-md-component=${e}]`,t)}function Bu(e){let t=G(".md-typeset > :first-child",e);return b(t,"click",{once:!0}).pipe(f(()=>G(".md-typeset",e)),f(r=>({hash:__md_hash(r.innerHTML)})))}function ls(e){if(!X("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=G(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return j(()=>{let t=new I;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Bu(e).pipe($(r=>t.next(r)),V(()=>t.complete()),f(r=>H({ref:e},r)))})}function Yu(e,{target$:t}){return t.pipe(f(r=>({hidden:r!==e})))}function us(e,t){let r=new I;return r.subscribe(({hidden:n})=>{e.hidden=n}),Yu(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))}function To(e,t){return t==="inline"?A("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})):A("div",{class:"md-tooltip",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"}))}function On(...e){return A("div",{class:"md-tooltip2",role:"dialog"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function ps(...e){return A("div",{class:"md-tooltip2",role:"tooltip"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function fs(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("a",{href:r,class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}else return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("span",{class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}function ms(e){return A("button",{class:"md-code__button",title:Bt("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function ds(){return A("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function hs(){return A("nav",{class:"md-code__nav"})}var Xu=_r(So());function bs(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Li(r):r)))}function Oo(e){let t=`tabbed-control tabbed-control--${e}`;return A("div",{class:t,hidden:!0},A("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function gs(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function Zu(e){var n;let t=Ue(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:`${r}`,class:"md-version__link"},e.title,((n=t.version)==null?void 0:n.alias)&&e.aliases.length>0&&A("span",{class:"md-version__alias"},e.aliases[0])))}function _s(e,t){var n;let r=Ue();return e=e.filter(o=>{var i;return!((i=o.properties)!=null&&i.hidden)}),A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":Bt("select.version")},t.title,((n=r.version)==null?void 0:n.alias)&&t.aliases.length>0&&A("span",{class:"md-version__alias"},t.aliases[0])),A("ul",{class:"md-version__list"},e.map(Zu)))}var Qu=0;function ep(e,t=250){let r=re([ir(e),Ft(e,t)]).pipe(f(([o,i])=>o||i),ie()),n=j(()=>Ai(e)).pipe(oe(Ut),Lr(1),Ze(r),f(()=>Ci(e)));return r.pipe(Sr(o=>o),g(()=>re([r,n])),f(([o,i])=>({active:o,offset:i})),xe())}function Rr(e,t,r=250){let{content$:n,viewport$:o}=t,i=`__tooltip2_${Qu++}`;return j(()=>{let a=new I,s=new Un(!1);a.pipe(he(),ye(!1)).subscribe(s);let c=s.pipe(Tr(u=>Ve(+!u*250,Wn)),ie(),g(u=>u?n:y),$(u=>u.id=i),xe());re([a.pipe(f(({active:u})=>u)),c.pipe(g(u=>Ft(u,250)),J(!1))]).pipe(f(u=>u.some(p=>p))).subscribe(s);let l=s.pipe(L(u=>u),pe(c,o),f(([u,p,{size:d}])=>{let m=e.getBoundingClientRect(),h=m.width/2;if(p.role==="tooltip")return{x:h,y:8+m.height};if(m.y>=d.height/2){let{height:v}=Ae(p);return{x:h,y:-16-v}}else return{x:h,y:16+m.height}}));return re([c,a,l]).subscribe(([u,{offset:p},d])=>{u.style.setProperty("--md-tooltip-host-x",`${p.x}px`),u.style.setProperty("--md-tooltip-host-y",`${p.y}px`),u.style.setProperty("--md-tooltip-x",`${d.x}px`),u.style.setProperty("--md-tooltip-y",`${d.y}px`),u.classList.toggle("md-tooltip2--top",d.y<0),u.classList.toggle("md-tooltip2--bottom",d.y>=0)}),s.pipe(L(u=>u),pe(c,(u,p)=>p),L(u=>u.role==="tooltip")).subscribe(u=>{let p=Ae(G(":scope > *",u));u.style.setProperty("--md-tooltip-width",`${p.width}px`),u.style.setProperty("--md-tooltip-tail","0px")}),s.pipe(ie(),Ie(je),pe(c)).subscribe(([u,p])=>{p.classList.toggle("md-tooltip2--active",u)}),re([s.pipe(L(u=>u)),c]).subscribe(([u,p])=>{p.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),s.pipe(L(u=>!u)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ep(e,r).pipe($(u=>a.next(u)),V(()=>a.complete()),f(u=>H({ref:e},u)))})}function Ge(e,{viewport$:t},r=document.body){return Rr(e,{content$:new U(n=>{let o=e.title,i=ps(o);return n.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",o)}}),viewport$:t},0)}function tp(e,t){let r=j(()=>re([Hi(e),Ut(t)])).pipe(f(([{x:n,y:o},i])=>{let{width:a,height:s}=Ae(e);return{x:n-i.x+a/2,y:o-i.y+s/2}}));return ir(e).pipe(g(n=>r.pipe(f(o=>({active:n,offset:o})),Me(+!n||1/0))))}function ys(e,t,{target$:r}){let[n,o]=Array.from(e.children);return j(()=>{let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),Et(e).pipe(Q(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),R(i.pipe(L(({active:s})=>s)),i.pipe(Be(250),L(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Xe(16,je)).subscribe(({active:s})=>{n.classList.toggle("md-tooltip--active",s)}),i.pipe(Lr(125,je),L(()=>!!e.offsetParent),f(()=>e.offsetParent.getBoundingClientRect()),f(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),b(o,"click").pipe(Q(a),L(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),b(o,"mousedown").pipe(Q(a),pe(i)).subscribe(([s,{active:c}])=>{var l;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(l=xt())==null||l.blur()}}),r.pipe(Q(a),L(s=>s===n),It(125)).subscribe(()=>e.focus()),tp(e,t).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function rp(e){let t=Ue();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate){let n=e.closest("[class|=language]");if(n)for(let o of Array.from(n.classList)){if(!o.startsWith("language-"))continue;let[,i]=o.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return P(r.join(", "),e)}function np(e){let t=[];for(let r of rp(e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let l=i.splitText(a.index);i=l.splitText(s.length),t.push(l)}else{i.textContent=s,t.push(i);break}}}}return t}function xs(e,t){t.append(...Array.from(e.childNodes))}function Ln(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let s of np(t)){let[,c]=s.textContent.match(/\((\d+)\)/);Le(`:scope > li:nth-child(${c})`,e)&&(a.set(c,fs(c,i)),s.replaceWith(a.get(c)))}return a.size===0?y:j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=[];for(let[u,p]of a)l.push([G(".md-typeset",p),G(`:scope > li:nth-child(${u})`,e)]);return n.pipe(Q(c)).subscribe(u=>{e.hidden=!u,e.classList.toggle("md-annotation-list",u);for(let[p,d]of l)u?xs(p,d):xs(d,p)}),R(...[...a].map(([,u])=>ys(u,t,{target$:r}))).pipe(V(()=>s.complete()),xe())})}function ws(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return ws(t)}}function Es(e,t){return j(()=>{let r=ws(e);return typeof r!="undefined"?Ln(r,e,t):y})}var Ss=_r(Mo());var op=0,Ts=R(b(window,"keydown").pipe(f(()=>!0)),R(b(window,"keyup"),b(window,"contextmenu")).pipe(f(()=>!1))).pipe(J(!1),se(1));function Os(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Os(t)}}function ip(e){return Re(e).pipe(f(({width:t})=>({scrollable:Mr(e).width>t})),fe("scrollable"))}function Ls(e,t){let{matches:r}=matchMedia("(hover)"),n=j(()=>{let o=new I,i=o.pipe(Gn(1));o.subscribe(({scrollable:m})=>{m&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[],s=e.closest("pre"),c=s.closest("[id]"),l=c?c.id:op++;s.id=`__code_${l}`;let u=[],p=e.closest(".highlight");if(p instanceof HTMLElement){let m=Os(p);if(typeof m!="undefined"&&(p.classList.contains("annotate")||X("content.code.annotate"))){let h=Ln(m,e,t);u.push(Re(p).pipe(Q(i),f(({width:v,height:x})=>v&&x),ie(),g(v=>v?h:y)))}}let d=P(":scope > span[id]",e);if(d.length&&(e.classList.add("md-code__content"),e.closest(".select")||X("content.code.select")&&!e.closest(".no-select"))){let m=+d[0].id.split("-").pop(),h=ds();a.push(h),X("content.tooltips")&&u.push(Ge(h,{viewport$}));let v=b(h,"click").pipe(Or(M=>!M,!1),$(()=>h.blur()),xe());v.subscribe(M=>{h.classList.toggle("md-code__button--active",M)});let x=me(d).pipe(oe(M=>Ft(M).pipe(f(O=>[M,O]))));v.pipe(g(M=>M?x:y)).subscribe(([M,O])=>{let N=Le(".hll.select",M);if(N&&!O)N.replaceWith(...Array.from(N.childNodes));else if(!N&&O){let ee=document.createElement("span");ee.className="hll select",ee.append(...Array.from(M.childNodes).slice(1)),M.append(ee)}});let w=me(d).pipe(oe(M=>b(M,"mousedown").pipe($(O=>O.preventDefault()),f(()=>M)))),E=v.pipe(g(M=>M?w:y),pe(Ts),f(([M,O])=>{var ee;let N=d.indexOf(M)+m;if(O===!1)return[N,N];{let le=P(".hll",e).map(ce=>d.indexOf(ce.parentElement)+m);return(ee=window.getSelection())==null||ee.removeAllRanges(),[Math.min(N,...le),Math.max(N,...le)]}})),_=_o(y).pipe(L(M=>M.startsWith(`__codelineno-${l}-`)));_.subscribe(M=>{let[,,O]=M.split("-"),N=O.split(":").map(le=>+le-m+1);N.length===1&&N.push(N[0]);for(let le of P(".hll:not(.select)",e))le.replaceWith(...Array.from(le.childNodes));let ee=d.slice(N[0]-1,N[1]);for(let le of ee){let ce=document.createElement("span");ce.className="hll",ce.append(...Array.from(le.childNodes).slice(1)),le.append(ce)}}),_.pipe(Me(1),Ie(ge)).subscribe(M=>{if(M.includes(":")){let O=document.getElementById(M.split(":")[0]);O&&setTimeout(()=>{let N=O,ee=-64;for(;N!==document.body;)ee+=N.offsetTop,N=N.offsetParent;window.scrollTo({top:ee})},1)}});let be=me(P('a[href^="#__codelineno"]',p)).pipe(oe(M=>b(M,"click").pipe($(O=>O.preventDefault()),f(()=>M)))).pipe(Q(i),pe(Ts),f(([M,O])=>{let ee=+G(`[id="${M.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(O===!1)return[ee,ee];{let le=P(".hll",e).map(ce=>+ce.parentElement.id.split("-").pop());return[Math.min(ee,...le),Math.max(ee,...le)]}}));R(E,be).subscribe(M=>{let O=`#__codelineno-${l}-`;M[0]===M[1]?O+=M[0]:O+=`${M[0]}:${M[1]}`,history.replaceState({},"",O),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+O,oldURL:window.location.href}))})}if(Ss.default.isSupported()&&(e.closest(".copy")||X("content.code.copy")&&!e.closest(".no-copy"))){let m=ms(s.id);a.push(m),X("content.tooltips")&&u.push(Ge(m,{viewport$}))}if(a.length){let m=hs();m.append(...a),s.insertBefore(m,e)}return ip(e).pipe($(m=>o.next(m)),V(()=>o.complete()),f(m=>H({ref:e},m)),Rt(R(...u).pipe(Q(i))))});return X("content.lazy")?Et(e).pipe(L(o=>o),Me(1),g(()=>n)):n}function ap(e,{target$:t,print$:r}){let n=!0;return R(t.pipe(f(o=>o.closest("details:not([open])")),L(o=>e===o),f(()=>({action:"open",reveal:!0}))),r.pipe(L(o=>o||!n),$(()=>n=e.open),f(o=>({action:o?"open":"close"}))))}function Ms(e,t){return j(()=>{let r=new I;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),ap(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}var ks=0,As=new Map;function sp(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],n=e.nextElementSibling;for(;n&&!(n instanceof HTMLHeadingElement);)r.push(n.cloneNode(!0)),n=n.nextElementSibling;return r}function cp(e,t){for(let r of P("[href], [src]",e))for(let n of["href","src"]){let o=r.getAttribute(n);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){r[n]=new URL(r.getAttribute(n),t).toString();break}}for(let r of P("[name^=__], [for]",e))for(let n of["id","for","name"]){let o=r.getAttribute(n);o&&r.setAttribute(n,`${o}$preview_${ks}`)}return ks++,Y(e)}function lp(e){let t=As.get(e.toString());return t?Y(t):En(e).pipe(g(r=>cp(r,e)),f(r=>(As.set(e.toString(),r),r)))}function Cs(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(X("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let n=re([ir(e),Ft(e).pipe(ke(1))]).pipe(f(([i,a])=>i||a),ie(),L(i=>i));return $t([r,n]).pipe(g(([i])=>{let a=new URL(e.href);return a.search=a.hash="",i.has(`${a}`)?Y(a):y}),g(i=>lp(i)),g(i=>{let a=e.hash?`article [id="${decodeURIComponent(e.hash.slice(1))}"]`:"article h1",s=Le(a,i);return typeof s=="undefined"?y:Y(sp(s))})).pipe(g(i=>{let a=new U(s=>{let c=On(...i);return s.next(c),document.body.append(c),()=>c.remove()});return Rr(e,H({content$:a},t))}))}var Hs=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var ko,pp=0;function fp(){return typeof mermaid=="undefined"||mermaid instanceof Element?ar("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):Y(void 0)}function $s(e){return e.classList.remove("mermaid"),ko||(ko=fp().pipe($(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Hs,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),f(()=>{}),se(1))),ko.subscribe(()=>Uo(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${pp++}`,r=A("div",{class:"mermaid"}),n=e.textContent,{svg:o,fn:i}=yield mermaid.render(t,n),a=r.attachShadow({mode:"closed"});a.innerHTML=o,e.replaceWith(r),i==null||i(a)})),ko.pipe(f(()=>({ref:e})))}var Ps=A("table");function Is(e){return e.replaceWith(Ps),Ps.replaceWith(gs(e)),Y({ref:e})}function mp(e){let t=e.find(r=>r.checked)||e[0];return R(...e.map(r=>b(r,"change").pipe(f(()=>G(`label[for="${r.id}"]`))))).pipe(J(G(`label[for="${t.id}"]`)),f(r=>({active:r})))}function Rs(e,{viewport$:t,target$:r}){let n=G(".tabbed-labels",e),o=P(":scope > input",e),i=Oo("prev");e.append(i);let a=Oo("next");return e.append(a),j(()=>{let s=new I,c=s.pipe(he(),ye(!0));re([s,Re(e),Et(e)]).pipe(Q(c),Xe(1,je)).subscribe({next([{active:l},u]){let p=wt(l),{width:d}=Ae(l);e.style.setProperty("--md-indicator-x",`${p.x}px`),e.style.setProperty("--md-indicator-width",`${d}px`);let m=ln(n);(p.xm.x+u.width)&&n.scrollTo({left:Math.max(0,p.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),re([Ut(n),Re(n)]).pipe(Q(c)).subscribe(([l,u])=>{let p=Mr(n);i.hidden=l.x<16,a.hidden=l.x>p.width-u.width-16}),R(b(i,"click").pipe(f(()=>-1)),b(a,"click").pipe(f(()=>1))).pipe(Q(c)).subscribe(l=>{let{width:u}=Ae(n);n.scrollBy({left:u*l,behavior:"smooth"})}),r.pipe(Q(c),L(l=>o.includes(l))).subscribe(l=>l.click()),n.classList.add("tabbed-labels--linked");for(let l of o){let u=G(`label[for="${l.id}"]`);u.replaceChildren(A("a",{href:`#${u.htmlFor}`,tabIndex:-1},...Array.from(u.childNodes))),b(u.firstElementChild,"click").pipe(Q(c),L(p=>!(p.metaKey||p.ctrlKey)),$(p=>{p.preventDefault(),p.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${u.htmlFor}`),u.click()})}return X("content.tabs.link")&&s.pipe(ke(1),pe(t)).subscribe(([{active:l},{offset:u}])=>{let p=l.innerText.trim();if(l.hasAttribute("data-md-switching"))l.removeAttribute("data-md-switching");else{let d=e.offsetTop-u.y;for(let h of P("[data-tabs]"))for(let v of P(":scope > input",h)){let x=G(`label[for="${v.id}"]`);if(x!==l&&x.innerText.trim()===p){x.setAttribute("data-md-switching",""),v.click();break}}window.scrollTo({top:e.offsetTop-d});let m=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([p,...m])])}}),s.pipe(Q(c)).subscribe(()=>{for(let l of P("audio, video",e))l.offsetWidth&&l.autoplay?l.play().catch(()=>{}):l.pause()}),mp(o).pipe($(l=>s.next(l)),V(()=>s.complete()),f(l=>H({ref:e},l)))}).pipe(Ht(ge))}function js(e,t){let{viewport$:r,target$:n,print$:o}=t;return R(...P(".annotate:not(.highlight)",e).map(i=>Es(i,{target$:n,print$:o})),...P("pre:not(.mermaid) > code",e).map(i=>Ls(i,{target$:n,print$:o})),...P("a",e).map(i=>Cs(i,t)),...P("pre.mermaid",e).map(i=>$s(i)),...P("table:not([class])",e).map(i=>Is(i)),...P("details",e).map(i=>Ms(i,{target$:n,print$:o})),...P("[data-tabs]",e).map(i=>Rs(i,{viewport$:r,target$:n})),...P("[title]:not([data-preview])",e).filter(()=>X("content.tooltips")).map(i=>Ge(i,{viewport$:r})),...P(".footnote-ref",e).filter(()=>X("content.footnote.tooltips")).map(i=>Rr(i,{content$:new U(a=>{let s=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(s).cloneNode(!0).children),l=On(...c);return a.next(l),document.body.append(l),()=>l.remove()}),viewport$:r})))}function dp(e,{alert$:t}){return t.pipe(g(r=>R(Y(!0),Y(!1).pipe(It(2e3))).pipe(f(n=>({message:r,active:n})))))}function Fs(e,t){let r=G(".md-typeset",e);return j(()=>{let n=new I;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),dp(e,t).pipe($(o=>n.next(o)),V(()=>n.complete()),f(o=>H({ref:e},o)))})}function hp({viewport$:e}){if(!X("header.autohide"))return Y(!1);let t=e.pipe(f(({offset:{y:o}})=>o),Pt(2,1),f(([o,i])=>[oMath.abs(i-o.y)>100),f(([,[o]])=>o),ie()),n=Tn("search");return re([e,n]).pipe(f(([{offset:o},i])=>o.y>400&&!i),ie(),g(o=>o?r:Y(!1)),J(!1))}function Us(e,t){return j(()=>re([Re(e),hp(t)])).pipe(f(([{height:r},n])=>({height:r,hidden:n})),ie((r,n)=>r.height===n.height&&r.hidden===n.hidden),se(1))}function Ns(e,{viewport$:t,header$:r,main$:n}){return j(()=>{let o=new I,i=o.pipe(he(),ye(!0));o.pipe(fe("active"),Ze(r)).subscribe(([{active:s},{hidden:c}])=>{e.classList.toggle("md-header--shadow",s&&!c),e.hidden=c});let a=me(P("[title]",e)).pipe(L(()=>X("content.tooltips")),oe(s=>Ge(s,{viewport$:t})));return n.subscribe(o),r.pipe(Q(i),f(s=>H({ref:e},s)),Rt(a.pipe(Q(i))))})}function vp(e,{viewport$:t,header$:r}){return Sn(e,{viewport$:t,header$:r}).pipe(f(({offset:{y:n}})=>{let{height:o}=Ae(e);return{active:o>0&&n>=o}}),fe("active"))}function Ds(e,t){return j(()=>{let r=new I;r.subscribe({next({active:o}){e.classList.toggle("md-header__title--active",o)},complete(){e.classList.remove("md-header__title--active")}});let n=Le(".md-content h1");return typeof n=="undefined"?y:vp(n,t).pipe($(o=>r.next(o)),V(()=>r.complete()),f(o=>H({ref:e},o)))})}function Ws(e,{viewport$:t,header$:r}){let n=r.pipe(f(({height:i})=>i),ie()),o=n.pipe(g(()=>Re(e).pipe(f(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),fe("bottom"))));return re([n,o,t]).pipe(f(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),ie((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function bp(e){let t=__md_get("__palette")||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return Y(...e).pipe(oe(n=>b(n,"change").pipe(f(()=>n))),J(e[r]),f(n=>({index:e.indexOf(n),color:{media:n.getAttribute("data-md-color-media"),scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),se(1))}function Vs(e){let t=P("input",e),r=A("meta",{name:"theme-color"});document.head.appendChild(r);let n=A("meta",{name:"color-scheme"});document.head.appendChild(n);let o=Ir("(prefers-color-scheme: light)");return j(()=>{let i=new I;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),pe(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(f(()=>{let a=ht("header"),s=window.getComputedStyle(a);return n.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Ie(ge)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),bp(t).pipe(Q(o.pipe(ke(1))),jt(),$(a=>i.next(a)),V(()=>i.complete()),f(a=>H({ref:e},a)))})}function zs(e,{progress$:t}){return j(()=>{let r=new I;return r.subscribe(({value:n})=>{e.style.setProperty("--md-progress-value",`${n}`)}),t.pipe($(n=>r.next({value:n})),V(()=>r.complete()),f(n=>({ref:e,value:n})))})}var qs='.v u{text-decoration:underline!important;text-decoration-style:wavy!important;text-decoration-thickness:1px!important}.p{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-backdrop)/var(--alpha-lighter));cursor:pointer;height:100%;pointer-events:auto;position:absolute;transition:opacity .25s;width:100%}.p.m{opacity:0;pointer-events:none;transition:opacity .35s}.r{align-items:center;background-color:initial;border:none;border-radius:var(--space-2);cursor:pointer;display:flex;flex-shrink:0;font-family:var(--font-family);height:36px;justify-content:center;outline:none;padding:0;position:relative;transition:background-color .25s,color .25s;width:36px;z-index:1}.r svg{stroke:rgb(var(--color-foreground));height:18px;opacity:.5;width:18px}.r:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.r:hover:before{opacity:1;transform:scale(1)}.r.c{cursor:auto}.r.c:before{display:none}.n{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background)/var(--alpha-light));border-radius:var(--space-3);box-shadow:0 0 60px #0000000d;display:flex;height:480px;overflow:hidden;pointer-events:auto;position:absolute;transition:transform .25s cubic-bezier(.16,1,.3,1),opacity .25s;width:640px}.n.l{opacity:0;pointer-events:none;transform:scale(1.1);transition:transform .25s .15s,opacity .15s}@media (max-width:680px){.n{border-radius:0;height:100%;width:100%}}.u{display:flex;flex-basis:min-content;flex-direction:column;flex-grow:1;flex-shrink:0}@keyframes d{0%{transform:scale(0)}50%{transform:scale(1.2)}to{transform:scale(1)}}.y{animation:d .25s ease-in-out;background:var(--color-highlight);border-radius:100%;color:#fff;font-size:8px;font-weight:700;height:12px;padding-top:1px;position:absolute;right:4px;top:4px;width:12px}.i{background-color:rgb(var(--color-background-subtle)/var(--alpha-lighter));flex-shrink:0;overflow:scroll;position:relative;transition:width .35s cubic-bezier(.16,1,.3,1),opacity .25s;width:200px}.i>*{transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.i.l{opacity:0;width:0}.i.l>*{transform:translate(-48px)}@media (max-width:680px){.i{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background-subtle)/var(--alpha-light));box-shadow:0 0 60px #00000026;height:100%;position:absolute;right:0;top:0}}.w{border-bottom:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));display:flex;gap:var(--space-1);padding:var(--space-2)}.k{-webkit-overflow-scrolling:touch;overflow:auto;overscroll-behavior:contain}.z{padding:8px 10px}.X{color:rgb(var(--color-foreground)/var(--alpha-light));padding:var(--space-2);position:absolute;width:200px}.X,.j{display:flex;flex-direction:column}.j{gap:2px;list-style:none;padding:0}.F,.j{margin:0}.F{font-size:16px;font-weight:400}.F,.I{padding:8px}.I{font-size:14px;margin:4px 0 0;opacity:.5}.I,.o{font-size:12px}.o{cursor:pointer;display:flex;padding:4px 8px;position:relative}.o:before{background-color:var(--color-highlight-transparent);border-radius:var(--space-1);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.o.g:before,.o:hover:before{opacity:1;transform:scale(1)}.o.g,.o:hover{color:var(--color-highlight)}.R{flex-grow:1}.R,.q{position:relative}.q{font-weight:700}.f{flex-grow:1}.f input{background:#0000;border:none;color:rgb(var(--color-foreground));font-family:var(--font-family);font-size:16px;height:100%;letter-spacing:-.25px;outline:none;width:100%}.b{color:rgb(var(--color-foreground)/var(--alpha-light));display:flex;flex-direction:column;gap:2px;line-height:1.3;list-style:none;margin:var(--space-2);margin-top:0;padding:0}.A,.b li{margin:0}.A{color:rgb(var(--color-foreground)/var(--alpha-lighter));font-size:12px;margin-top:var(--space-2);padding:0 18px}.a{border-radius:var(--space-2);color:inherit;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;padding:8px 10px;position:relative;text-decoration:none}.a:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";display:block;inset:0;opacity:0;position:absolute;transform:scale(.9);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.a.h:before,.a:hover:before{opacity:1;transform:scale(1)}}.a mark{background:#0000;color:var(--color-highlight)}.a u{background-color:var(--color-highlight-transparent);border-radius:2px;box-shadow:0 0 0 1px var(--color-highlight-transparent);text-decoration:none}.B{flex-grow:1}.s{margin-right:-8px;opacity:0;position:relative;transform:translate(-2px);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.h>.s,:hover>.s{opacity:1;transform:none}}.x{font-size:14px;margin:0;position:relative}.x code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);font-size:13px;padding:2px 4px}.t{color:rgb(var(--color-foreground)/var(--alpha-lighter));display:inline-flex;flex-wrap:wrap;font-size:12px;gap:var(--space-1);list-style:none;margin:0;padding:0;position:relative}.t li{white-space:nowrap}.t li:after{content:"/";display:inline;margin-left:var(--space-1)}.t li:last-child:after{content:"";display:none}.e{--space-1:4px;--space-2:calc(var(--space-1)*2);--space-3:calc(var(--space-2)*2);--space-4:calc(var(--space-3)*2);--space-5:calc(var(--space-4)*2);--alpha-light:.7;--alpha-lighter:.54;--alpha-lightest:.1;--color-highlight:var(--md-accent-fg-color,#526cfe);--color-highlight-transparent:var(--md-accent-fg-color--transparent,#526cfe1a);--border-radius-1:var(--space-1);--border-radius-2:var(--space-2);--border-radius-3:calc(var(--space-1) + var(--space-2));--font-family:var(--md-text-font-family,Inter,Roboto Flex,system-ui,sans-serif);--font-size:16px;--line-height:1.5;--letter-spacing:-.5px;-webkit-font-smoothing:antialiased;align-items:center;display:flex;font-family:var(--font-family);font-size:var(--font-size);height:100vh;justify-content:center;letter-spacing:var(--letter-spacing);line-height:var(--line-height);pointer-events:none;position:absolute;width:100vw}@media (pointer:coarse){.e{height:-webkit-fill-available}}.e *,.e :after,.e :before{box-sizing:border-box}';function Ks(e,{index$:t}){let r=Ue(),n=document.createElement("div");document.body.appendChild(n),n.style.position="fixed",n.style.height="100%",n.style.top="0",n.style.zIndex="4";let o=n.attachShadow({mode:"open"});o.appendChild(A("style",{},qs.toString()));try{Ya(r.search,{highlight:r.features.includes("search.highlight")}),me(t).subscribe(i=>{for(let a of i.items)a.location=new URL(a.location,r.base).toString();Ja(i,o)}),b(e,"click").subscribe(()=>{go()}),Tn("search").pipe(ke(1)).subscribe(()=>go())}catch(i){e.hidden=!0;let a=G("label[for=__search]");a.hidden=!0}return Ke}var Bs=_r(So());function Ys(e,{index$:t,location$:r}){return re([t,r.pipe(J(Ye()),L(n=>!!n.searchParams.get("h")))]).pipe(f(([n,o])=>_p(n.config)(o.searchParams.get("h"))),f(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,l=n(c);l.length>c.length&&o.set(s,l)}for(let[s,c]of o){let{childNodes:l}=A("span",null,c);s.replaceWith(...Array.from(l))}return{ref:e,nodes:o}}))}function _p(e){let t=e.separator.split("|").map(o=>o.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":o).join("|"),r=new RegExp(t,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/\s+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${o.split(r).map(a=>a.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&")).filter(a=>a.length>0).join("|")})`,"img");return a=>(0,Bs.default)(a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function yp(e,{viewport$:t,main$:r}){let n=e.closest(".md-grid"),o=n.offsetTop-n.parentElement.offsetTop;return re([r,t]).pipe(f(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),ie((i,a)=>i.height===a.height&&i.locked===a.locked))}function Ao(e,n){var o=n,{header$:t}=o,r=gr(o,["header$"]);let i=G(".md-sidebar__scrollwrap",e),{y:a}=wt(i);return j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=s.pipe(Xe(0,je));return l.pipe(pe(t)).subscribe({next([{height:u},{height:p}]){i.style.height=`${u-2*a}px`,e.style.top=`${p}px`},complete(){i.style.height="",e.style.top=""}}),l.pipe(Sr()).subscribe(()=>{for(let u of P(".md-nav__link--active[href]",e)){if(!u.clientHeight)continue;let p=u.closest(".md-sidebar__scrollwrap");if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2})}}}),me(P("label[tabindex]",e)).pipe(oe(u=>b(u,"click").pipe(Ie(ge),f(()=>u),Q(c)))).subscribe(u=>{let p=G(`[id="${u.htmlFor}"]`);G(`[aria-labelledby="${u.id}"]`).setAttribute("aria-expanded",`${p.checked}`)}),X("content.tooltips")&&me(P("abbr[title]",e)).pipe(oe(u=>Ge(u,{viewport$})),Q(c)).subscribe(),yp(e,r).pipe($(u=>s.next(u)),V(()=>s.complete()),f(u=>H({ref:e},u)))})}function Gs(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return $t(et(`${r}/releases/latest`).pipe(_e(()=>y),f(n=>({version:n.tag_name})),ot({})),et(r).pipe(_e(()=>y),f(n=>({stars:n.stargazers_count,forks:n.forks_count})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return et(r).pipe(f(n=>({repositories:n.public_repos})),ot({}))}}function Js(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return $t(et(`${r}/releases/permalink/latest`).pipe(_e(()=>y),f(({tag_name:n})=>({version:n})),ot({})),et(r).pipe(_e(()=>y),f(({star_count:n,forks_count:o})=>({stars:n,forks:o})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}function Xs(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,n]=t;return Gs(r,n)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,n]=t;return Js(r,n)}return y}var xp;function wp(e){return xp||(xp=j(()=>{let t=__md_get("__source",sessionStorage);if(t)return Y(t);if(Ee("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return y}return Xs(e.href).pipe($(n=>__md_set("__source",n,sessionStorage)))}).pipe(_e(()=>y),L(t=>Object.keys(t).length>0),f(t=>({facts:t})),se(1)))}function Zs(e){let t=G(":scope > :last-child",e);return j(()=>{let r=new I;return r.subscribe(({facts:n})=>{t.appendChild(bs(n)),t.classList.add("md-source__repository--active")}),wp(e).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Ep(e,{viewport$:t,header$:r}){return Re(document.body).pipe(g(()=>Sn(e,{header$:r,viewport$:t})),f(({offset:{y:n}})=>({hidden:n>=10})),fe("hidden"))}function Qs(e,t){return j(()=>{let r=new I;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(X("navigation.tabs.sticky")?Y({hidden:!1}):Ep(e,t)).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Tp(e,{viewport$:t,header$:r}){let n=new Map,o=P(".md-nav__link",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),l=Le(`[id="${c}"]`);typeof l!="undefined"&&n.set(s,l)}let i=r.pipe(fe("height"),f(({height:s})=>{let c=ht("main"),l=G(":scope > :first-child",c);return s+.9*(l.offsetTop-c.offsetTop)}),xe());return Re(document.body).pipe(fe("height"),g(s=>j(()=>{let c=[];return Y([...n].reduce((l,[u,p])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=p.tagName;)c.pop();let d=p.offsetTop;for(;!d&&p.parentElement;)p=p.parentElement,d=p.offsetTop;let m=p.offsetParent;for(;m;m=m.offsetParent)d+=m.offsetTop;return l.set([...c=[...c,u]].reverse(),d)},new Map))}).pipe(f(c=>new Map([...c].sort(([,l],[,u])=>l-u))),Ze(i),g(([c,l])=>t.pipe(Or(([u,p],{offset:{y:d},size:m})=>{let h=d+m.height>=Math.floor(s.height);for(;p.length;){let[,v]=p[0];if(v-l=d&&!h)p=[u.pop(),...p];else break}return[u,p]},[[],[...c]]),ie((u,p)=>u[0]===p[0]&&u[1]===p[1])))))).pipe(f(([s,c])=>({prev:s.map(([l])=>l),next:c.map(([l])=>l)})),J({prev:[],next:[]}),Pt(2,1),f(([s,c])=>s.prev.length{let i=new I,a=i.pipe(he(),ye(!0));if(i.subscribe(({prev:s,next:c})=>{for(let[l]of c)l.classList.remove("md-nav__link--passed"),l.classList.remove("md-nav__link--active");for(let[l,[u]]of s.entries())u.classList.add("md-nav__link--passed"),u.classList.toggle("md-nav__link--active",l===s.length-1)}),X("toc.follow")){let s=R(t.pipe(Be(1),f(()=>{})),t.pipe(Be(250),f(()=>"smooth")));i.pipe(L(({prev:c})=>c.length>0),Ze(n.pipe(Ie(ge))),pe(s)).subscribe(([[{prev:c}],l])=>{let[u]=c[c.length-1];if(u.offsetHeight){let p=ki(u);if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2,behavior:l})}}})}return X("navigation.tracking")&&t.pipe(Q(a),fe("offset"),Be(250),ke(1),Q(o.pipe(ke(1))),jt({delay:250}),pe(i)).subscribe(([,{prev:s}])=>{let c=Ye(),l=s[s.length-1];if(l&&l.length){let[u]=l,{hash:p}=new URL(u.href);c.hash!==p&&(c.hash=p,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),Tp(e,{viewport$:t,header$:r}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function Sp(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(f(({offset:{y:a}})=>a),Pt(2,1),f(([a,s])=>a>s&&s>0),ie()),i=r.pipe(f(({active:a})=>a));return re([i,o]).pipe(f(([a,s])=>!(a&&s)),ie(),Q(n.pipe(ke(1))),ye(!0),jt({delay:250}),f(a=>({hidden:a})))}function tc(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(Q(a),fe("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),b(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Sp(e,{viewport$:t,main$:n,target$:o}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))}function rc(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,t.port&&(e.port=t.port),e}function Op(e,t){let r=new Map;for(let n of P("url",e)){let o=G("loc",n),i=[rc(new URL(o.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",n)){let s=a.getAttribute("href");s!=null&&i.push(rc(new URL(s),t))}}return r}function dr(e){return ns(new URL("sitemap.xml",e)).pipe(f(t=>Op(t,new URL(e))),_e(()=>Y(new Map)),xe())}function nc({document$:e}){let t=new Map;e.pipe(g(()=>P("link[rel=alternate]")),f(r=>new URL(r.href)),L(r=>!t.has(r.toString())),oe(r=>dr(r).pipe(f(n=>[r,n]),_e(()=>y)))).subscribe(([r,n])=>{t.set(r.toString().replace(/\/$/,""),n)}),b(document.body,"click").pipe(L(r=>!r.metaKey&&!r.ctrlKey),g(r=>{if(r.target instanceof Element){let n=r.target.closest("a");if(n&&!n.target){let o=[...t].find(([p])=>n.href.startsWith(`${p}/`));if(typeof o=="undefined")return y;let[i,a]=o,s=Ye();if(s.href.startsWith(i))return y;let c=Ue(),l=s.href.replace(c.base,"");l=`${i}/${l}`;let u=a.has(l.split("#")[0])?new URL(l,c.base):new URL(i);return r.preventDefault(),Y(u)}}return y})).subscribe(r=>dt(r,!0))}var Co=_r(Mo());function Lp(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function oc({alert$:e}){Co.default.isSupported()&&new U(t=>{new Co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Lp(G(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe($(t=>{t.trigger.focus()}),f(()=>Bt("clipboard.copied"))).subscribe(e)}function ic(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let n=new URL(r.href);return n.search=n.hash="",t.has(`${n}`)?(e.preventDefault(),Y(r)):y}function ac(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function sc(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let n=t.getAttribute(r);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){t[r]=t[r];break}}return Y(e)}function Mp(e){for(let n of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...X("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let o=Le(n),i=Le(n,e);typeof o!="undefined"&&typeof i!="undefined"&&o.replaceWith(i)}let t=ac(document);for(let[n,o]of ac(e))t.has(n)?t.delete(n):document.head.appendChild(o);for(let n of t.values()){let o=n.getAttribute("name");o!=="theme-color"&&o!=="color-scheme"&&n.remove()}let r=ht("container");return nt(P("script",r)).pipe(g(n=>{let o=e.createElement("script");if(n.src){for(let i of n.getAttributeNames())o.setAttribute(i,n.getAttribute(i));return n.replaceWith(o),new U(i=>{o.onload=()=>i.complete()})}else return o.textContent=n.textContent,n.replaceWith(o),y}),he(),ye(document))}function cc({sitemap$:e,location$:t,viewport$:r,progress$:n}){if(location.protocol==="file:")return Ke;Y(document).subscribe(sc);let o=b(document.body,"click").pipe(Ze(e),g(([s,c])=>ic(s,c)),f(({href:s})=>new URL(s)),xe()),i=b(window,"popstate").pipe(f(Ye),xe());o.pipe(pe(r)).subscribe(([s,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",s)}),R(o,i).subscribe(t);let a=t.pipe(fe("pathname"),g(s=>En(s,{progress$:n}).pipe(_e(()=>(dt(s,!0),y)))),g(sc),g(Mp),xe());return R(a.pipe(pe(t,(s,c)=>c)),a.pipe(g(()=>t),fe("hash")),t.pipe(ie((s,c)=>s.pathname===c.pathname&&s.hash===c.hash),g(()=>o),$(()=>history.back()))).subscribe(s=>{var c,l;history.state!==null||!s.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",es(s.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),b(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(fe("offset"),Be(100)).subscribe(({offset:s})=>{history.replaceState(s,"")}),X("navigation.instant.prefetch")&&R(b(document.body,"mousemove"),b(document.body,"focusin")).pipe(Ze(e),g(([s,c])=>ic(s,c)),Be(25),Yn(({href:s})=>s),cn(s=>{let c=document.createElement("link");return c.rel="prefetch",c.href=s.toString(),document.head.appendChild(c),b(c,"load").pipe(f(()=>c),Me(1))})).subscribe(s=>s.remove()),a}function lc(e){var u;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:n,currentBaseURL:o}=e,i=(u=Ho(o))==null?void 0:u.pathname;if(i===void 0)return;let a=kp(n.pathname,i);if(a===void 0)return;let s=Cp(t.keys());if(!t.has(s))return;let c=Ho(a,s);if(!c||!t.has(c.href))return;let l=Ho(a,r);if(l)return l.hash=n.hash,l.search=n.search,l}function Ho(e,t){try{return new URL(e,t)}catch(r){return}}function kp(e,t){if(e.startsWith(t))return e.slice(t.length)}function Ap(e,t){let r=Math.min(e.length,t.length),n;for(n=0;ny)),n=r.pipe(f(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));r.pipe(f(o=>new Map(o.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),g(o=>b(document.body,"click").pipe(L(i=>!i.metaKey&&!i.ctrlKey),pe(n),g(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&o.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&o.get(c)===a?y:(i.preventDefault(),Y(new URL(c)))}}return y}),g(i=>dr(i).pipe(f(a=>{var s;return(s=lc({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:Ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(o=>dt(o,!0)),re([r,n]).subscribe(([o,i])=>{G(".md-header__topic").appendChild(_s(o,i))}),e.pipe(g(()=>n)).subscribe(o=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let c=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let l of c)for(let u of o.aliases.concat(o.version))if(new RegExp(l,"i").test(u)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let c of Ee("outdated"))c.hidden=!1})}function pc({document$:e,viewport$:t}){e.pipe(g(()=>P(".md-ellipsis")),oe(r=>Et(r).pipe(Q(e.pipe(ke(1))),L(n=>n),f(()=>r),Me(1))),L(r=>r.offsetWidth{let n=r.innerText,o=r.closest("a")||r;return o.title=n,X("content.tooltips")?Ge(o,{viewport$:t}).pipe(Q(e.pipe(ke(1))),V(()=>o.removeAttribute("title"))):y})).subscribe(),X("content.tooltips")&&e.pipe(g(()=>P(".md-status")),oe(r=>Ge(r,{viewport$:t}))).subscribe()}function fc({document$:e,tablet$:t}){e.pipe(g(()=>P(".md-toggle--indeterminate")),$(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>b(r,"change").pipe(Xn(()=>r.classList.contains("md-toggle--indeterminate")),f(()=>r))),pe(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function Hp(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function mc({document$:e}){e.pipe(g(()=>P("[data-md-scrollfix]")),$(t=>t.removeAttribute("data-md-scrollfix")),L(Hp),oe(t=>b(t,"touchstart").pipe(f(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));function $p(){return location.protocol==="file:"?ar(`${new URL("search.js",Mn.base)}`).pipe(f(()=>__index),_e(()=>Ke),se(1)):et(new URL("search.json",Mn.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var vt=Si(),Ur=Za(),hr=ts(Ur),hc=Xa(),ze=cs(),$o=Ir("(min-width: 60em)"),vc=Ir("(min-width: 76.25em)"),bc=rs(),Mn=Ue(),gc=Le(".md-search")?$p():Ke,Po=new I;oc({alert$:Po});nc({document$:vt});var Io=new I,_c=dr(Mn.base);X("navigation.instant")&&cc({sitemap$:_c,location$:Ur,viewport$:ze,progress$:Io}).subscribe(vt);var dc;((dc=Mn.version)==null?void 0:dc.provider)==="mike"&&uc({document$:vt});R(Ur,hr).pipe(It(125)).subscribe(()=>{Eo("drawer",!1),Eo("search",!1)});hc.pipe(L(({mode:e,meta:t})=>e==="global"&&!t)).subscribe(e=>{switch(e.type){case",":case"p":let t=document.querySelector("link[rel=prev]");t instanceof HTMLLinkElement&&dt(t);break;case".":case"n":let r=document.querySelector("link[rel=next]");r instanceof HTMLLinkElement&&dt(r);break;case"/":let n=document.querySelector("[data-md-component=search] button");n instanceof HTMLButtonElement&&n.click();break;case"Enter":let o=xt();o instanceof HTMLLabelElement&&o.click()}});pc({viewport$:ze,document$:vt});fc({document$:vt,tablet$:$o});mc({document$:vt});var Lt=Us(ht("header"),{viewport$:ze}),Fr=vt.pipe(f(()=>ht("main")),g(e=>Ws(e,{viewport$:ze,header$:Lt})),se(1)),Pp=R(...Ee("consent").map(e=>us(e,{target$:hr})),...Ee("dialog").map(e=>Fs(e,{alert$:Po})),...Ee("palette").map(e=>Vs(e)),...Ee("progress").map(e=>zs(e,{progress$:Io})),...Ee("search").map(e=>Ks(e,{index$:gc})),...Ee("source").map(e=>Zs(e))),Ip=j(()=>R(...Ee("announce").map(e=>ls(e)),...Ee("content").map(e=>js(e,{sitemap$:_c,viewport$:ze,target$:hr,print$:bc})),...Ee("content").map(e=>X("search.highlight")?Ys(e,{index$:gc,location$:Ur}):y),...Ee("header").map(e=>Ns(e,{viewport$:ze,header$:Lt,main$:Fr})),...Ee("header-title").map(e=>Ds(e,{viewport$:ze,header$:Lt})),...Ee("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?yo(vc,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr})):yo($o,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr}))),...Ee("tabs").map(e=>Qs(e,{viewport$:ze,header$:Lt})),...Ee("toc").map(e=>ec(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})),...Ee("top").map(e=>tc(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})))),yc=vt.pipe(g(()=>Ip),Rt(Pp),se(1));yc.subscribe();window.document$=vt;window.location$=Ur;window.target$=hr;window.keyboard$=hc;window.viewport$=ze;window.tablet$=$o;window.screen$=vc;window.print$=bc;window.alert$=Po;window.progress$=Io;window.component$=yc;})(); -/*! update cache: 20260331044513 */ +/*! update cache: 20260331053206 */ diff --git a/en/chapter_stack_and_queue/queue/index.html b/en/chapter_stack_and_queue/queue/index.html index 104248652..52648b100 100644 --- a/en/chapter_stack_and_queue/queue/index.html +++ b/en/chapter_stack_and_queue/queue/index.html @@ -6441,7 +6441,7 @@ typedef struct { int *nums; // Array for storing queue elements int front; // Front pointer, points to the front of the queue element - int queSize; // Rear pointer, points to rear + 1 + int queSize; // Current number of elements in the queue int queCapacity; // Queue capacity } ArrayQueue; diff --git a/en/javascripts/katex.js b/en/javascripts/katex.js index eff15a0d4..3a9f13b68 100644 --- a/en/javascripts/katex.js +++ b/en/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260331044513 */ +/*! update cache: 20260331053206 */ diff --git a/en/javascripts/mathjax.js b/en/javascripts/mathjax.js index 312339a55..1b73758e5 100644 --- a/en/javascripts/mathjax.js +++ b/en/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260331044513 */ +/*! update cache: 20260331053206 */ diff --git a/en/javascripts/starfield.js b/en/javascripts/starfield.js index 3df115a59..b2ae3c56e 100644 --- a/en/javascripts/starfield.js +++ b/en/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260331044513 */ +/*! update cache: 20260331053206 */ diff --git a/en/search.json b/en/search.json index 45aca1f8b..1ec805970 100644 --- a/en/search.json +++ b/en/search.json @@ -1 +1 @@ -{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"Chapter 16.   Appendix","text":"","path":["Chapter 16. Appendix","Chapter 16.   Appendix"],"tags":[]},{"location":"chapter_appendix/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 16.1   Programming Environment Installation
  • 16.2   Contributing Together
  • 16.3   Terminology Table
","path":["Chapter 16. Appendix","Chapter 16.   Appendix"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   Contributing Together","text":"

Due to limited capacity, there may be inevitable omissions and errors in this book. We appreciate your understanding and are grateful for your help in correcting them. If you discover typos, broken links, missing content, ambiguous wording, unclear explanations, or structural issues, please help us make corrections to provide readers with higher-quality learning resources.

The GitHub IDs of all contributors will be displayed on the homepage of the book repository, the web version, and the PDF version to acknowledge their selfless contributions to the open source community.

The Charm of Open Source

The interval between two printings of a physical book is often quite long, making content updates very inconvenient.

In this open source book, the time for content updates has been shortened to just days or even hours.

","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#1-minor-content-adjustments","level":3,"title":"1.   Minor Content Adjustments","text":"

As shown in Figure 16-3, there is an \"edit icon\" in the top-right corner of each page. You can modify text or code by following these steps.

  1. Click the \"edit icon\". If you encounter a prompt asking you to \"Fork this repository\", please approve the operation.
  2. Modify the content of the Markdown source file, verify the correctness of the content, and maintain consistent formatting as much as possible.
  3. Fill in a description of your changes at the bottom of the page, then click the \"Propose file change\" button. After the page transitions, click the \"Create pull request\" button to submit your pull request.

Figure 16-3   Page edit button

Images cannot be directly modified. Please describe the issue by creating a new Issue or leaving a comment. We will promptly redraw and replace the images.

","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#2-content-creation","level":3,"title":"2.   Content Creation","text":"

If you are interested in contributing to this open source project, including translating code into other programming languages or expanding article content, you will need to follow the Pull Request workflow below.

  1. Log in to GitHub and Fork the book's code repository to your personal account.
  2. Enter your forked repository webpage and use the git clone command to clone the repository to your local machine.
  3. Create content locally and conduct comprehensive tests to verify code correctness.
  4. Commit your local changes and push them to the remote repository.
  5. Refresh the repository webpage and click the \"Create pull request\" button to submit your pull request.
","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#3-docker-deployment","level":3,"title":"3.   Docker Deployment","text":"

From the root directory of hello-algo, run the following Docker script to access the project at http://localhost:8000:

docker-compose up -d\n

Use the following command to remove the deployment:

docker-compose down\n
","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/installation/","level":1,"title":"16.1   Programming Environment Installation","text":"","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1611-installing-ide","level":2,"title":"16.1.1   Installing Ide","text":"

We recommend using the open-source and lightweight VS Code as the local integrated development environment (IDE). Visit the VS Code official website, and download and install the appropriate version of VS Code according to your operating system.

Figure 16-1   Download VS Code from the Official Website

VS Code has a powerful ecosystem of extensions that supports running and debugging most programming languages. For example, after installing the \"Python Extension Pack\" extension, you can debug Python code. The installation steps are shown in the following figure.

Figure 16-2   Install VS Code Extensions

","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1612-installing-language-environments","level":2,"title":"16.1.2   Installing Language Environments","text":"","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1-python-environment","level":3,"title":"1.   Python Environment","text":"
  1. Download and install Miniconda3, which requires Python 3.10 or newer.
  2. Search for python in the VS Code extension marketplace and install the Python Extension Pack.
  3. (Optional) Enter pip install black on the command line to install the code formatter.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#2-cc-environment","level":3,"title":"2.   C/c++ Environment","text":"
  1. Windows systems need to install MinGW (configuration tutorial); macOS comes with Clang built-in and does not require installation.
  2. Search for c++ in the VS Code extension marketplace and install the C/C++ Extension Pack.
  3. (Optional) Open the Settings page, search for the Clang_format_fallback Style code formatting option, and set it to { BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#3-java-environment","level":3,"title":"3.   Java Environment","text":"
  1. Download and install OpenJDK (version must be > JDK 9).
  2. Search for java in the VS Code extension marketplace and install the Extension Pack for Java.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#4-c-environment","level":3,"title":"4.   C# Environment","text":"
  1. Download and install .Net 8.0.
  2. Search for C# Dev Kit in the VS Code extension marketplace and install C# Dev Kit (configuration tutorial).
  3. You can also use Visual Studio (installation tutorial).
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#5-go-environment","level":3,"title":"5.   Go Environment","text":"
  1. Download and install Go.
  2. Search for go in the VS Code extension marketplace and install Go.
  3. Press Ctrl + Shift + P to open the command palette, type go, select Go: Install/Update Tools, check all options and install.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#6-swift-environment","level":3,"title":"6.   Swift Environment","text":"
  1. Download and install Swift.
  2. Search for swift in the VS Code extension marketplace and install Swift for Visual Studio Code.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#7-javascript-environment","level":3,"title":"7.   Javascript Environment","text":"
  1. Download and install Node.js.
  2. (Optional) Search for Prettier in the VS Code extension marketplace and install the code formatter.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#8-typescript-environment","level":3,"title":"8.   Typescript Environment","text":"
  1. Follow the same installation steps as the JavaScript environment.
  2. Install TypeScript Execute (tsx).
  3. Search for typescript in the VS Code extension marketplace and install Pretty TypeScript Errors.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#9-dart-environment","level":3,"title":"9.   Dart Environment","text":"
  1. Download and install Dart.
  2. Search for dart in the VS Code extension marketplace and install Dart.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#10-rust-environment","level":3,"title":"10.   Rust Environment","text":"
  1. Download and install Rust.
  2. Search for rust in the VS Code extension marketplace and install rust-analyzer.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/terminology/","level":1,"title":"16.3   Terminology Table","text":"

The following table lists important terms that appear in this book. It is worth noting the following points:

  • We recommend remembering the English names of terms to help with reading English literature.
  • Some terms have different names in Simplified Chinese and Traditional Chinese.

Table 16-1   Important Terms in Data Structures and Algorithms

English Simplified Chinese Traditional Chinese algorithm 算法 演算法 data structure 数据结构 資料結構 code 代码 程式碼 file 文件 檔案 function 函数 函式 method 方法 方法 variable 变量 變數 asymptotic complexity analysis 渐近复杂度分析 漸近複雜度分析 time complexity 时间复杂度 時間複雜度 space complexity 空间复杂度 空間複雜度 loop 循环 迴圈 iteration 迭代 迭代 recursion 递归 遞迴 tail recursion 尾递归 尾遞迴 recursion tree 递归树 遞迴樹 big-\\(O\\) notation 大 \\(O\\) 记号 大 \\(O\\) 記號 asymptotic upper bound 渐近上界 漸近上界 sign-magnitude 原码 原碼 1’s complement 反码 一補數 2’s complement 补码 二補數 array 数组 陣列 index 索引 索引 linked list 链表 鏈結串列 linked list node, list node 链表节点 鏈結串列節點 head node 头节点 頭節點 tail node 尾节点 尾節點 list 列表 串列 dynamic array 动态数组 動態陣列 hard disk 硬盘 硬碟 random-access memory (RAM) 内存 記憶體 cache memory 缓存 快取 cache miss 缓存未命中 快取未命中 cache hit rate 缓存命中率 快取命中率 stack 栈 堆疊 top of the stack 栈顶 堆疊頂 bottom of the stack 栈底 堆疊底 queue 队列 佇列 double-ended queue 双向队列 雙向佇列 front of the queue 队首 佇列首 rear of the queue 队尾 佇列尾 hash table 哈希表 雜湊表 hash set 哈希集合 雜湊集合 bucket 桶 桶 hash function 哈希函数 雜湊函式 hash collision 哈希冲突 雜湊衝突 load factor 负载因子 負載因子 separate chaining 链式地址 鏈結位址 open addressing 开放寻址 開放定址 linear probing 线性探测 線性探查 lazy deletion 懒删除 懶刪除 binary tree 二叉树 二元樹 tree node 树节点 樹節點 left-child node 左子节点 左子節點 right-child node 右子节点 右子節點 parent node 父节点 父節點 left subtree 左子树 左子樹 right subtree 右子树 右子樹 root node 根节点 根節點 leaf node 叶节点 葉節點 edge 边 邊 level 层 層 degree 度 度 height 高度 高度 depth 深度 深度 perfect binary tree 完美二叉树 完美二元樹 complete binary tree 完全二叉树 完全二元樹 full binary tree 完满二叉树 完滿二元樹 balanced binary tree 平衡二叉树 平衡二元樹 binary search tree 二叉搜索树 二元搜尋樹 AVL tree AVL 树 AVL 樹 red-black tree 红黑树 紅黑樹 level-order traversal 层序遍历 層序走訪 breadth-first traversal 广度优先遍历 廣度優先走訪 depth-first traversal 深度优先遍历 深度優先走訪 binary search tree 二叉搜索树 二元搜尋樹 balanced binary search tree 平衡二叉搜索树 平衡二元搜尋樹 balance factor 平衡因子 平衡因子 heap 堆 堆積 max heap 大顶堆 大頂堆積 min heap 小顶堆 小頂堆積 priority queue 优先队列 優先佇列 heapify 堆化 堆積化 top-\\(k\\) problem Top-\\(k\\) 问题 Top-\\(k\\) 問題 graph 图 圖 vertex 顶点 頂點 undirected graph 无向图 無向圖 directed graph 有向图 有向圖 connected graph 连通图 連通圖 disconnected graph 非连通图 非連通圖 weighted graph 有权图 有權圖 adjacency 邻接 鄰接 path 路径 路徑 in-degree 入度 入度 out-degree 出度 出度 adjacency matrix 邻接矩阵 鄰接矩陣 adjacency list 邻接表 鄰接表 breadth-first search 广度优先搜索 廣度優先搜尋 depth-first search 深度优先搜索 深度優先搜尋 binary search 二分查找 二分搜尋 searching algorithm 搜索算法 搜尋演算法 sorting algorithm 排序算法 排序演算法 selection sort 选择排序 選擇排序 bubble sort 冒泡排序 泡沫排序 insertion sort 插入排序 插入排序 quick sort 快速排序 快速排序 merge sort 归并排序 合併排序 heap sort 堆排序 堆積排序 bucket sort 桶排序 桶排序 counting sort 计数排序 計數排序 radix sort 基数排序 基數排序 divide and conquer 分治 分治 hanota problem 汉诺塔问题 河內塔問題 backtracking algorithm 回溯算法 回溯演算法 constraint 约束 約束 solution 解 解 state 状态 狀態 pruning 剪枝 剪枝 permutations problem 全排列问题 全排列問題 subset-sum problem 子集和问题 子集合問題 \\(n\\)-queens problem \\(n\\) 皇后问题 \\(n\\) 皇后問題 dynamic programming 动态规划 動態規劃 initial state 初始状态 初始狀態 state-transition equation 状态转移方程 狀態轉移方程 knapsack problem 背包问题 背包問題 edit distance problem 编辑距离问题 編輯距離問題 greedy algorithm 贪心算法 貪婪演算法","path":["Chapter 16. Appendix","16.3   Terminology Table"],"tags":[]},{"location":"chapter_array_and_linkedlist/","level":1,"title":"Chapter 4.   Array and Linked List","text":"

Abstract

The world of data structures is like a solid brick wall.

Array bricks are neatly arranged, tightly packed one by one. Linked list bricks are scattered everywhere, with connecting vines freely weaving through the gaps between bricks.

","path":["Chapter 4. Array and Linked List","Chapter 4.   Array and Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 4.1   Array
  • 4.2   Linked List
  • 4.3   List
  • 4.4   Memory and Cache *
  • 4.5   Summary
","path":["Chapter 4. Array and Linked List","Chapter 4.   Array and Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   Array","text":"

An array is a linear data structure that stores elements of the same type in contiguous memory space. The position of an element in the array is called the element's index. Figure 4-1 illustrates the main concepts and storage method of arrays.

Figure 4-1   Array definition and storage method

","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#411-common-array-operations","level":2,"title":"4.1.1   Common Array Operations","text":"","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#1-initializing-arrays","level":3,"title":"1.   Initializing Arrays","text":"

We can choose between two array initialization methods based on our needs: without initial values or with given initial values. When no initial values are specified, most programming languages will initialize array elements to \\(0\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
# Initialize array\narr: list[int] = [0] * 5  # [ 0, 0, 0, 0, 0 ]\nnums: list[int] = [1, 3, 2, 5, 4]\n
array.cpp
/* Initialize array */\n// Stored on stack\nint arr[5];\nint nums[5] = { 1, 3, 2, 5, 4 };\n// Stored on heap (requires manual memory release)\nint* arr1 = new int[5];\nint* nums1 = new int[5] { 1, 3, 2, 5, 4 };\n
array.java
/* Initialize array */\nint[] arr = new int[5]; // { 0, 0, 0, 0, 0 }\nint[] nums = { 1, 3, 2, 5, 4 };\n
array.cs
/* Initialize array */\nint[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]\nint[] nums = [1, 3, 2, 5, 4];\n
array.go
/* Initialize array */\nvar arr [5]int\n// In Go, specifying length ([5]int) creates an array; not specifying length ([]int) creates a slice\n// Since Go's arrays are designed to have their length determined at compile time, only constants can be used to specify the length\n// For convenience in implementing the extend() method, slices are treated as arrays below\nnums := []int{1, 3, 2, 5, 4}\n
array.swift
/* Initialize array */\nlet arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]\nlet nums = [1, 3, 2, 5, 4]\n
array.js
/* Initialize array */\nvar arr = new Array(5).fill(0);\nvar nums = [1, 3, 2, 5, 4];\n
array.ts
/* Initialize array */\nlet arr: number[] = new Array(5).fill(0);\nlet nums: number[] = [1, 3, 2, 5, 4];\n
array.dart
/* Initialize array */\nList<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]\nList<int> nums = [1, 3, 2, 5, 4];\n
array.rs
/* Initialize array */\nlet arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]\nlet slice: &[i32] = &[0; 5];\n// In Rust, specifying length ([i32; 5]) creates an array; not specifying length (&[i32]) creates a slice\n// Since Rust's arrays are designed to have their length determined at compile time, only constants can be used to specify the length\n// Vector is the type generally used as a dynamic array in Rust\n// For convenience in implementing the extend() method, vectors are treated as arrays below\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
array.c
/* Initialize array */\nint arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }\nint nums[5] = { 1, 3, 2, 5, 4 };\n
array.kt
/* Initialize array */\nvar arr = IntArray(5) // { 0, 0, 0, 0, 0 }\nvar nums = intArrayOf(1, 3, 2, 5, 4)\n
array.rb
# Initialize array\narr = Array.new(5, 0)\nnums = [1, 3, 2, 5, 4]\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#2-accessing-elements","level":3,"title":"2.   Accessing Elements","text":"

Array elements are stored in contiguous memory space, which means calculating the memory address of array elements is very easy. Given the array's memory address (the memory address of the first element) and an element's index, we can use the formula shown in Figure 4-2 to calculate the element's memory address and directly access that element.

Figure 4-2   Memory address calculation for array elements

Observing Figure 4-2, we find that the first element of an array has an index of \\(0\\), which may seem counterintuitive since counting from \\(1\\) would be more natural. However, from the perspective of the address calculation formula, an index is essentially an offset from the memory address. The address offset of the first element is \\(0\\), so it is reasonable for its index to be \\(0\\).

Accessing elements in an array is highly efficient; we can randomly access any element in the array in \\(O(1)\\) time.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def random_access(nums: list[int]) -> int:\n    \"\"\"Random access to element\"\"\"\n    # Randomly select a number from the interval [0, len(nums)-1]\n    random_index = random.randint(0, len(nums) - 1)\n    # Retrieve and return the random element\n    random_num = nums[random_index]\n    return random_num\n
array.cpp
/* Random access to element */\nint randomAccess(int *nums, int size) {\n    // Randomly select a number from interval [0, size)\n    int randomIndex = rand() % size;\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.java
/* Random access to element */\nint randomAccess(int[] nums) {\n    // Randomly select a number in the interval [0, nums.length)\n    int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.cs
/* Random access to element */\nint RandomAccess(int[] nums) {\n    Random random = new();\n    // Randomly select a number in interval [0, nums.Length)\n    int randomIndex = random.Next(nums.Length);\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.go
/* Random access to element */\nfunc randomAccess(nums []int) (randomNum int) {\n    // Randomly select a number in the interval [0, nums.length)\n    randomIndex := rand.Intn(len(nums))\n    // Retrieve and return the random element\n    randomNum = nums[randomIndex]\n    return\n}\n
array.swift
/* Random access to element */\nfunc randomAccess(nums: [Int]) -> Int {\n    // Randomly select a number in interval [0, nums.count)\n    let randomIndex = nums.indices.randomElement()!\n    // Retrieve and return the random element\n    let randomNum = nums[randomIndex]\n    return randomNum\n}\n
array.js
/* Random access to element */\nfunction randomAccess(nums) {\n    // Randomly select a number in the interval [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Retrieve and return the random element\n    const random_num = nums[random_index];\n    return random_num;\n}\n
array.ts
/* Random access to element */\nfunction randomAccess(nums: number[]): number {\n    // Randomly select a number in the interval [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Retrieve and return the random element\n    const random_num = nums[random_index];\n    return random_num;\n}\n
array.dart
/* Random access to element */\nint randomAccess(List<int> nums) {\n  // Randomly select a number in the interval [0, nums.length)\n  int randomIndex = Random().nextInt(nums.length);\n  // Retrieve and return the random element\n  int randomNum = nums[randomIndex];\n  return randomNum;\n}\n
array.rs
/* Random access to element */\nfn random_access(nums: &[i32]) -> i32 {\n    // Randomly select a number in interval [0, nums.len())\n    let random_index = rand::thread_rng().gen_range(0..nums.len());\n    // Retrieve and return the random element\n    let random_num = nums[random_index];\n    random_num\n}\n
array.c
/* Random access to element */\nint randomAccess(int *nums, int size) {\n    // Randomly select a number from interval [0, size)\n    int randomIndex = rand() % size;\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.kt
/* Random access to element */\nfun randomAccess(nums: IntArray): Int {\n    // Randomly select a number in interval [0, nums.size)\n    val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size)\n    // Retrieve and return the random element\n    val randomNum = nums[randomIndex]\n    return randomNum\n}\n
array.rb
### Random access element ###\ndef random_access(nums)\n  # Randomly select a number in the interval [0, nums.length)\n  random_index = Random.rand(0...nums.length)\n\n  # Retrieve and return the random element\n  nums[random_index]\nend\n
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#3-inserting-elements","level":3,"title":"3.   Inserting Elements","text":"

Array elements are stored \"tightly adjacent\" in memory, with no space between them to store any additional data. As shown in Figure 4-3, if we want to insert an element in the middle of an array, we need to shift all elements after that position backward by one position, and then assign the value to that index.

Figure 4-3   Example of inserting an element into an array

It is worth noting that since the length of an array is fixed, inserting an element will inevitably cause the element at the end of the array to be \"lost\". We will leave the solution to this problem for discussion in the \"List\" chapter.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def insert(nums: list[int], num: int, index: int):\n    \"\"\"Insert element num at index index in the array\"\"\"\n    # Move all elements at and after index index backward by one position\n    for i in range(len(nums) - 1, index, -1):\n        nums[i] = nums[i - 1]\n    # Assign num to the element at index index\n    nums[index] = num\n
array.cpp
/* Insert element num at index index in the array */\nvoid insert(int *nums, int size, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.java
/* Insert element num at index index in the array */\nvoid insert(int[] nums, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.cs
/* Insert element num at index index in the array */\nvoid Insert(int[] nums, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = nums.Length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.go
/* Insert element num at index index in the array */\nfunc insert(nums []int, num int, index int) {\n    // Move all elements at and after index index backward by one position\n    for i := len(nums) - 1; i > index; i-- {\n        nums[i] = nums[i-1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
array.swift
/* Insert element num at index index in the array */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n    // Move all elements at and after index index backward by one position\n    for i in nums.indices.dropFirst(index).reversed() {\n        nums[i] = nums[i - 1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
array.js
/* Insert element num at index index in the array */\nfunction insert(nums, num, index) {\n    // Move all elements at and after index index backward by one position\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.ts
/* Insert element num at index index in the array */\nfunction insert(nums: number[], num: number, index: number): void {\n    // Move all elements at and after index index backward by one position\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.dart
/* Insert element _num at array index index */\nvoid insert(List<int> nums, int _num, int index) {\n  // Move all elements at and after index index backward by one position\n  for (var i = nums.length - 1; i > index; i--) {\n    nums[i] = nums[i - 1];\n  }\n  // Assign _num to element at index\n  nums[index] = _num;\n}\n
array.rs
/* Insert element num at index index in the array */\nfn insert(nums: &mut [i32], num: i32, index: usize) {\n    // Move all elements at and after index index backward by one position\n    for i in (index + 1..nums.len()).rev() {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.c
/* Insert element num at index index in the array */\nvoid insert(int *nums, int size, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.kt
/* Insert element num at index index in the array */\nfun insert(nums: IntArray, num: Int, index: Int) {\n    // Move all elements at and after index index backward by one position\n    for (i in nums.size - 1 downTo index + 1) {\n        nums[i] = nums[i - 1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
array.rb
### Insert element num at index in array ###\ndef insert(nums, num, index)\n  # Move all elements at and after index index backward by one position\n  for i in (nums.length - 1).downto(index + 1)\n    nums[i] = nums[i - 1]\n  end\n\n  # Assign num to the element at index index\n  nums[index] = num\nend\n
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4-removing-elements","level":3,"title":"4.   Removing Elements","text":"

Similarly, as shown in Figure 4-4, to delete the element at index \\(i\\), we need to shift all elements after index \\(i\\) forward by one position.

Figure 4-4   Example of removing an element from an array

Note that after the deletion is complete, the original last element becomes \"meaningless\", so we do not need to specifically modify it.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def remove(nums: list[int], index: int):\n    \"\"\"Remove the element at index index\"\"\"\n    # Move all elements after index index forward by one position\n    for i in range(index, len(nums) - 1):\n        nums[i] = nums[i + 1]\n
array.cpp
/* Remove the element at index index */\nvoid remove(int *nums, int size, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.java
/* Remove the element at index index */\nvoid remove(int[] nums, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.cs
/* Remove the element at index index */\nvoid Remove(int[] nums, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < nums.Length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.go
/* Remove the element at index index */\nfunc remove(nums []int, index int) {\n    // Move all elements after index index forward by one position\n    for i := index; i < len(nums)-1; i++ {\n        nums[i] = nums[i+1]\n    }\n}\n
array.swift
/* Remove the element at index index */\nfunc remove(nums: inout [Int], index: Int) {\n    // Move all elements after index index forward by one position\n    for i in nums.indices.dropFirst(index).dropLast() {\n        nums[i] = nums[i + 1]\n    }\n}\n
array.js
/* Remove the element at index index */\nfunction remove(nums, index) {\n    // Move all elements after index index forward by one position\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.ts
/* Remove the element at index index */\nfunction remove(nums: number[], index: number): void {\n    // Move all elements after index index forward by one position\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.dart
/* Remove the element at index index */\nvoid remove(List<int> nums, int index) {\n  // Move all elements after index index forward by one position\n  for (var i = index; i < nums.length - 1; i++) {\n    nums[i] = nums[i + 1];\n  }\n}\n
array.rs
/* Remove the element at index index */\nfn remove(nums: &mut [i32], index: usize) {\n    // Move all elements after index index forward by one position\n    for i in index..nums.len() - 1 {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.c
/* Remove the element at index index */\n// Note: stdio.h occupies the remove keyword\nvoid removeItem(int *nums, int size, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.kt
/* Remove the element at index index */\nfun remove(nums: IntArray, index: Int) {\n    // Move all elements after index index forward by one position\n    for (i in index..<nums.size - 1) {\n        nums[i] = nums[i + 1]\n    }\n}\n
array.rb
### Delete element at index ###\ndef remove(nums, index)\n  # Move all elements after index index forward by one position\n  for i in index...(nums.length - 1)\n    nums[i] = nums[i + 1]\n  end\nend\n

Overall, array insertion and deletion operations have the following drawbacks:

  • High time complexity: The average time complexity for both insertion and deletion in arrays is \\(O(n)\\), where \\(n\\) is the length of the array.
  • Loss of elements: Since the length of an array is immutable, after inserting an element, elements that exceed the array's length will be lost.
  • Memory waste: We can initialize a relatively long array and only use the front portion, so that when inserting data, the lost elements at the end are \"meaningless\", but this causes some memory space to be wasted.
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#5-traversing-arrays","level":3,"title":"5.   Traversing Arrays","text":"

In most programming languages, we can traverse an array either by index or by directly iterating through each element in the array:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def traverse(nums: list[int]):\n    \"\"\"Traverse array\"\"\"\n    count = 0\n    # Traverse array by index\n    for i in range(len(nums)):\n        count += nums[i]\n    # Direct traversal of array elements\n    for num in nums:\n        count += num\n    # Traverse simultaneously data index and elements\n    for i, num in enumerate(nums):\n        count += nums[i]\n        count += num\n
array.cpp
/* Traverse array */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
array.java
/* Traverse array */\nvoid traverse(int[] nums) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (int num : nums) {\n        count += num;\n    }\n}\n
array.cs
/* Traverse array */\nvoid Traverse(int[] nums) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < nums.Length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    foreach (int num in nums) {\n        count += num;\n    }\n}\n
array.go
/* Traverse array */\nfunc traverse(nums []int) {\n    count := 0\n    // Traverse array by index\n    for i := 0; i < len(nums); i++ {\n        count += nums[i]\n    }\n    count = 0\n    // Direct traversal of array elements\n    for _, num := range nums {\n        count += num\n    }\n    // Traverse simultaneously data index and elements\n    for i, num := range nums {\n        count += nums[i]\n        count += num\n    }\n}\n
array.swift
/* Traverse array */\nfunc traverse(nums: [Int]) {\n    var count = 0\n    // Traverse array by index\n    for i in nums.indices {\n        count += nums[i]\n    }\n    // Direct traversal of array elements\n    for num in nums {\n        count += num\n    }\n    // Traverse simultaneously data index and elements\n    for (i, num) in nums.enumerated() {\n        count += nums[i]\n        count += num\n    }\n}\n
array.js
/* Traverse array */\nfunction traverse(nums) {\n    let count = 0;\n    // Traverse array by index\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (const num of nums) {\n        count += num;\n    }\n}\n
array.ts
/* Traverse array */\nfunction traverse(nums: number[]): void {\n    let count = 0;\n    // Traverse array by index\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (const num of nums) {\n        count += num;\n    }\n}\n
array.dart
/* Traverse array elements */\nvoid traverse(List<int> nums) {\n  int count = 0;\n  // Traverse array by index\n  for (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n  }\n  // Direct traversal of array elements\n  for (int _num in nums) {\n    count += _num;\n  }\n  // Traverse array using forEach method\n  nums.forEach((_num) {\n    count += _num;\n  });\n}\n
array.rs
/* Traverse array */\nfn traverse(nums: &[i32]) {\n    let mut _count = 0;\n    // Traverse array by index\n    for i in 0..nums.len() {\n        _count += nums[i];\n    }\n    // Direct traversal of array elements\n    _count = 0;\n    for &num in nums {\n        _count += num;\n    }\n}\n
array.c
/* Traverse array */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
array.kt
/* Traverse array */\nfun traverse(nums: IntArray) {\n    var count = 0\n    // Traverse array by index\n    for (i in nums.indices) {\n        count += nums[i]\n    }\n    // Direct traversal of array elements\n    for (j in nums) {\n        count += j\n    }\n}\n
array.rb
### Traverse array ###\ndef traverse(nums)\n  count = 0\n\n  # Traverse array by index\n  for i in 0...nums.length\n    count += nums[i]\n  end\n\n  # Direct traversal of array elements\n  for num in nums\n    count += num\n  end\nend\n
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#6-finding-elements","level":3,"title":"6.   Finding Elements","text":"

Finding a specified element in an array requires traversing the array and checking whether the element value matches in each iteration; if it matches, output the corresponding index.

Since an array is a linear data structure, the above search operation is called a \"linear search\".

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def find(nums: list[int], target: int) -> int:\n    \"\"\"Find the specified element in the array\"\"\"\n    for i in range(len(nums)):\n        if nums[i] == target:\n            return i\n    return -1\n
array.cpp
/* Find the specified element in the array */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.java
/* Find the specified element in the array */\nint find(int[] nums, int target) {\n    for (int i = 0; i < nums.length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.cs
/* Find the specified element in the array */\nint Find(int[] nums, int target) {\n    for (int i = 0; i < nums.Length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.go
/* Find the specified element in the array */\nfunc find(nums []int, target int) (index int) {\n    index = -1\n    for i := 0; i < len(nums); i++ {\n        if nums[i] == target {\n            index = i\n            break\n        }\n    }\n    return\n}\n
array.swift
/* Find the specified element in the array */\nfunc find(nums: [Int], target: Int) -> Int {\n    for i in nums.indices {\n        if nums[i] == target {\n            return i\n        }\n    }\n    return -1\n}\n
array.js
/* Find the specified element in the array */\nfunction find(nums, target) {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) return i;\n    }\n    return -1;\n}\n
array.ts
/* Find the specified element in the array */\nfunction find(nums: number[], target: number): number {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) {\n            return i;\n        }\n    }\n    return -1;\n}\n
array.dart
/* Find the specified element in the array */\nint find(List<int> nums, int target) {\n  for (var i = 0; i < nums.length; i++) {\n    if (nums[i] == target) return i;\n  }\n  return -1;\n}\n
array.rs
/* Find the specified element in the array */\nfn find(nums: &[i32], target: i32) -> Option<usize> {\n    for i in 0..nums.len() {\n        if nums[i] == target {\n            return Some(i);\n        }\n    }\n    None\n}\n
array.c
/* Find the specified element in the array */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.kt
/* Find the specified element in the array */\nfun find(nums: IntArray, target: Int): Int {\n    for (i in nums.indices) {\n        if (nums[i] == target)\n            return i\n    }\n    return -1\n}\n
array.rb
### Find specified element in array ###\ndef find(nums, target)\n  for i in 0...nums.length\n    return i if nums[i] == target\n  end\n\n  -1\nend\n
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#7-expanding-arrays","level":3,"title":"7.   Expanding Arrays","text":"

In complex system environments, programs cannot guarantee that the memory space after an array is available, making it unsafe to expand the array's capacity. Therefore, in most programming languages, the length of an array is immutable.

If we want to expand an array, we need to create a new, larger array and then copy the original array elements to the new array one by one. This is an \\(O(n)\\) operation, which is very time-consuming when the array is large. The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def extend(nums: list[int], enlarge: int) -> list[int]:\n    \"\"\"Extend array length\"\"\"\n    # Initialize an array with extended length\n    res = [0] * (len(nums) + enlarge)\n    # Copy all elements from the original array to the new array\n    for i in range(len(nums)):\n        res[i] = nums[i]\n    # Return the extended new array\n    return res\n
array.cpp
/* Extend array length */\nint *extend(int *nums, int size, int enlarge) {\n    // Initialize an array with extended length\n    int *res = new int[size + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Free memory\n    delete[] nums;\n    // Return the extended new array\n    return res;\n}\n
array.java
/* Extend array length */\nint[] extend(int[] nums, int enlarge) {\n    // Initialize an array with extended length\n    int[] res = new int[nums.length + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
array.cs
/* Extend array length */\nint[] Extend(int[] nums, int enlarge) {\n    // Initialize an array with extended length\n    int[] res = new int[nums.Length + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < nums.Length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
array.go
/* Extend array length */\nfunc extend(nums []int, enlarge int) []int {\n    // Initialize an array with extended length\n    res := make([]int, len(nums)+enlarge)\n    // Copy all elements from the original array to the new array\n    for i, num := range nums {\n        res[i] = num\n    }\n    // Return the extended new array\n    return res\n}\n
array.swift
/* Extend array length */\nfunc extend(nums: [Int], enlarge: Int) -> [Int] {\n    // Initialize an array with extended length\n    var res = Array(repeating: 0, count: nums.count + enlarge)\n    // Copy all elements from the original array to the new array\n    for i in nums.indices {\n        res[i] = nums[i]\n    }\n    // Return the extended new array\n    return res\n}\n
array.js
/* Extend array length */\n// Note: JavaScript's Array is dynamic array, can be directly expanded\n// For learning purposes, this function treats Array as fixed-length array\nfunction extend(nums, enlarge) {\n    // Initialize an array with extended length\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Copy all elements from the original array to the new array\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
array.ts
/* Extend array length */\n// Note: TypeScript's Array is dynamic array, can be directly expanded\n// For learning purposes, this function treats Array as fixed-length array\nfunction extend(nums: number[], enlarge: number): number[] {\n    // Initialize an array with extended length\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Copy all elements from the original array to the new array\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
array.dart
/* Extend array length */\nList<int> extend(List<int> nums, int enlarge) {\n  // Initialize an array with extended length\n  List<int> res = List.filled(nums.length + enlarge, 0);\n  // Copy all elements from the original array to the new array\n  for (var i = 0; i < nums.length; i++) {\n    res[i] = nums[i];\n  }\n  // Return the extended new array\n  return res;\n}\n
array.rs
/* Extend array length */\nfn extend(nums: &[i32], enlarge: usize) -> Vec<i32> {\n    // Initialize an array with extended length\n    let mut res: Vec<i32> = vec![0; nums.len() + enlarge];\n    // Copy all elements from original array to new\n    res[0..nums.len()].copy_from_slice(nums);\n\n    // Return the extended new array\n    res\n}\n
array.c
/* Extend array length */\nint *extend(int *nums, int size, int enlarge) {\n    // Initialize an array with extended length\n    int *res = (int *)malloc(sizeof(int) * (size + enlarge));\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Initialize expanded space\n    for (int i = size; i < size + enlarge; i++) {\n        res[i] = 0;\n    }\n    // Return the extended new array\n    return res;\n}\n
array.kt
/* Extend array length */\nfun extend(nums: IntArray, enlarge: Int): IntArray {\n    // Initialize an array with extended length\n    val res = IntArray(nums.size + enlarge)\n    // Copy all elements from the original array to the new array\n    for (i in nums.indices) {\n        res[i] = nums[i]\n    }\n    // Return the extended new array\n    return res\n}\n
array.rb
### Extend array length ###\n# Note: Ruby's Array is dynamic array, can be directly expanded\n# For learning purposes, this function treats Array as fixed-length array\ndef extend(nums, enlarge)\n  # Initialize an array with extended length\n  res = Array.new(nums.length + enlarge, 0)\n\n  # Copy all elements from the original array to the new array\n  for i in 0...nums.length\n    res[i] = nums[i]\n  end\n\n  # Return the extended new array\n  res\nend\n
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#412-advantages-and-limitations-of-arrays","level":2,"title":"4.1.2   Advantages and Limitations of Arrays","text":"

Arrays are stored in contiguous memory space with elements of the same type. This approach contains rich prior information that the system can use to optimize the efficiency of data structure operations.

  • High space efficiency: Arrays allocate contiguous memory blocks for data without additional structural overhead.
  • Support for random access: Arrays allow accessing any element in \\(O(1)\\) time.
  • Cache locality: When accessing array elements, the computer not only loads the element but also caches the surrounding data, thereby leveraging the cache to improve the execution speed of subsequent operations.

Contiguous space storage is a double-edged sword with the following limitations:

  • Low insertion and deletion efficiency: When an array has many elements, insertion and deletion operations require shifting a large number of elements.
  • Immutable length: After an array is initialized, its length is fixed. Expanding the array requires copying all data to a new array, which is very costly.
  • Space waste: If the allocated size of an array exceeds what is actually needed, the extra space is wasted.
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#413-typical-applications-of-arrays","level":2,"title":"4.1.3   Typical Applications of Arrays","text":"

Arrays are a fundamental and common data structure, frequently used in various algorithms and for implementing various complex data structures.

  • Random access: If we want to randomly sample some items, we can use an array to store them and generate a random sequence to implement random sampling based on indices.
  • Sorting and searching: Arrays are the most commonly used data structure for sorting and searching algorithms. Quick sort, merge sort, binary search, and others are primarily performed on arrays.
  • Lookup tables: When we need to quickly find an element or its corresponding relationship, we can use an array as a lookup table. For example, if we want to implement a mapping from characters to ASCII codes, we can use the ASCII code value of a character as an index, with the corresponding element stored at that position in the array.
  • Machine learning: Neural networks make extensive use of linear algebra operations between vectors, matrices, and tensors, all of which are constructed in the form of arrays. Arrays are the most commonly used data structure in neural network programming.
  • Data structure implementation: Arrays can be used to implement stacks, queues, hash tables, heaps, graphs, and other data structures. For example, the adjacency matrix representation of a graph is essentially a two-dimensional array.
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   Linked List","text":"

Memory space is a shared resource for all programs. In a complex system runtime environment, available memory space may be scattered throughout the memory. We know that the memory space for storing an array must be contiguous, and when the array is very large, the memory may not be able to provide such a large contiguous space. This is where the flexibility advantage of linked lists becomes apparent.

A linked list is a linear data structure in which each element is a node object, and the nodes are connected through \"references\". A reference records the memory address of the next node, through which the next node can be accessed from the current node.

The design of linked lists allows nodes to be stored scattered throughout the memory, and their memory addresses do not need to be contiguous.

Figure 4-5   Linked list definition and storage method

Observing Figure 4-5, the basic unit of a linked list is a node object. Each node contains two pieces of data: the node's \"value\" and a \"reference\" to the next node.

  • The first node of a linked list is called the \"head node\", and the last node is called the \"tail node\".
  • The tail node points to \"null\", which is denoted as null, nullptr, and None in Java, C++, and Python, respectively.
  • In languages that support pointers, such as C, C++, Go, and Rust, the aforementioned \"reference\" should be replaced with \"pointer\".

As shown in the following code, a linked list node ListNode contains not only a value but also an additional reference (pointer). Therefore, linked lists occupy more memory space than arrays when storing the same amount of data.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class ListNode:\n    \"\"\"Linked list node class\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val               # Node value\n        self.next: ListNode | None = None # Reference to the next node\n
/* Linked list node structure */\nstruct ListNode {\n    int val;         // Node value\n    ListNode *next;  // Pointer to the next node\n    ListNode(int x) : val(x), next(nullptr) {}  // Constructor\n};\n
/* Linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode next;  // Reference to the next node\n    ListNode(int x) { val = x; }  // Constructor\n}\n
/* Linked list node class */\nclass ListNode(int x) {  // Constructor\n    int val = x;         // Node value\n    ListNode? next;      // Reference to the next node\n}\n
/* Linked list node structure */\ntype ListNode struct {\n    Val  int       // Node value\n    Next *ListNode // Pointer to the next node\n}\n\n// NewListNode Constructor, creates a new linked list\nfunc NewListNode(val int) *ListNode {\n    return &ListNode{\n        Val:  val,\n        Next: nil,\n    }\n}\n
/* Linked list node class */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Reference to the next node\n\n    init(x: Int) { // Constructor\n        val = x\n    }\n}\n
/* Linked list node class */\nclass ListNode {\n    constructor(val, next) {\n        this.val = (val === undefined ? 0 : val);       // Node value\n        this.next = (next === undefined ? null : next); // Reference to the next node\n    }\n}\n
/* Linked list node class */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    constructor(val?: number, next?: ListNode | null) {\n        this.val = val === undefined ? 0 : val;        // Node value\n        this.next = next === undefined ? null : next;  // Reference to the next node\n    }\n}\n
/* Linked list node class */\nclass ListNode {\n  int val; // Node value\n  ListNode? next; // Reference to the next node\n  ListNode(this.val, [this.next]); // Constructor\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n/* Linked list node class */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Node value\n    next: Option<Rc<RefCell<ListNode>>>, // Pointer to the next node\n}\n
/* Linked list node structure */\ntypedef struct ListNode {\n    int val;               // Node value\n    struct ListNode *next; // Pointer to the next node\n} ListNode;\n\n/* Constructor */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    return node;\n}\n
/* Linked list node class */\n// Constructor\nclass ListNode(x: Int) {\n    val _val: Int = x          // Node value\n    val next: ListNode? = null // Reference to the next node\n}\n
# Linked list node class\nclass ListNode\n  attr_accessor :val  # Node value\n  attr_accessor :next # Reference to the next node\n\n  def initialize(val=0, next_node=nil)\n    @val = val\n    @next = next_node\n  end\nend\n
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#421-common-linked-list-operations","level":2,"title":"4.2.1   Common Linked List Operations","text":"","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#1-initializing-a-linked-list","level":3,"title":"1.   Initializing a Linked List","text":"

Building a linked list involves two steps: first, initializing each node object; second, constructing the reference relationships between nodes. Once initialization is complete, we can traverse all nodes starting from the head node of the linked list through the reference next.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
# Initialize linked list 1 -> 3 -> 2 -> 5 -> 4\n# Initialize each node\nn0 = ListNode(1)\nn1 = ListNode(3)\nn2 = ListNode(2)\nn3 = ListNode(5)\nn4 = ListNode(4)\n# Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
linked_list.cpp
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode* n0 = new ListNode(1);\nListNode* n1 = new ListNode(3);\nListNode* n2 = new ListNode(2);\nListNode* n3 = new ListNode(5);\nListNode* n4 = new ListNode(4);\n// Build references between nodes\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
linked_list.java
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode n0 = new ListNode(1);\nListNode n1 = new ListNode(3);\nListNode n2 = new ListNode(2);\nListNode n3 = new ListNode(5);\nListNode n4 = new ListNode(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.cs
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode n0 = new(1);\nListNode n1 = new(3);\nListNode n2 = new(2);\nListNode n3 = new(5);\nListNode n4 = new(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.go
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nn0 := NewListNode(1)\nn1 := NewListNode(3)\nn2 := NewListNode(2)\nn3 := NewListNode(5)\nn4 := NewListNode(4)\n// Build references between nodes\nn0.Next = n1\nn1.Next = n2\nn2.Next = n3\nn3.Next = n4\n
linked_list.swift
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nlet n0 = ListNode(x: 1)\nlet n1 = ListNode(x: 3)\nlet n2 = ListNode(x: 2)\nlet n3 = ListNode(x: 5)\nlet n4 = ListNode(x: 4)\n// Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
linked_list.js
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.ts
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.dart
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\\\n// Initialize each node\nListNode n0 = ListNode(1);\nListNode n1 = ListNode(3);\nListNode n2 = ListNode(2);\nListNode n3 = ListNode(5);\nListNode n4 = ListNode(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.rs
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nlet n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));\nlet n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));\nlet n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));\nlet n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));\nlet n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));\n\n// Build references between nodes\nn0.borrow_mut().next = Some(n1.clone());\nn1.borrow_mut().next = Some(n2.clone());\nn2.borrow_mut().next = Some(n3.clone());\nn3.borrow_mut().next = Some(n4.clone());\n
linked_list.c
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode* n0 = newListNode(1);\nListNode* n1 = newListNode(3);\nListNode* n2 = newListNode(2);\nListNode* n3 = newListNode(5);\nListNode* n4 = newListNode(4);\n// Build references between nodes\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
linked_list.kt
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nval n0 = ListNode(1)\nval n1 = ListNode(3)\nval n2 = ListNode(2)\nval n3 = ListNode(5)\nval n4 = ListNode(4)\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.rb
# Initialize linked list 1 -> 3 -> 2 -> 5 -> 4\n# Initialize each node\nn0 = ListNode.new(1)\nn1 = ListNode.new(3)\nn2 = ListNode.new(2)\nn3 = ListNode.new(5)\nn4 = ListNode.new(4)\n# Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
Code Visualization

Full Screen >

An array is a single variable; for example, an array nums contains elements nums[0], nums[1], etc. A linked list, however, is composed of multiple independent node objects. We typically use the head node as the reference to the linked list; for example, the linked list in the above code can be referred to as linked list n0.

","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2-inserting-a-node","level":3,"title":"2.   Inserting a Node","text":"

Inserting a node in a linked list is very easy. As shown in Figure 4-6, suppose we want to insert a new node P between two adjacent nodes n0 and n1. We only need to change two node references (pointers), with a time complexity of \\(O(1)\\).

In contrast, the time complexity of inserting an element in an array is \\(O(n)\\), which is inefficient when dealing with large amounts of data.

Figure 4-6   Example of inserting a node into a linked list

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def insert(n0: ListNode, P: ListNode):\n    \"\"\"Insert node P after node n0 in the linked list\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
linked_list.cpp
/* Insert node P after node n0 in the linked list */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
linked_list.java
/* Insert node P after node n0 in the linked list */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.cs
/* Insert node P after node n0 in the linked list */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.go
/* Insert node P after node n0 in the linked list */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
linked_list.swift
/* Insert node P after node n0 in the linked list */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
linked_list.js
/* Insert node P after node n0 in the linked list */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.ts
/* Insert node P after node n0 in the linked list */\nfunction insert(n0: ListNode, P: ListNode): void {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.dart
/* Insert node P after node n0 in the linked list */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
linked_list.rs
/* Insert node P after node n0 in the linked list */\n#[allow(non_snake_case)]\npub fn insert<T>(n0: &Rc<RefCell<ListNode<T>>>, P: Rc<RefCell<ListNode<T>>>) {\n    let n1 = n0.borrow_mut().next.take();\n    P.borrow_mut().next = n1;\n    n0.borrow_mut().next = Some(P);\n}\n
linked_list.c
/* Insert node P after node n0 in the linked list */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
linked_list.kt
/* Insert node P after node n0 in the linked list */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
linked_list.rb
### Insert node _p after node n0 in linked list ###\n# Ruby's `p` is a built-in function, `P` is a constant, so use `_p` instead\ndef insert(n0, _p)\n  n1 = n0.next\n  _p.next = n1\n  n0.next = _p\nend\n
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#3-removing-a-node","level":3,"title":"3.   Removing a Node","text":"

As shown in Figure 4-7, removing a node in a linked list is also very convenient. We only need to change one node's reference (pointer).

Note that although node P still points to n1 after the deletion operation is complete, the linked list can no longer access P when traversing, which means P no longer belongs to this linked list.

Figure 4-7   Removing a node from a linked list

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def remove(n0: ListNode):\n    \"\"\"Remove the first node after node n0 in the linked list\"\"\"\n    if not n0.next:\n        return\n    # n0 -> P -> n1\n    P = n0.next\n    n1 = P.next\n    n0.next = n1\n
linked_list.cpp
/* Remove the first node after node n0 in the linked list */\nvoid remove(ListNode *n0) {\n    if (n0->next == nullptr)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // Free memory\n    delete P;\n}\n
linked_list.java
/* Remove the first node after node n0 in the linked list */\nvoid remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.cs
/* Remove the first node after node n0 in the linked list */\nvoid Remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode? n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.go
/* Remove the first node after node n0 in the linked list */\nfunc removeItem(n0 *ListNode) {\n    if n0.Next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    P := n0.Next\n    n1 := P.Next\n    n0.Next = n1\n}\n
linked_list.swift
/* Remove the first node after node n0 in the linked list */\nfunc remove(n0: ListNode) {\n    if n0.next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    let P = n0.next\n    let n1 = P?.next\n    n0.next = n1\n}\n
linked_list.js
/* Remove the first node after node n0 in the linked list */\nfunction remove(n0) {\n    if (!n0.next) return;\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.ts
/* Remove the first node after node n0 in the linked list */\nfunction remove(n0: ListNode): void {\n    if (!n0.next) {\n        return;\n    }\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.dart
/* Remove the first node after node n0 in the linked list */\nvoid remove(ListNode n0) {\n  if (n0.next == null) return;\n  // n0 -> P -> n1\n  ListNode P = n0.next!;\n  ListNode? n1 = P.next;\n  n0.next = n1;\n}\n
linked_list.rs
/* Remove the first node after node n0 in the linked list */\n#[allow(non_snake_case)]\npub fn remove<T>(n0: &Rc<RefCell<ListNode<T>>>) {\n    // n0 -> P -> n1\n    let P = n0.borrow_mut().next.take();\n    if let Some(node) = P {\n        let n1 = node.borrow_mut().next.take();\n        n0.borrow_mut().next = n1;\n    }\n}\n
linked_list.c
/* Remove the first node after node n0 in the linked list */\n// Note: stdio.h occupies the remove keyword\nvoid removeItem(ListNode *n0) {\n    if (!n0->next)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // Free memory\n    free(P);\n}\n
linked_list.kt
/* Remove the first node after node n0 in the linked list */\nfun remove(n0: ListNode?) {\n    if (n0?.next == null)\n        return\n    // n0 -> P -> n1\n    val p = n0.next\n    val n1 = p?.next\n    n0.next = n1\n}\n
linked_list.rb
### Delete first node after node n0 in linked list ###\ndef remove(n0)\n  return if n0.next.nil?\n\n  # n0 -> remove_node -> n1\n  remove_node = n0.next\n  n1 = remove_node.next\n  n0.next = n1\nend\n
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#4-accessing-a-node","level":3,"title":"4.   Accessing a Node","text":"

Accessing nodes in a linked list is less efficient. As mentioned in the previous section, we can access any element in an array in \\(O(1)\\) time. This is not the case with linked lists. The program needs to start from the head node and traverse backward one by one until the target node is found. That is, accessing the \\(i\\)-th node in a linked list requires \\(i - 1\\) iterations, with a time complexity of \\(O(n)\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def access(head: ListNode, index: int) -> ListNode | None:\n    \"\"\"Access the node at index index in the linked list\"\"\"\n    for _ in range(index):\n        if not head:\n            return None\n        head = head.next\n    return head\n
linked_list.cpp
/* Access the node at index index in the linked list */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == nullptr)\n            return nullptr;\n        head = head->next;\n    }\n    return head;\n}\n
linked_list.java
/* Access the node at index index in the linked list */\nListNode access(ListNode head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.cs
/* Access the node at index index in the linked list */\nListNode? Access(ListNode? head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.go
/* Access the node at index index in the linked list */\nfunc access(head *ListNode, index int) *ListNode {\n    for i := 0; i < index; i++ {\n        if head == nil {\n            return nil\n        }\n        head = head.Next\n    }\n    return head\n}\n
linked_list.swift
/* Access the node at index index in the linked list */\nfunc access(head: ListNode, index: Int) -> ListNode? {\n    var head: ListNode? = head\n    for _ in 0 ..< index {\n        if head == nil {\n            return nil\n        }\n        head = head?.next\n    }\n    return head\n}\n
linked_list.js
/* Access the node at index index in the linked list */\nfunction access(head, index) {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.ts
/* Access the node at index index in the linked list */\nfunction access(head: ListNode | null, index: number): ListNode | null {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.dart
/* Access the node at index index in the linked list */\nListNode? access(ListNode? head, int index) {\n  for (var i = 0; i < index; i++) {\n    if (head == null) return null;\n    head = head.next;\n  }\n  return head;\n}\n
linked_list.rs
/* Access the node at index index in the linked list */\npub fn access<T>(head: Rc<RefCell<ListNode<T>>>, index: i32) -> Option<Rc<RefCell<ListNode<T>>>> {\n    fn dfs<T>(\n        head: Option<&Rc<RefCell<ListNode<T>>>>,\n        index: i32,\n    ) -> Option<Rc<RefCell<ListNode<T>>>> {\n        if index <= 0 {\n            return head.cloned();\n        }\n\n        if let Some(node) = head {\n            dfs(node.borrow().next.as_ref(), index - 1)\n        } else {\n            None\n        }\n    }\n\n    dfs(Some(head).as_ref(), index)\n}\n
linked_list.c
/* Access the node at index index in the linked list */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == NULL)\n            return NULL;\n        head = head->next;\n    }\n    return head;\n}\n
linked_list.kt
/* Access the node at index index in the linked list */\nfun access(head: ListNode?, index: Int): ListNode? {\n    var h = head\n    for (i in 0..<index) {\n        if (h == null)\n            return null\n        h = h.next\n    }\n    return h\n}\n
linked_list.rb
### Access node at index in linked list ###\ndef access(head, index)\n  for i in 0...index\n    return nil if head.nil?\n    head = head.next\n  end\n\n  head\nend\n
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#5-finding-a-node","level":3,"title":"5.   Finding a Node","text":"

Traverse the linked list to find a node with value target, and output the index of that node in the linked list. This process is also a linear search. The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def find(head: ListNode, target: int) -> int:\n    \"\"\"Find the first node with value target in the linked list\"\"\"\n    index = 0\n    while head:\n        if head.val == target:\n            return index\n        head = head.next\n        index += 1\n    return -1\n
linked_list.cpp
/* Find the first node with value target in the linked list */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head != nullptr) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.java
/* Find the first node with value target in the linked list */\nint find(ListNode head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.cs
/* Find the first node with value target in the linked list */\nint Find(ListNode? head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.go
/* Find the first node with value target in the linked list */\nfunc findNode(head *ListNode, target int) int {\n    index := 0\n    for head != nil {\n        if head.Val == target {\n            return index\n        }\n        head = head.Next\n        index++\n    }\n    return -1\n}\n
linked_list.swift
/* Find the first node with value target in the linked list */\nfunc find(head: ListNode, target: Int) -> Int {\n    var head: ListNode? = head\n    var index = 0\n    while head != nil {\n        if head?.val == target {\n            return index\n        }\n        head = head?.next\n        index += 1\n    }\n    return -1\n}\n
linked_list.js
/* Find the first node with value target in the linked list */\nfunction find(head, target) {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
linked_list.ts
/* Find the first node with value target in the linked list */\nfunction find(head: ListNode | null, target: number): number {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
linked_list.dart
/* Find the first node with value target in the linked list */\nint find(ListNode? head, int target) {\n  int index = 0;\n  while (head != null) {\n    if (head.val == target) {\n      return index;\n    }\n    head = head.next;\n    index++;\n  }\n  return -1;\n}\n
linked_list.rs
/* Find the first node with value target in the linked list */\npub fn find<T: PartialEq>(head: Rc<RefCell<ListNode<T>>>, target: T) -> i32 {\n    fn find<T: PartialEq>(head: Option<&Rc<RefCell<ListNode<T>>>>, target: T, idx: i32) -> i32 {\n        if let Some(node) = head {\n            if node.borrow().val == target {\n                return idx;\n            }\n            return find(node.borrow().next.as_ref(), target, idx + 1);\n        } else {\n            -1\n        }\n    }\n\n    find(Some(head).as_ref(), target, 0)\n}\n
linked_list.c
/* Find the first node with value target in the linked list */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.kt
/* Find the first node with value target in the linked list */\nfun find(head: ListNode?, target: Int): Int {\n    var index = 0\n    var h = head\n    while (h != null) {\n        if (h._val == target)\n            return index\n        h = h.next\n        index++\n    }\n    return -1\n}\n
linked_list.rb
### Find first node with value target in linked list ###\ndef find(head, target)\n  index = 0\n  while head\n    return index if head.val == target\n    head = head.next\n    index += 1\n  end\n\n  -1\nend\n
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#422-arrays-vs-linked-lists","level":2,"title":"4.2.2   Arrays vs. Linked Lists","text":"

Table 4-1 summarizes the characteristics of arrays and linked lists and compares their operational efficiencies. Since they employ two opposite storage strategies, their various properties and operational efficiencies also exhibit contrasting characteristics.

Table 4-1   Comparison of array and linked list efficiencies

Array Linked List Storage method Contiguous memory space Scattered memory space Capacity expansion Immutable length Flexible expansion Memory efficiency Elements occupy less memory, but space may be wasted Elements occupy more memory Accessing an element \\(O(1)\\) \\(O(n)\\) Adding an element \\(O(n)\\) \\(O(1)\\) Removing an element \\(O(n)\\) \\(O(1)\\)","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#423-common-types-of-linked-lists","level":2,"title":"4.2.3   Common Types of Linked Lists","text":"

As shown in Figure 4-8, there are three common types of linked lists:

  • Singly linked list: This is the ordinary linked list introduced earlier. The nodes of a singly linked list contain a value and a reference to the next node. We call the first node the head node and the last node the tail node, which points to null None.
  • Circular linked list: If we make the tail node of a singly linked list point to the head node (connecting the tail to the head), we get a circular linked list. In a circular linked list, any node can be viewed as the head node.
  • Doubly linked list: Compared to a singly linked list, a doubly linked list records references in both directions. The node definition of a doubly linked list includes references to both the successor node (next node) and the predecessor node (previous node). Compared to a singly linked list, a doubly linked list is more flexible and can traverse the linked list in both directions, but it also requires more memory space.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class ListNode:\n    \"\"\"Doubly linked list node class\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Node value\n        self.next: ListNode | None = None  # Reference to the successor node\n        self.prev: ListNode | None = None  # Reference to the predecessor node\n
/* Doubly linked list node structure */\nstruct ListNode {\n    int val;         // Node value\n    ListNode *next;  // Pointer to the successor node\n    ListNode *prev;  // Pointer to the predecessor node\n    ListNode(int x) : val(x), next(nullptr), prev(nullptr) {}  // Constructor\n};\n
/* Doubly linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode next;  // Reference to the successor node\n    ListNode prev;  // Reference to the predecessor node\n    ListNode(int x) { val = x; }  // Constructor\n}\n
/* Doubly linked list node class */\nclass ListNode(int x) {  // Constructor\n    int val = x;    // Node value\n    ListNode next;  // Reference to the successor node\n    ListNode prev;  // Reference to the predecessor node\n}\n
/* Doubly linked list node structure */\ntype DoublyListNode struct {\n    Val  int             // Node value\n    Next *DoublyListNode // Pointer to the successor node\n    Prev *DoublyListNode // Pointer to the predecessor node\n}\n\n// NewDoublyListNode Initialization\nfunc NewDoublyListNode(val int) *DoublyListNode {\n    return &DoublyListNode{\n        Val:  val,\n        Next: nil,\n        Prev: nil,\n    }\n}\n
/* Doubly linked list node class */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Reference to the successor node\n    var prev: ListNode? // Reference to the predecessor node\n\n    init(x: Int) { // Constructor\n        val = x\n    }\n}\n
/* Doubly linked list node class */\nclass ListNode {\n    constructor(val, next, prev) {\n        this.val = val  ===  undefined ? 0 : val;        // Node value\n        this.next = next  ===  undefined ? null : next;  // Reference to the successor node\n        this.prev = prev  ===  undefined ? null : prev;  // Reference to the predecessor node\n    }\n}\n
/* Doubly linked list node class */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    prev: ListNode | null;\n    constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {\n        this.val = val  ===  undefined ? 0 : val;        // Node value\n        this.next = next  ===  undefined ? null : next;  // Reference to the successor node\n        this.prev = prev  ===  undefined ? null : prev;  // Reference to the predecessor node\n    }\n}\n
/* Doubly linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode? next;  // Reference to the successor node\n    ListNode? prev;  // Reference to the predecessor node\n    ListNode(this.val, [this.next, this.prev]);  // Constructor\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Doubly linked list node type */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Node value\n    next: Option<Rc<RefCell<ListNode>>>, // Pointer to the successor node\n    prev: Option<Rc<RefCell<ListNode>>>, // Pointer to the predecessor node\n}\n\n/* Constructor */\nimpl ListNode {\n    fn new(val: i32) -> Self {\n        ListNode {\n            val,\n            next: None,\n            prev: None,\n        }\n    }\n}\n
/* Doubly linked list node structure */\ntypedef struct ListNode {\n    int val;               // Node value\n    struct ListNode *next; // Pointer to the successor node\n    struct ListNode *prev; // Pointer to the predecessor node\n} ListNode;\n\n/* Constructor */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    node->prev = NULL;\n    return node;\n}\n
/* Doubly linked list node class */\n// Constructor\nclass ListNode(x: Int) {\n    val _val: Int = x           // Node value\n    val next: ListNode? = null  // Reference to the successor node\n    val prev: ListNode? = null  // Reference to the predecessor node\n}\n
# Doubly linked list node class\nclass ListNode\n  attr_accessor :val    # Node value\n  attr_accessor :next   # Reference to the successor node\n  attr_accessor :prev   # Reference to the predecessor node\n\n  def initialize(val=0, next_node=nil, prev_node=nil)\n    @val = val\n    @next = next_node\n    @prev = prev_node\n  end\nend\n

Figure 4-8   Common types of linked lists

","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#424-typical-applications-of-linked-lists","level":2,"title":"4.2.4   Typical Applications of Linked Lists","text":"

Singly linked lists are commonly used to implement stacks, queues, hash tables, and graphs.

  • Stacks and queues: When insertion and deletion operations both occur at one end of the linked list, it exhibits last-in-first-out characteristics, corresponding to a stack. When insertion operations occur at one end of the linked list and deletion operations occur at the other end, it exhibits first-in-first-out characteristics, corresponding to a queue.
  • Hash tables: Separate chaining is one of the mainstream solutions for resolving hash collisions. In this approach, all colliding elements are placed in a linked list.
  • Graphs: An adjacency list is a common way to represent a graph, where each vertex in the graph is associated with a linked list, and each element in the linked list represents another vertex connected to that vertex.

Doubly linked lists are commonly used in scenarios where quick access to the previous and next elements is needed.

  • Advanced data structures: For example, in red-black trees and B-trees, we need to access the parent node of a node, which can be achieved by saving a reference to the parent node in the node, similar to a doubly linked list.
  • Browser history: In web browsers, when a user clicks the forward or backward button, the browser needs to know the previous and next web pages the user visited. The characteristics of doubly linked lists make this operation simple.
  • LRU algorithm: In cache eviction (LRU) algorithms, we need to quickly find the least recently used data and support quick addition and deletion of nodes. Using a doubly linked list is very suitable for this.

Circular linked lists are commonly used in scenarios that require periodic operations, such as operating system resource scheduling.

  • Round-robin scheduling algorithm: In operating systems, round-robin scheduling is a common CPU scheduling algorithm that needs to cycle through a set of processes. Each process is assigned a time slice, and when the time slice expires, the CPU switches to the next process. This cyclic operation can be implemented using a circular linked list.
  • Data buffers: In some data buffer implementations, circular linked lists may also be used. For example, in audio and video players, the data stream may be divided into multiple buffer blocks and placed in a circular linked list to achieve seamless playback.
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   List","text":"

A list is an abstract data structure concept that represents an ordered collection of elements, supporting operations such as element access, modification, insertion, deletion, and traversal, without requiring users to consider capacity limitations. Lists can be implemented based on linked lists or arrays.

  • A linked list can naturally be viewed as a list, supporting element insertion, deletion, search, and modification operations, and can flexibly expand dynamically.
  • An array also supports element insertion, deletion, search, and modification, but since its length is immutable, it can only be viewed as a list with length limitations.

When implementing lists using arrays, the immutable length property reduces the practicality of the list. This is because we usually cannot determine in advance how much data we need to store, making it difficult to choose an appropriate list length. If the length is too small, it may fail to meet usage requirements; if the length is too large, it will waste memory space.

To solve this problem, we can use a dynamic array to implement a list. It inherits all the advantages of arrays and can dynamically expand during program execution.

In fact, the lists provided in the standard libraries of many programming languages are implemented based on dynamic arrays, such as list in Python, ArrayList in Java, vector in C++, and List in C#. In the following discussion, we will treat \"list\" and \"dynamic array\" as equivalent concepts.

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#431-common-list-operations","level":2,"title":"4.3.1   Common List Operations","text":"","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#1-initialize-a-list","level":3,"title":"1.   Initialize a List","text":"

We typically use two initialization methods: \"without initial values\" and \"with initial values\":

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Initialize a list\n# Without initial values\nnums1: list[int] = []\n# With initial values\nnums: list[int] = [1, 3, 2, 5, 4]\n
list.cpp
/* Initialize a list */\n// Note that vector in C++ is equivalent to nums as described in this article\n// Without initial values\nvector<int> nums1;\n// With initial values\nvector<int> nums = { 1, 3, 2, 5, 4 };\n
list.java
/* Initialize a list */\n// Without initial values\nList<Integer> nums1 = new ArrayList<>();\n// With initial values (note that array elements should use the wrapper class Integer[] instead of int[])\nInteger[] numbers = new Integer[] { 1, 3, 2, 5, 4 };\nList<Integer> nums = new ArrayList<>(Arrays.asList(numbers));\n
list.cs
/* Initialize a list */\n// Without initial values\nList<int> nums1 = [];\n// With initial values\nint[] numbers = [1, 3, 2, 5, 4];\nList<int> nums = [.. numbers];\n
list_test.go
/* Initialize a list */\n// Without initial values\nnums1 := []int{}\n// With initial values\nnums := []int{1, 3, 2, 5, 4}\n
list.swift
/* Initialize a list */\n// Without initial values\nlet nums1: [Int] = []\n// With initial values\nvar nums = [1, 3, 2, 5, 4]\n
list.js
/* Initialize a list */\n// Without initial values\nconst nums1 = [];\n// With initial values\nconst nums = [1, 3, 2, 5, 4];\n
list.ts
/* Initialize a list */\n// Without initial values\nconst nums1: number[] = [];\n// With initial values\nconst nums: number[] = [1, 3, 2, 5, 4];\n
list.dart
/* Initialize a list */\n// Without initial values\nList<int> nums1 = [];\n// With initial values\nList<int> nums = [1, 3, 2, 5, 4];\n
list.rs
/* Initialize a list */\n// Without initial values\nlet nums1: Vec<i32> = Vec::new();\n// With initial values\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Initialize a list */\n// Without initial values\nvar nums1 = listOf<Int>()\n// With initial values\nvar numbers = arrayOf(1, 3, 2, 5, 4)\nvar nums = numbers.toMutableList()\n
list.rb
# Initialize a list\n# Without initial values\nnums1 = []\n# With initial values\nnums = [1, 3, 2, 5, 4]\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#2-access-elements","level":3,"title":"2.   Access Elements","text":"

Since a list is essentially an array, we can access and update elements in \\(O(1)\\) time complexity, which is very efficient.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Access an element\nnum: int = nums[1]  # Access element at index 1\n\n# Update an element\nnums[1] = 0    # Update element at index 1 to 0\n
list.cpp
/* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
list.java
/* Access an element */\nint num = nums.get(1);  // Access element at index 1\n\n/* Update an element */\nnums.set(1, 0);  // Update element at index 1 to 0\n
list.cs
/* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
list_test.go
/* Access an element */\nnum := nums[1]  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0     // Update element at index 1 to 0\n
list.swift
/* Access an element */\nlet num = nums[1] // Access element at index 1\n\n/* Update an element */\nnums[1] = 0 // Update element at index 1 to 0\n
list.js
/* Access an element */\nconst num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
list.ts
/* Access an element */\nconst num: number = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
list.dart
/* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
list.rs
/* Access an element */\nlet num: i32 = nums[1];  // Access element at index 1\n/* Update an element */\nnums[1] = 0;             // Update element at index 1 to 0\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Access an element */\nval num = nums[1]       // Access element at index 1\n/* Update an element */\nnums[1] = 0             // Update element at index 1 to 0\n
list.rb
# Access an element\nnum = nums[1] # Access element at index 1\n# Update an element\nnums[1] = 0 # Update element at index 1 to 0\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#3-insert-and-delete-elements","level":3,"title":"3.   Insert and Delete Elements","text":"

Compared to arrays, lists can freely add and delete elements. Adding an element at the end of a list has a time complexity of \\(O(1)\\), but inserting and deleting elements still have the same efficiency as arrays, with a time complexity of \\(O(n)\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Clear the list\nnums.clear()\n\n# Add elements at the end\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n# Insert an element in the middle\nnums.insert(3, 6)  # Insert number 6 at index 3\n\n# Delete an element\nnums.pop(3)        # Delete element at index 3\n
list.cpp
/* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.push_back(1);\nnums.push_back(3);\nnums.push_back(2);\nnums.push_back(5);\nnums.push_back(4);\n\n/* Insert an element in the middle */\nnums.insert(nums.begin() + 3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.erase(nums.begin() + 3);      // Delete element at index 3\n
list.java
/* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.add(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);  // Delete element at index 3\n
list.cs
/* Clear the list */\nnums.Clear();\n\n/* Add elements at the end */\nnums.Add(1);\nnums.Add(3);\nnums.Add(2);\nnums.Add(5);\nnums.Add(4);\n\n/* Insert an element in the middle */\nnums.Insert(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.RemoveAt(3);  // Delete element at index 3\n
list_test.go
/* Clear the list */\nnums = nil\n\n/* Add elements at the end */\nnums = append(nums, 1)\nnums = append(nums, 3)\nnums = append(nums, 2)\nnums = append(nums, 5)\nnums = append(nums, 4)\n\n/* Insert an element in the middle */\nnums = append(nums[:3], append([]int{6}, nums[3:]...)...) // Insert number 6 at index 3\n\n/* Delete an element */\nnums = append(nums[:3], nums[4:]...) // Delete element at index 3\n
list.swift
/* Clear the list */\nnums.removeAll()\n\n/* Add elements at the end */\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n/* Insert an element in the middle */\nnums.insert(6, at: 3) // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(at: 3) // Delete element at index 3\n
list.js
/* Clear the list */\nnums.length = 0;\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.splice(3, 0, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.splice(3, 1);  // Delete element at index 3\n
list.ts
/* Clear the list */\nnums.length = 0;\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.splice(3, 0, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.splice(3, 1);  // Delete element at index 3\n
list.dart
/* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.insert(3, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.removeAt(3); // Delete element at index 3\n
list.rs
/* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.insert(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);    // Delete element at index 3\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.add(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);  // Delete element at index 3\n
list.rb
# Clear the list\nnums.clear\n\n# Add elements at the end\nnums << 1\nnums << 3\nnums << 2\nnums << 5\nnums << 4\n\n# Insert an element in the middle\nnums.insert(3, 6) # Insert number 6 at index 3\n\n# Delete an element\nnums.delete_at(3) # Delete element at index 3\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#4-traverse-a-list","level":3,"title":"4.   Traverse a List","text":"

Like arrays, lists can be traversed by index or by directly iterating through elements.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Traverse the list by index\ncount = 0\nfor i in range(len(nums)):\n    count += nums[i]\n\n# Traverse list elements directly\nfor num in nums:\n    count += num\n
list.cpp
/* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (int num : nums) {\n    count += num;\n}\n
list.java
/* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums.get(i);\n}\n\n/* Traverse list elements directly */\nfor (int num : nums) {\n    count += num;\n}\n
list.cs
/* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.Count; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nforeach (int num in nums) {\n    count += num;\n}\n
list_test.go
/* Traverse the list by index */\ncount := 0\nfor i := 0; i < len(nums); i++ {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\ncount = 0\nfor _, num := range nums {\n    count += num\n}\n
list.swift
/* Traverse the list by index */\nvar count = 0\nfor i in nums.indices {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\ncount = 0\nfor num in nums {\n    count += num\n}\n
list.js
/* Traverse the list by index */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
list.ts
/* Traverse the list by index */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
list.dart
/* Traverse the list by index */\nint count = 0;\nfor (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (var num in nums) {\n    count += num;\n}\n
list.rs
// Traverse the list by index\nlet mut _count = 0;\nfor i in 0..nums.len() {\n    _count += nums[i];\n}\n\n// Traverse list elements directly\n_count = 0;\nfor num in &nums {\n    _count += num;\n}\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Traverse the list by index */\nvar count = 0\nfor (i in nums.indices) {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\nfor (num in nums) {\n    count += num\n}\n
list.rb
# Traverse the list by index\ncount = 0\nfor i in 0...nums.length\n    count += nums[i]\nend\n\n# Traverse list elements directly\ncount = 0\nfor num in nums\n    count += num\nend\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#5-concatenate-lists","level":3,"title":"5.   Concatenate Lists","text":"

Given a new list nums1, we can concatenate it to the end of the original list.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Concatenate two lists\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1  # Concatenate list nums1 to the end of nums\n
list.cpp
/* Concatenate two lists */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// Concatenate list nums1 to the end of nums\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n
list.java
/* Concatenate two lists */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1);  // Concatenate list nums1 to the end of nums\n
list.cs
/* Concatenate two lists */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1);  // Concatenate list nums1 to the end of nums\n
list_test.go
/* Concatenate two lists */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...)  // Concatenate list nums1 to the end of nums\n
list.swift
/* Concatenate two lists */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // Concatenate list nums1 to the end of nums\n
list.js
/* Concatenate two lists */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Concatenate list nums1 to the end of nums\n
list.ts
/* Concatenate two lists */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Concatenate list nums1 to the end of nums\n
list.dart
/* Concatenate two lists */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1);  // Concatenate list nums1 to the end of nums\n
list.rs
/* Concatenate two lists */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Concatenate two lists */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1)  // Concatenate list nums1 to the end of nums\n
list.rb
# Concatenate two lists\nnums1 = [6, 8, 7, 10, 9]\nnums += nums1\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#6-sort-a-list","level":3,"title":"6.   Sort a List","text":"

After sorting a list, we can use \"binary search\" and \"two-pointer\" algorithms, which are frequently tested in array algorithm problems.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Sort a list\nnums.sort()  # After sorting, list elements are arranged from smallest to largest\n
list.cpp
/* Sort a list */\nsort(nums.begin(), nums.end());  // After sorting, list elements are arranged from smallest to largest\n
list.java
/* Sort a list */\nCollections.sort(nums);  // After sorting, list elements are arranged from smallest to largest\n
list.cs
/* Sort a list */\nnums.Sort(); // After sorting, list elements are arranged from smallest to largest\n
list_test.go
/* Sort a list */\nsort.Ints(nums)  // After sorting, list elements are arranged from smallest to largest\n
list.swift
/* Sort a list */\nnums.sort() // After sorting, list elements are arranged from smallest to largest\n
list.js
/* Sort a list */\nnums.sort((a, b) => a - b);  // After sorting, list elements are arranged from smallest to largest\n
list.ts
/* Sort a list */\nnums.sort((a, b) => a - b);  // After sorting, list elements are arranged from smallest to largest\n
list.dart
/* Sort a list */\nnums.sort(); // After sorting, list elements are arranged from smallest to largest\n
list.rs
/* Sort a list */\nnums.sort(); // After sorting, list elements are arranged from smallest to largest\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Sort a list */\nnums.sort() // After sorting, list elements are arranged from smallest to largest\n
list.rb
# Sort a list\nnums = nums.sort { |a, b| a <=> b } # After sorting, list elements are arranged from smallest to largest\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#432-list-implementation","level":2,"title":"4.3.2   List Implementation","text":"

Many programming languages have built-in lists, such as Java, C++, and Python. Their implementations are quite complex, and the parameters are carefully considered, such as initial capacity, expansion multiples, and so on. Interested readers can consult the source code to learn more.

To deepen our understanding of how lists work, we attempt to implement a simple list with three key design considerations:

  • Initial capacity: Select a reasonable initial capacity for the underlying array. In this example, we choose 10 as the initial capacity.
  • Size tracking: Declare a variable size to record the current number of elements in the list and update it in real-time as elements are inserted and deleted. Based on this variable, we can locate the end of the list and determine whether expansion is needed.
  • Expansion mechanism: When the list capacity is full upon inserting an element, we need to expand. We create a larger array based on the expansion multiple and then move all elements from the current array to the new array in order. In this example, we specify that the array should be expanded to 2 times its previous size each time.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_list.py
class MyList:\n    \"\"\"List class\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._capacity: int = 10  # List capacity\n        self._arr: list[int] = [0] * self._capacity  # Array (stores list elements)\n        self._size: int = 0  # List length (current number of elements)\n        self._extend_ratio: int = 2  # Multiple by which the list capacity is extended each time\n\n    def size(self) -> int:\n        \"\"\"Get list length (current number of elements)\"\"\"\n        return self._size\n\n    def capacity(self) -> int:\n        \"\"\"Get list capacity\"\"\"\n        return self._capacity\n\n    def get(self, index: int) -> int:\n        \"\"\"Access element\"\"\"\n        # If the index is out of bounds, throw an exception, as below\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        return self._arr[index]\n\n    def set(self, num: int, index: int):\n        \"\"\"Update element\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        self._arr[index] = num\n\n    def add(self, num: int):\n        \"\"\"Add element at the end\"\"\"\n        # When the number of elements exceeds capacity, trigger the extension mechanism\n        if self.size() == self.capacity():\n            self.extend_capacity()\n        self._arr[self._size] = num\n        self._size += 1\n\n    def insert(self, num: int, index: int):\n        \"\"\"Insert element in the middle\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        # When the number of elements exceeds capacity, trigger the extension mechanism\n        if self._size == self.capacity():\n            self.extend_capacity()\n        # Move all elements at and after index index backward by one position\n        for j in range(self._size - 1, index - 1, -1):\n            self._arr[j + 1] = self._arr[j]\n        self._arr[index] = num\n        # Update the number of elements\n        self._size += 1\n\n    def remove(self, index: int) -> int:\n        \"\"\"Remove element\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        num = self._arr[index]\n        # Move all elements after index index forward by one position\n        for j in range(index, self._size - 1):\n            self._arr[j] = self._arr[j + 1]\n        # Update the number of elements\n        self._size -= 1\n        # Return the removed element\n        return num\n\n    def extend_capacity(self):\n        \"\"\"Extend list capacity\"\"\"\n        # Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1)\n        # Update list capacity\n        self._capacity = len(self._arr)\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return list with valid length\"\"\"\n        return self._arr[: self._size]\n
my_list.cpp
/* List class */\nclass MyList {\n  private:\n    int *arr;             // Array (stores list elements)\n    int arrCapacity = 10; // List capacity\n    int arrSize = 0;      // List length (current number of elements)\n    int extendRatio = 2;   // Multiple by which the list capacity is extended each time\n\n  public:\n    /* Constructor */\n    MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Destructor */\n    ~MyList() {\n        delete[] arr;\n    }\n\n    /* Get list length (current number of elements)*/\n    int size() {\n        return arrSize;\n    }\n\n    /* Get list capacity */\n    int capacity() {\n        return arrCapacity;\n    }\n\n    /* Update element */\n    int get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    void set(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    void add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size() == capacity())\n            extendCapacity();\n        arr[size()] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Sort list */\n    void insert(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size() == capacity())\n            extendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = size() - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Remove element */\n    int remove(int index) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        int num = arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for (int j = index; j < size() - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        arrSize--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    void extendCapacity() {\n        // Create a new array with length extendRatio times the original array\n        int newCapacity = capacity() * extendRatio;\n        int *tmp = arr;\n        arr = new int[newCapacity];\n        // Copy all elements from the original array to the new array\n        for (int i = 0; i < size(); i++) {\n            arr[i] = tmp[i];\n        }\n        // Free memory\n        delete[] tmp;\n        arrCapacity = newCapacity;\n    }\n\n    /* Convert list to Vector for printing */\n    vector<int> toVector() {\n        // Elements enqueue\n        vector<int> vec(size());\n        for (int i = 0; i < size(); i++) {\n            vec[i] = arr[i];\n        }\n        return vec;\n    }\n};\n
my_list.java
/* List class */\nclass MyList {\n    private int[] arr; // Array (stores list elements)\n    private int capacity = 10; // List capacity\n    private int size = 0; // List length (current number of elements)\n    private int extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    public MyList() {\n        arr = new int[capacity];\n    }\n\n    /* Get list length (current number of elements) */\n    public int size() {\n        return size;\n    }\n\n    /* Get list capacity */\n    public int capacity() {\n        return capacity;\n    }\n\n    /* Update element */\n    public int get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    public void set(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public void add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity();\n        arr[size] = num;\n        // Update the number of elements\n        size++;\n    }\n\n    /* Sort list */\n    public void insert(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = size - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        size++;\n    }\n\n    /* Remove element */\n    public int remove(int index) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        int num = arr[index];\n        // Move all elements after index forward by one position\n        for (int j = index; j < size - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public void extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = Arrays.copyOf(arr, capacity() * extendRatio);\n        // Add elements at the end\n        capacity = arr.length;\n    }\n\n    /* Convert list to array */\n    public int[] toArray() {\n        int size = size();\n        // Elements enqueue\n        int[] arr = new int[size];\n        for (int i = 0; i < size; i++) {\n            arr[i] = get(i);\n        }\n        return arr;\n    }\n}\n
my_list.cs
/* List class */\nclass MyList {\n    private int[] arr;           // Array (stores list elements)\n    private int arrCapacity = 10;    // List capacity\n    private int arrSize = 0;         // List length (current number of elements)\n    private readonly int extendRatio = 2;  // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    public MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Get list length (current number of elements) */\n    public int Size() {\n        return arrSize;\n    }\n\n    /* Get list capacity */\n    public int Capacity() {\n        return arrCapacity;\n    }\n\n    /* Update element */\n    public int Get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    public void Set(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public void Add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        arr[arrSize] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Sort list */\n    public void Insert(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = arrSize - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Remove element */\n    public int Remove(int index) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        int num = arr[index];\n        // Move all elements after index forward by one position\n        for (int j = index; j < arrSize - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        arrSize--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public void ExtendCapacity() {\n        // Create new array of length arrCapacity * extendRatio and copy original array to new array\n        Array.Resize(ref arr, arrCapacity * extendRatio);\n        // Add elements at the end\n        arrCapacity = arr.Length;\n    }\n\n    /* Convert list to array */\n    public int[] ToArray() {\n        // Elements enqueue\n        int[] arr = new int[arrSize];\n        for (int i = 0; i < arrSize; i++) {\n            arr[i] = Get(i);\n        }\n        return arr;\n    }\n}\n
my_list.go
/* List class */\ntype myList struct {\n    arrCapacity int\n    arr         []int\n    arrSize     int\n    extendRatio int\n}\n\n/* Constructor */\nfunc newMyList() *myList {\n    return &myList{\n        arrCapacity: 10,              // List capacity\n        arr:         make([]int, 10), // Array (stores list elements)\n        arrSize:     0,               // List length (current number of elements)\n        extendRatio: 2,               // Multiple by which the list capacity is extended each time\n    }\n}\n\n/* Get list length (current number of elements) */\nfunc (l *myList) size() int {\n    return l.arrSize\n}\n\n/* Get list capacity */\nfunc (l *myList) capacity() int {\n    return l.arrCapacity\n}\n\n/* Update element */\nfunc (l *myList) get(index int) int {\n    // If the index is out of bounds, throw an exception, as below\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    return l.arr[index]\n}\n\n/* Add elements at the end */\nfunc (l *myList) set(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    l.arr[index] = num\n}\n\n/* Direct traversal of list elements */\nfunc (l *myList) add(num int) {\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    l.arr[l.arrSize] = num\n    // Update the number of elements\n    l.arrSize++\n}\n\n/* Sort list */\nfunc (l *myList) insert(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    // Move all elements after index index forward by one position\n    for j := l.arrSize - 1; j >= index; j-- {\n        l.arr[j+1] = l.arr[j]\n    }\n    l.arr[index] = num\n    // Update the number of elements\n    l.arrSize++\n}\n\n/* Remove element */\nfunc (l *myList) remove(index int) int {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    num := l.arr[index]\n    // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n    for j := index; j < l.arrSize-1; j++ {\n        l.arr[j] = l.arr[j+1]\n    }\n    // Update the number of elements\n    l.arrSize--\n    // Return the removed element\n    return num\n}\n\n/* Driver Code */\nfunc (l *myList) extendCapacity() {\n    // Create a new array with length extendRatio times the original array and copy the original array to the new array\n    l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...)\n    // Add elements at the end\n    l.arrCapacity = len(l.arr)\n}\n\n/* Return list with valid length */\nfunc (l *myList) toArray() []int {\n    // Elements enqueue\n    return l.arr[:l.arrSize]\n}\n
my_list.swift
/* List class */\nclass MyList {\n    private var arr: [Int] // Array (stores list elements)\n    private var _capacity: Int // List capacity\n    private var _size: Int // List length (current number of elements)\n    private let extendRatio: Int // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    init() {\n        _capacity = 10\n        _size = 0\n        extendRatio = 2\n        arr = Array(repeating: 0, count: _capacity)\n    }\n\n    /* Get list length (current number of elements) */\n    func size() -> Int {\n        _size\n    }\n\n    /* Get list capacity */\n    func capacity() -> Int {\n        _capacity\n    }\n\n    /* Update element */\n    func get(index: Int) -> Int {\n        // Throw error if index out of bounds, same below\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        return arr[index]\n    }\n\n    /* Add elements at the end */\n    func set(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        arr[index] = num\n    }\n\n    /* Direct traversal of list elements */\n    func add(num: Int) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if size() == capacity() {\n            extendCapacity()\n        }\n        arr[size()] = num\n        // Update the number of elements\n        _size += 1\n    }\n\n    /* Sort list */\n    func insert(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if size() == capacity() {\n            extendCapacity()\n        }\n        // Move all elements after index index forward by one position\n        for j in (index ..< size()).reversed() {\n            arr[j + 1] = arr[j]\n        }\n        arr[index] = num\n        // Update the number of elements\n        _size += 1\n    }\n\n    /* Remove element */\n    @discardableResult\n    func remove(index: Int) -> Int {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        let num = arr[index]\n        // Move all elements after index forward by one position\n        for j in index ..< (size() - 1) {\n            arr[j] = arr[j + 1]\n        }\n        // Update the number of elements\n        _size -= 1\n        // Return the removed element\n        return num\n    }\n\n    /* Driver Code */\n    func extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1))\n        // Add elements at the end\n        _capacity = arr.count\n    }\n\n    /* Convert list to array */\n    func toArray() -> [Int] {\n        Array(arr.prefix(size()))\n    }\n}\n
my_list.js
/* List class */\nclass MyList {\n    #arr = new Array(); // Array (stores list elements)\n    #capacity = 10; // List capacity\n    #size = 0; // List length (current number of elements)\n    #extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    constructor() {\n        this.#arr = new Array(this.#capacity);\n    }\n\n    /* Get list length (current number of elements) */\n    size() {\n        return this.#size;\n    }\n\n    /* Get list capacity */\n    capacity() {\n        return this.#capacity;\n    }\n\n    /* Update element */\n    get(index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        return this.#arr[index];\n    }\n\n    /* Add elements at the end */\n    set(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        this.#arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    add(num) {\n        // If length equals capacity, need to expand\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Add new element to end of list\n        this.#arr[this.#size] = num;\n        this.#size++;\n    }\n\n    /* Sort list */\n    insert(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Move all elements after index index forward by one position\n        for (let j = this.#size - 1; j >= index; j--) {\n            this.#arr[j + 1] = this.#arr[j];\n        }\n        // Update the number of elements\n        this.#arr[index] = num;\n        this.#size++;\n    }\n\n    /* Remove element */\n    remove(index) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        let num = this.#arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for (let j = index; j < this.#size - 1; j++) {\n            this.#arr[j] = this.#arr[j + 1];\n        }\n        // Update the number of elements\n        this.#size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        this.#arr = this.#arr.concat(\n            new Array(this.capacity() * (this.#extendRatio - 1))\n        );\n        // Add elements at the end\n        this.#capacity = this.#arr.length;\n    }\n\n    /* Convert list to array */\n    toArray() {\n        let size = this.size();\n        // Elements enqueue\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
my_list.ts
/* List class */\nclass MyList {\n    private arr: Array<number>; // Array (stores list elements)\n    private _capacity: number = 10; // List capacity\n    private _size: number = 0; // List length (current number of elements)\n    private extendRatio: number = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    constructor() {\n        this.arr = new Array(this._capacity);\n    }\n\n    /* Get list length (current number of elements) */\n    public size(): number {\n        return this._size;\n    }\n\n    /* Get list capacity */\n    public capacity(): number {\n        return this._capacity;\n    }\n\n    /* Update element */\n    public get(index: number): number {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        return this.arr[index];\n    }\n\n    /* Add elements at the end */\n    public set(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        this.arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public add(num: number): void {\n        // If length equals capacity, need to expand\n        if (this._size === this._capacity) this.extendCapacity();\n        // Add new element to end of list\n        this.arr[this._size] = num;\n        this._size++;\n    }\n\n    /* Sort list */\n    public insert(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (this._size === this._capacity) {\n            this.extendCapacity();\n        }\n        // Move all elements after index index forward by one position\n        for (let j = this._size - 1; j >= index; j--) {\n            this.arr[j + 1] = this.arr[j];\n        }\n        // Update the number of elements\n        this.arr[index] = num;\n        this._size++;\n    }\n\n    /* Remove element */\n    public remove(index: number): number {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        let num = this.arr[index];\n        // Move all elements after index forward by one position\n        for (let j = index; j < this._size - 1; j++) {\n            this.arr[j] = this.arr[j + 1];\n        }\n        // Update the number of elements\n        this._size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public extendCapacity(): void {\n        // Create new array of length size and copy original array to new array\n        this.arr = this.arr.concat(\n            new Array(this.capacity() * (this.extendRatio - 1))\n        );\n        // Add elements at the end\n        this._capacity = this.arr.length;\n    }\n\n    /* Convert list to array */\n    public toArray(): number[] {\n        let size = this.size();\n        // Elements enqueue\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
my_list.dart
/* List class */\nclass MyList {\n  late List<int> _arr; // Array (stores list elements)\n  int _capacity = 10; // List capacity\n  int _size = 0; // List length (current number of elements)\n  int _extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n  /* Constructor */\n  MyList() {\n    _arr = List.filled(_capacity, 0);\n  }\n\n  /* Get list length (current number of elements) */\n  int size() => _size;\n\n  /* Get list capacity */\n  int capacity() => _capacity;\n\n  /* Update element */\n  int get(int index) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    return _arr[index];\n  }\n\n  /* Add elements at the end */\n  void set(int index, int _num) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    _arr[index] = _num;\n  }\n\n  /* Direct traversal of list elements */\n  void add(int _num) {\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (_size == _capacity) extendCapacity();\n    _arr[_size] = _num;\n    // Update the number of elements\n    _size++;\n  }\n\n  /* Sort list */\n  void insert(int index, int _num) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (_size == _capacity) extendCapacity();\n    // Move all elements after index index forward by one position\n    for (var j = _size - 1; j >= index; j--) {\n      _arr[j + 1] = _arr[j];\n    }\n    _arr[index] = _num;\n    // Update the number of elements\n    _size++;\n  }\n\n  /* Remove element */\n  int remove(int index) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    int _num = _arr[index];\n    // Move all elements after index forward by one position\n    for (var j = index; j < _size - 1; j++) {\n      _arr[j] = _arr[j + 1];\n    }\n    // Update the number of elements\n    _size--;\n    // Return the removed element\n    return _num;\n  }\n\n  /* Driver Code */\n  void extendCapacity() {\n    // Create new array with length _extendRatio times original array\n    final _newNums = List.filled(_capacity * _extendRatio, 0);\n    // Copy original array to new array\n    List.copyRange(_newNums, 0, _arr);\n    // Update _arr reference\n    _arr = _newNums;\n    // Add elements at the end\n    _capacity = _arr.length;\n  }\n\n  /* Convert list to array */\n  List<int> toArray() {\n    List<int> arr = [];\n    for (var i = 0; i < _size; i++) {\n      arr.add(get(i));\n    }\n    return arr;\n  }\n}\n
my_list.rs
/* List class */\n#[allow(dead_code)]\nstruct MyList {\n    arr: Vec<i32>,       // Array (stores list elements)\n    capacity: usize,     // List capacity\n    size: usize,         // List length (current number of elements)\n    extend_ratio: usize, // Multiple by which the list capacity is extended each time\n}\n\n#[allow(unused, unused_comparisons)]\nimpl MyList {\n    /* Constructor */\n    pub fn new(capacity: usize) -> Self {\n        let mut vec = vec![0; capacity];\n        Self {\n            arr: vec,\n            capacity,\n            size: 0,\n            extend_ratio: 2,\n        }\n    }\n\n    /* Get list length (current number of elements) */\n    pub fn size(&self) -> usize {\n        return self.size;\n    }\n\n    /* Get list capacity */\n    pub fn capacity(&self) -> usize {\n        return self.capacity;\n    }\n\n    /* Update element */\n    pub fn get(&self, index: usize) -> i32 {\n        // If the index is out of bounds, throw an exception, as below\n        if index >= self.size {\n            panic!(\"Index out of bounds\")\n        };\n        return self.arr[index];\n    }\n\n    /* Add elements at the end */\n    pub fn set(&mut self, index: usize, num: i32) {\n        if index >= self.size {\n            panic!(\"Index out of bounds\")\n        };\n        self.arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    pub fn add(&mut self, num: i32) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        self.arr[self.size] = num;\n        // Update the number of elements\n        self.size += 1;\n    }\n\n    /* Sort list */\n    pub fn insert(&mut self, index: usize, num: i32) {\n        if index >= self.size() {\n            panic!(\"Index out of bounds\")\n        };\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        // Move all elements after index index forward by one position\n        for j in (index..self.size).rev() {\n            self.arr[j + 1] = self.arr[j];\n        }\n        self.arr[index] = num;\n        // Update the number of elements\n        self.size += 1;\n    }\n\n    /* Remove element */\n    pub fn remove(&mut self, index: usize) -> i32 {\n        if index >= self.size() {\n            panic!(\"Index out of bounds\")\n        };\n        let num = self.arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for j in index..self.size - 1 {\n            self.arr[j] = self.arr[j + 1];\n        }\n        // Update the number of elements\n        self.size -= 1;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    pub fn extend_capacity(&mut self) {\n        // Create new array with length extend_ratio times original, copy original array to new array\n        let new_capacity = self.capacity * self.extend_ratio;\n        self.arr.resize(new_capacity, 0);\n        // Add elements at the end\n        self.capacity = new_capacity;\n    }\n\n    /* Convert list to array */\n    pub fn to_array(&self) -> Vec<i32> {\n        // Elements enqueue\n        let mut arr = Vec::new();\n        for i in 0..self.size {\n            arr.push(self.get(i));\n        }\n        arr\n    }\n}\n
my_list.c
/* List class */\ntypedef struct {\n    int *arr;        // Array (stores list elements)\n    int capacity;    // List capacity\n    int size;        // List size\n    int extendRatio; // List expansion multiplier\n} MyList;\n\n/* Constructor */\nMyList *newMyList() {\n    MyList *nums = malloc(sizeof(MyList));\n    nums->capacity = 10;\n    nums->arr = malloc(sizeof(int) * nums->capacity);\n    nums->size = 0;\n    nums->extendRatio = 2;\n    return nums;\n}\n\n/* Destructor */\nvoid delMyList(MyList *nums) {\n    free(nums->arr);\n    free(nums);\n}\n\n/* Get list length */\nint size(MyList *nums) {\n    return nums->size;\n}\n\n/* Get list capacity */\nint capacity(MyList *nums) {\n    return nums->capacity;\n}\n\n/* Update element */\nint get(MyList *nums, int index) {\n    assert(index >= 0 && index < nums->size);\n    return nums->arr[index];\n}\n\n/* Add elements at the end */\nvoid set(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < nums->size);\n    nums->arr[index] = num;\n}\n\n/* Direct traversal of list elements */\nvoid add(MyList *nums, int num) {\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Expand capacity\n    }\n    nums->arr[size(nums)] = num;\n    nums->size++;\n}\n\n/* Sort list */\nvoid insert(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < size(nums));\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Expand capacity\n    }\n    for (int i = size(nums); i > index; --i) {\n        nums->arr[i] = nums->arr[i - 1];\n    }\n    nums->arr[index] = num;\n    nums->size++;\n}\n\n/* Remove element */\n// Note: stdio.h occupies the remove keyword\nint removeItem(MyList *nums, int index) {\n    assert(index >= 0 && index < size(nums));\n    int num = nums->arr[index];\n    for (int i = index; i < size(nums) - 1; i++) {\n        nums->arr[i] = nums->arr[i + 1];\n    }\n    nums->size--;\n    return num;\n}\n\n/* Driver Code */\nvoid extendCapacity(MyList *nums) {\n    // Allocate space first\n    int newCapacity = capacity(nums) * nums->extendRatio;\n    int *extend = (int *)malloc(sizeof(int) * newCapacity);\n    int *temp = nums->arr;\n\n    // Copy old data to new data\n    for (int i = 0; i < size(nums); i++)\n        extend[i] = nums->arr[i];\n\n    // Free old data\n    free(temp);\n\n    // Update new data\n    nums->arr = extend;\n    nums->capacity = newCapacity;\n}\n\n/* Convert list to Array for printing */\nint *toArray(MyList *nums) {\n    return nums->arr;\n}\n
my_list.kt
/* List class */\nclass MyList {\n    private var arr: IntArray = intArrayOf() // Array (stores list elements)\n    private var capacity: Int = 10 // List capacity\n    private var size: Int = 0 // List length (current number of elements)\n    private var extendRatio: Int = 2 // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    init {\n        arr = IntArray(capacity)\n    }\n\n    /* Get list length (current number of elements) */\n    fun size(): Int {\n        return size\n    }\n\n    /* Get list capacity */\n    fun capacity(): Int {\n        return capacity\n    }\n\n    /* Update element */\n    fun get(index: Int): Int {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        return arr[index]\n    }\n\n    /* Add elements at the end */\n    fun set(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        arr[index] = num\n    }\n\n    /* Direct traversal of list elements */\n    fun add(num: Int) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity()\n        arr[size] = num\n        // Update the number of elements\n        size++\n    }\n\n    /* Sort list */\n    fun insert(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity()\n        // Move all elements after index index forward by one position\n        for (j in size - 1 downTo index)\n            arr[j + 1] = arr[j]\n        arr[index] = num\n        // Update the number of elements\n        size++\n    }\n\n    /* Remove element */\n    fun remove(index: Int): Int {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        val num = arr[index]\n        // Move all elements after index forward by one position\n        for (j in index..<size - 1)\n            arr[j] = arr[j + 1]\n        // Update the number of elements\n        size--\n        // Return the removed element\n        return num\n    }\n\n    /* Driver Code */\n    fun extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = arr.copyOf(capacity() * extendRatio)\n        // Add elements at the end\n        capacity = arr.size\n    }\n\n    /* Convert list to array */\n    fun toArray(): IntArray {\n        val size = size()\n        // Elements enqueue\n        val arr = IntArray(size)\n        for (i in 0..<size) {\n            arr[i] = get(i)\n        }\n        return arr\n    }\n}\n
my_list.rb
### List class ###\nclass MyList\n  attr_reader :size       # Get list length (current number of elements)\n  attr_reader :capacity   # Get list capacity\n\n  ### Constructor ###\n  def initialize\n    @capacity = 10\n    @size = 0\n    @extend_ratio = 2\n    @arr = Array.new(capacity)\n  end\n\n  ### Access element ###\n  def get(index)\n    # If the index is out of bounds, throw an exception, as below\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    @arr[index]\n  end\n\n  ### Access element ###\n  def set(index, num)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    @arr[index] = num\n  end\n\n  ### Add element at end ###\n  def add(num)\n    # When the number of elements exceeds capacity, trigger the extension mechanism\n    extend_capacity if size == capacity\n    @arr[size] = num\n\n    # Update the number of elements\n    @size += 1\n  end\n\n  ### Insert element in middle ###\n  def insert(index, num)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n\n    # When the number of elements exceeds capacity, trigger the extension mechanism\n    extend_capacity if size == capacity\n\n    # Move all elements after index index forward by one position\n    for j in (size - 1).downto(index)\n      @arr[j + 1] = @arr[j]\n    end\n    @arr[index] = num\n\n    # Update the number of elements\n    @size += 1\n  end\n\n  ### Delete element ###\n  def remove(index)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    num = @arr[index]\n\n    # Move all elements after index forward by one position\n    for j in index...size\n      @arr[j] = @arr[j + 1]\n    end\n\n    # Update the number of elements\n    @size -= 1\n\n    # Return the removed element\n    num\n  end\n\n  ### Expand list capacity ###\n  def extend_capacity\n    # Create new array with length extend_ratio times original, copy original array to new array\n    arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1))\n    # Add elements at the end\n    @capacity = arr.length\n  end\n\n  ### Convert list to array ###\n  def to_array\n    sz = size\n    # Elements enqueue\n    arr = Array.new(sz)\n    for i in 0...sz\n      arr[i] = get(i)\n    end\n    arr\n  end\nend\n
","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/","level":1,"title":"4.4   Random-Access Memory and Cache *","text":"

In the first two sections of this chapter, we explored arrays and linked lists, two fundamental and important data structures that represent \"contiguous storage\" and \"distributed storage\" as two physical structures, respectively.

In fact, physical structure largely determines the efficiency with which programs utilize memory and cache, which in turn affects the overall performance of algorithmic programs.

","path":["Chapter 4. Array and Linked List","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#441-computer-storage-devices","level":2,"title":"4.4.1   Computer Storage Devices","text":"

Computers include three types of storage devices: hard disk, random-access memory (RAM), and cache memory. The following table shows their different roles and performance characteristics in a computer system.

Table 4-2   Computer Storage Devices

Hard Disk RAM Cache Purpose Long-term storage of data, including operating systems, programs, and files Temporary storage of currently running programs and data being processed Storage of frequently accessed data and instructions to reduce CPU's accesses to memory Volatility Data is not lost after power-off Data is lost after power-off Data is lost after power-off Capacity Large, on the order of terabytes (TB) Small, on the order of gigabytes (GB) Very small, on the order of megabytes (MB) Speed Slow, hundreds to thousands of MB/s Fast, tens of GB/s Very fast, tens to hundreds of GB/s Cost (USD/GB) Inexpensive, fractions of a dollar to a few dollars per GB Expensive, tens to hundreds of dollars per GB Very expensive, priced as part of the CPU package

We can imagine the computer storage system as a pyramid structure as shown in the diagram below. Storage devices closer to the top of the pyramid are faster, have smaller capacity, and are more expensive. This multi-layered design is not by accident, but rather the result of careful consideration by computer scientists and engineers.

  • Hard disk cannot be easily replaced by RAM. First, data in memory is lost after power-off, making it unsuitable for long-term data storage. Second, memory is tens of times more expensive than hard disk, which makes it difficult to popularize in the consumer market.
  • Cache cannot simultaneously achieve large capacity and high speed. As the capacity of L1, L2, and L3 caches increases, their physical size becomes larger, and the physical distance between them and the CPU core increases, resulting in longer data transmission time and higher element access latency. With current technology, the multi-layered cache structure represents the best balance point between capacity, speed, and cost.

Figure 4-9   Computer Storage System

Tip

The storage hierarchy of computers embodies a delicate balance among speed, capacity, and cost. In fact, such trade-offs are common across all industrial fields, requiring us to find the optimal balance point between different advantages and constraints.

In summary, hard disk is used for long-term storage of large amounts of data, RAM is used for temporary storage of data being processed during program execution, and cache is used for storage of frequently accessed data and instructions, to improve program execution efficiency. The three work together to ensure efficient operation of the computer system.

As shown in the diagram below, during program execution, data is read from the hard disk into RAM for CPU computation. Cache can be viewed as part of the CPU, it intelligently loads data from RAM, providing the CPU with high-speed data reading, thereby significantly improving program execution efficiency and reducing reliance on slower RAM.

Figure 4-10   Data Flow Among Hard Disk, RAM, and Cache

","path":["Chapter 4. Array and Linked List","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#442-memory-efficiency-of-data-structures","level":2,"title":"4.4.2   Memory Efficiency of Data Structures","text":"

In terms of memory space utilization, arrays and linked lists each have advantages and limitations.

On one hand, memory is limited, and the same memory cannot be shared by multiple programs, so we hope data structures can utilize space as efficiently as possible. Array elements are tightly packed and do not require additional space to store references (pointers) between linked list nodes, thus having higher space efficiency. However, arrays need to allocate sufficient contiguous memory space at once, which may lead to memory waste, and array expansion requires additional time and space costs. In comparison, linked lists perform dynamic memory allocation and deallocation on a \"node\" basis, providing greater flexibility.

On the other hand, during program execution, as memory is repeatedly allocated and freed, the degree of fragmentation of free memory becomes increasingly severe, leading to reduced memory utilization efficiency. Arrays, due to their contiguous storage approach, are relatively less prone to memory fragmentation. Conversely, linked list elements are distributed in storage, and frequent insertion and deletion operations are more likely to cause memory fragmentation.

","path":["Chapter 4. Array and Linked List","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#443-cache-efficiency-of-data-structures","level":2,"title":"4.4.3   Cache Efficiency of Data Structures","text":"

Although cache has much smaller space capacity than memory, it is much faster than memory and plays a crucial role in program execution speed. Since cache capacity is limited and can only store a small portion of frequently accessed data, when the CPU attempts to access data that is not in the cache, a cache miss occurs, and the CPU must load the required data from the slower memory.

Clearly, the fewer \"cache misses,\" the higher the efficiency of CPU data reads and writes, and the better the program performance. We call the proportion of data that the CPU successfully obtains from the cache the cache hit rate, a metric typically used to measure cache efficiency.

To achieve the highest efficiency possible, cache employs the following data loading mechanisms.

  • Cache lines: The cache does not store and load data on a byte-by-byte basis, but rather as cache lines. Compared to byte-by-byte transmission, cache line transmission is more efficient.
  • Prefetching mechanism: The processor attempts to predict data access patterns (e.g., sequential access, fixed-stride jumping access, etc.) and loads data into the cache according to specific patterns, thereby improving hit rate.
  • Spatial locality: If a piece of data is accessed, nearby data may also be accessed in the near future. Therefore, when the cache loads a particular piece of data, it also loads nearby data to improve hit rate.
  • Temporal locality: If a piece of data is accessed, it is likely to be accessed again in the near future. Cache leverages this principle by retaining recently accessed data to improve hit rate.

In fact, arrays and linked lists have different efficiencies in utilizing cache, manifested in the following aspects.

  • Space occupied: Linked list elements occupy more space than array elements, resulting in fewer effective data in the cache.
  • Cache lines: Linked list data are scattered throughout memory, while cache loads \"by lines,\" so the proportion of invalid data loaded is higher.
  • Prefetching mechanism: Arrays have more \"predictable\" data access patterns than linked lists, making it easier for the system to guess which data will be loaded next.
  • Spatial locality: Arrays are stored in centralized memory space, so data near loaded data is more likely to be accessed soon.

Overall, arrays have higher cache hit rates, thus they usually outperform linked lists in operation efficiency. This makes data structures implemented based on arrays more popular when solving algorithmic problems.

It is important to note that high cache efficiency does not mean arrays are superior to linked lists in all cases. In practical applications, which data structure to choose should be determined based on specific requirements. For example, both arrays and linked lists can implement the \"stack\" data structure (which will be discussed in detail in the next chapter), but they are suitable for different scenarios.

  • When solving algorithm problems, we tend to prefer stack implementations based on arrays, because they provide higher operation efficiency and the ability of random access, at the cost of needing to pre-allocate a certain amount of memory space for the array.
  • If the data volume is very large, the dynamic nature is high, and the expected size of the stack is difficult to estimate, then a stack implementation based on linked lists is more suitable. Linked lists can distribute large amounts of data across different parts of memory and avoid the additional overhead produced by array expansion.
","path":["Chapter 4. Array and Linked List","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/","level":1,"title":"4.5   Summary","text":"","path":["Chapter 4. Array and Linked List","4.5   Summary"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Arrays and linked lists are two fundamental data structures, representing two different ways data can be stored in computer memory: contiguous memory storage and scattered memory storage. The characteristics of the two complement each other.
  • Arrays support random access and use less memory; however, inserting and deleting elements is inefficient, and the length is immutable after initialization.
  • Linked lists achieve efficient insertion and deletion of nodes by modifying references (pointers), and can flexibly adjust length; however, node access is inefficient and memory consumption is higher. Common linked list types include singly linked lists, circular linked lists, and doubly linked lists.
  • A list is an ordered collection of elements that supports insertion, deletion, search, and modification, typically implemented based on dynamic arrays. It retains the advantages of arrays while allowing flexible adjustment of length.
  • The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space.
  • During program execution, data is primarily stored in memory. Arrays provide higher memory space efficiency, while linked lists offer greater flexibility in memory usage.
  • Caches provide fast data access to the CPU through mechanisms such as cache lines, prefetching, and spatial and temporal locality, significantly improving program execution efficiency.
  • Because arrays have higher cache hit rates, they are generally more efficient than linked lists. When choosing a data structure, appropriate selection should be made based on specific requirements and scenarios.
","path":["Chapter 4. Array and Linked List","4.5   Summary"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Does storing an array on the stack versus on the heap affect time efficiency and space efficiency?

Arrays stored on the stack and on the heap are both stored in contiguous memory space, so data operation efficiency is basically the same. However, the stack and heap have their own characteristics, leading to the following differences.

  1. Allocation and deallocation efficiency: The stack is a relatively small piece of memory, with allocation automatically handled by the compiler; the heap is relatively larger and can be dynamically allocated in code, more prone to fragmentation. Therefore, allocation and deallocation operations on the heap are usually slower than on the stack.
  2. Size limitations: Stack memory is relatively small, and the heap size is generally limited by available memory. Therefore, the heap is more suitable for storing large arrays.
  3. Flexibility: The size of an array on the stack must be determined at compile time, while the size of an array on the heap can be determined dynamically at runtime.

Q: Why do arrays require elements of the same type, while linked lists do not emphasize this requirement?

Linked lists are composed of nodes, with nodes connected through references (pointers), and each node can store different types of data, such as int, double, string, object, etc.

In contrast, array elements must be of the same type, so that the corresponding element position can be obtained by calculating the offset. For example, if an array contains both int and long types, with individual elements occupying 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset, because the array contains two different \"element lengths\".

# Element Memory Address = Array Memory Address (first Element Memory address) + Element Length * Element Index\n

Q: After deleting node P, do we need to set P.next to None?

It is not necessary to modify P.next. From the perspective of the linked list, traversing from the head node to the tail node will no longer encounter P. This means that node P has been removed from the linked list, and it doesn't matter where node P points to at this time—it won't affect the linked list.

From a data structures and algorithms perspective (problem-solving), not disconnecting the pointer doesn't matter as long as the program logic is correct. From the perspective of standard libraries, disconnecting is safer and the logic is clearer. If not disconnected, assuming the deleted node is not properly reclaimed, it may affect the memory reclamation of its successor nodes.

Q: In a linked list, the time complexity of insertion and deletion operations is \\(O(1)\\). However, both insertion and deletion require \\(O(n)\\) time to find the element; why isn't the time complexity \\(O(n)\\)?

If the element is first found and then deleted, the time complexity is indeed \\(O(n)\\). However, the advantage of \\(O(1)\\) insertion and deletion in linked lists can be demonstrated in other applications. For example, a deque is well-suited for linked list implementation, where we maintain pointer variables always pointing to the head and tail nodes, with each insertion and deletion operation being \\(O(1)\\).

Q: In the diagram \"Linked List Definition and Storage Methods\", does the light blue pointer node occupy a single memory address, or does it share equally with the node value?

This diagram is a qualitative representation; a quantitative representation requires analysis based on the specific situation.

  • Different types of node values occupy different amounts of space, such as int, long, double, and instance objects, etc.
  • The amount of memory space occupied by pointer variables depends on the operating system and compilation environment used, usually 8 bytes or 4 bytes.

Q: Is appending an element at the end of a list always \\(O(1)\\)?

If appending an element exceeds the list length, the list must first be expanded before adding. The system allocates a new block of memory and moves all elements from the original list to it, in which case the time complexity becomes \\(O(n)\\).

Q: \"The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space\"—does this space waste refer to the memory occupied by additional variables such as capacity, length, and expansion factor?

This space waste mainly has two aspects: on one hand, lists typically set an initial length, which we may not need to fully utilize; on the other hand, to prevent frequent expansion, expansion generally multiplies by a coefficient, such as \\(\\times 1.5\\). As a result, there will be many empty positions that we typically cannot completely fill.

Q: In Python, after initializing n = [1, 2, 3], the addresses of these 3 elements are contiguous, but initializing m = [2, 1, 3] reveals that each element's id is not continuous; rather, they are the same as those in n. Since the addresses of these elements are not contiguous, is m still an array?

If we replace list elements with linked list nodes n = [n1, n2, n3, n4, n5], usually these 5 node objects are also scattered throughout memory. However, given a list index, we can still obtain the node memory address in \\(O(1)\\) time, thereby accessing the corresponding node. This is because the array stores references to nodes, not the nodes themselves.

Unlike many languages, numbers in Python are wrapped as objects, and lists store not the numbers themselves, but references to the numbers. Therefore, we find that the same numbers in two arrays have the same id, and the memory addresses of these numbers need not be contiguous.

Q: C++ STL has std::list which has already implemented a doubly linked list, but it seems that some algorithm books don't use it directly. Is there a limitation?

On one hand, we often prefer to use arrays for implementing algorithms and only use linked lists when necessary, mainly for two reasons.

  • Space overhead: Since each element requires two additional pointers (one for the previous element and one for the next element), std::list typically consumes more space than std::vector.
  • Cache unfriendliness: Since data is not stored contiguously, std::list has lower cache utilization. In general, std::vector has better performance.

On the other hand, cases where linked lists are necessary mainly involve binary trees and graphs. Stacks and queues usually use the stack and queue provided by the programming language, rather than linked lists.

Q: Does the operation res = [[0]] * n create a 2D list where each [0] is independent?

No, they are not independent. In this 2D list, all the [0] are actually references to the same object. If we modify one element, we will find that all corresponding elements change accordingly.

If we want each [0] in the 2D list to be independent, we can use res = [[0] for _ in range(n)] to achieve this. The principle of this approach is to initialize \\(n\\) independent [0] list objects.

Q: Does the operation res = [0] * n create a list where each integer 0 is independent?

In this list, all integer 0s are references to the same object. This is because Python uses a caching mechanism for small integers (typically -5 to 256) to maximize object reuse and improve performance.

Although they point to the same object, we can still independently modify each element in the list. This is because Python integers are \"immutable objects\". When we modify an element, we are actually switching to a reference of another object, rather than changing the original object itself.

However, when list elements are \"mutable objects\" (such as lists, dictionaries, or class instances), modifying an element directly changes the object itself, and all elements referencing that object will have the same change.

","path":["Chapter 4. Array and Linked List","4.5   Summary"],"tags":[]},{"location":"chapter_backtracking/","level":1,"title":"Chapter 13.   Backtracking","text":"

Abstract

We are like explorers in a maze, and may encounter difficulties on the path forward.

The power of backtracking allows us to start over, keep trying, and eventually find the exit leading to light.

","path":["Chapter 13. Backtracking","Chapter 13.   Backtracking"],"tags":[]},{"location":"chapter_backtracking/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 13.1   Backtracking Algorithm
  • 13.2   Permutations Problem
  • 13.3   Subset-Sum Problem
  • 13.4   N-Queens Problem
  • 13.5   Summary
","path":["Chapter 13. Backtracking","Chapter 13.   Backtracking"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   Backtracking Algorithm","text":"

The backtracking algorithm is a method for solving problems through exhaustive search. Its core idea is to start from an initial state and exhaustively search all possible solutions. When a correct solution is found, it is recorded. This process continues until a solution is found or all possible choices have been tried without finding a solution.

The backtracking algorithm typically employs \"depth-first search\" to traverse the solution space. In the \"Binary Tree\" chapter, we mentioned that preorder, inorder, and postorder traversals all belong to depth-first search. Next, we will construct a backtracking problem using preorder traversal to progressively understand how the backtracking algorithm works.

Example 1

Given a binary tree, search and record all nodes with value \\(7\\), and return a list of these nodes.

For this problem, we perform a preorder traversal of the tree and check whether the current node's value is \\(7\\). If it is, we add the node to the result list res. The relevant implementation is shown in the following figure and code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 1\"\"\"\n    if root is None:\n        return\n    if root.val == 7:\n        # Record solution\n        res.append(root)\n    pre_order(root.left)\n    pre_order(root.right)\n
preorder_traversal_i_compact.cpp
/* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(root);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
preorder_traversal_i_compact.java
/* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Record solution\n        res.add(root);\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n}\n
preorder_traversal_i_compact.cs
/* Preorder traversal: Example 1 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Record solution\n        res.Add(root);\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n
preorder_traversal_i_compact.go
/* Preorder traversal: Example 1 */\nfunc preOrderI(root *TreeNode, res *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    if (root.Val).(int) == 7 {\n        // Record solution\n        *res = append(*res, root)\n    }\n    preOrderI(root.Left, res)\n    preOrderI(root.Right, res)\n}\n
preorder_traversal_i_compact.swift
/* Preorder traversal: Example 1 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    if root.val == 7 {\n        // Record solution\n        res.append(root)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n
preorder_traversal_i_compact.js
/* Preorder traversal: Example 1 */\nfunction preOrder(root, res) {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Record solution\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.ts
/* Preorder traversal: Example 1 */\nfunction preOrder(root: TreeNode | null, res: TreeNode[]): void {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Record solution\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.dart
/* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode? root, List<TreeNode> res) {\n  if (root == null) {\n    return;\n  }\n  if (root.val == 7) {\n    // Record solution\n    res.add(root);\n  }\n  preOrder(root.left, res);\n  preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.rs
/* Preorder traversal: Example 1 */\nfn pre_order(res: &mut Vec<Rc<RefCell<TreeNode>>>, root: Option<&Rc<RefCell<TreeNode>>>) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        if node.borrow().val == 7 {\n            // Record solution\n            res.push(node.clone());\n        }\n        pre_order(res, node.borrow().left.as_ref());\n        pre_order(res, node.borrow().right.as_ref());\n    }\n}\n
preorder_traversal_i_compact.c
/* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    if (root->val == 7) {\n        // Record solution\n        res[resSize++] = root;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
preorder_traversal_i_compact.kt
/* Preorder traversal: Example 1 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(root)\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n}\n
preorder_traversal_i_compact.rb
### Pre-order traversal: example 1 ###\ndef pre_order(root)\n  return unless root\n\n  # Record solution\n  $res << root if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\nend\n

Figure 13-1   Search for nodes in preorder traversal

","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1311-attempt-and-backtrack","level":2,"title":"13.1.1   Attempt and Backtrack","text":"

The reason it is called a backtracking algorithm is that it employs \"attempt\" and \"backtrack\" strategies when searching the solution space. When the algorithm encounters a state where it cannot continue forward or cannot find a solution that satisfies the constraints, it will undo the previous choice, return to a previous state, and try other possible choices.

For Example 1, visiting each node represents an \"attempt\", while skipping over a leaf node or a function return from the parent node represents a \"backtrack\".

It is worth noting that backtracking is not limited to function returns alone. To illustrate this, let's extend Example 1 slightly.

Example 2

In a binary tree, search all nodes with value \\(7\\), and return the paths from the root node to these nodes.

Based on the code from Example 1, we need to use a list path to record the visited node path. When we reach a node with value \\(7\\), we copy path and add it to the result list res. After traversal is complete, res contains all the solutions. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 2\"\"\"\n    if root is None:\n        return\n    # Attempt\n    path.append(root)\n    if root.val == 7:\n        # Record solution\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Backtrack\n    path.pop()\n
preorder_traversal_ii_compact.cpp
/* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    // Attempt\n    path.push_back(root);\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    path.pop_back();\n}\n
preorder_traversal_ii_compact.java
/* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    // Attempt\n    path.add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Backtrack\n    path.remove(path.size() - 1);\n}\n
preorder_traversal_ii_compact.cs
/* Preorder traversal: Example 2 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    // Attempt\n    path.Add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Backtrack\n    path.RemoveAt(path.Count - 1);\n}\n
preorder_traversal_ii_compact.go
/* Preorder traversal: Example 2 */\nfunc preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    // Attempt\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Record solution\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderII(root.Left, res, path)\n    preOrderII(root.Right, res, path)\n    // Backtrack\n    *path = (*path)[:len(*path)-1]\n}\n
preorder_traversal_ii_compact.swift
/* Preorder traversal: Example 2 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Attempt\n    path.append(root)\n    if root.val == 7 {\n        // Record solution\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Backtrack\n    path.removeLast()\n}\n
preorder_traversal_ii_compact.js
/* Preorder traversal: Example 2 */\nfunction preOrder(root, path, res) {\n    if (root === null) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
preorder_traversal_ii_compact.ts
/* Preorder traversal: Example 2 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    if (root === null) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
preorder_traversal_ii_compact.dart
/* Preorder traversal: Example 2 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null) {\n    return;\n  }\n\n  // Attempt\n  path.add(root);\n  if (root.val == 7) {\n    // Record solution\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Backtrack\n  path.removeLast();\n}\n
preorder_traversal_ii_compact.rs
/* Preorder traversal: Example 2 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        // Attempt\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Record solution\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // Backtrack\n        path.pop();\n    }\n}\n
preorder_traversal_ii_compact.c
/* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    // Attempt\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Record solution\n        for (int i = 0; i < pathSize; ++i) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    pathSize--;\n}\n
preorder_traversal_ii_compact.kt
/* Preorder traversal: Example 2 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    // Attempt\n    path!!.add(root)\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Backtrack\n    path!!.removeAt(path!!.size - 1)\n}\n
preorder_traversal_ii_compact.rb
### Pre-order traversal: example 2 ###\ndef pre_order(root)\n  return unless root\n\n  # Attempt\n  $path << root\n\n  # Record solution\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Backtrack\n  $path.pop\nend\n

In each \"attempt\", we record the path by adding the current node to path; before \"backtracking\", we need to remove the node from path, to restore the state before this attempt.

Observing the process shown in the following figure, we can understand attempt and backtrack as \"advance\" and \"undo\", two operations that are the reverse of each other.

<1><2><3><4><5><6><7><8><9><10><11>

Figure 13-2   Attempt and backtrack

","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1312-pruning","level":2,"title":"13.1.2   Pruning","text":"

Complex backtracking problems usually contain one or more constraints. Constraints can typically be used for \"pruning\".

Example 3

In a binary tree, search all nodes with value \\(7\\) and return the paths from the root node to these nodes, but require that the paths do not contain nodes with value \\(3\\).

To satisfy the above constraints, we need to add pruning operations: during the search process, if we encounter a node with value \\(3\\), we return early and do not continue searching. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 3\"\"\"\n    # Pruning\n    if root is None or root.val == 3:\n        return\n    # Attempt\n    path.append(root)\n    if root.val == 7:\n        # Record solution\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Backtrack\n    path.pop()\n
preorder_traversal_iii_compact.cpp
/* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode *root) {\n    // Pruning\n    if (root == nullptr || root->val == 3) {\n        return;\n    }\n    // Attempt\n    path.push_back(root);\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    path.pop_back();\n}\n
preorder_traversal_iii_compact.java
/* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode root) {\n    // Pruning\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Attempt\n    path.add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Backtrack\n    path.remove(path.size() - 1);\n}\n
preorder_traversal_iii_compact.cs
/* Preorder traversal: Example 3 */\nvoid PreOrder(TreeNode? root) {\n    // Pruning\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Attempt\n    path.Add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Backtrack\n    path.RemoveAt(path.Count - 1);\n}\n
preorder_traversal_iii_compact.go
/* Preorder traversal: Example 3 */\nfunc preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    // Pruning\n    if root == nil || root.Val == 3 {\n        return\n    }\n    // Attempt\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Record solution\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderIII(root.Left, res, path)\n    preOrderIII(root.Right, res, path)\n    // Backtrack\n    *path = (*path)[:len(*path)-1]\n}\n
preorder_traversal_iii_compact.swift
/* Preorder traversal: Example 3 */\nfunc preOrder(root: TreeNode?) {\n    // Pruning\n    guard let root = root, root.val != 3 else {\n        return\n    }\n    // Attempt\n    path.append(root)\n    if root.val == 7 {\n        // Record solution\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Backtrack\n    path.removeLast()\n}\n
preorder_traversal_iii_compact.js
/* Preorder traversal: Example 3 */\nfunction preOrder(root, path, res) {\n    // Pruning\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
preorder_traversal_iii_compact.ts
/* Preorder traversal: Example 3 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Pruning\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
preorder_traversal_iii_compact.dart
/* Preorder traversal: Example 3 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null || root.val == 3) {\n    return;\n  }\n\n  // Attempt\n  path.add(root);\n  if (root.val == 7) {\n    // Record solution\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Backtrack\n  path.removeLast();\n}\n
preorder_traversal_iii_compact.rs
/* Preorder traversal: Example 3 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    // Pruning\n    if root.is_none() || root.as_ref().unwrap().borrow().val == 3 {\n        return;\n    }\n    if let Some(node) = root {\n        // Attempt\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Record solution\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // Backtrack\n        path.pop();\n    }\n}\n
preorder_traversal_iii_compact.c
/* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode *root) {\n    // Pruning\n    if (root == NULL || root->val == 3) {\n        return;\n    }\n    // Attempt\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Record solution\n        for (int i = 0; i < pathSize; i++) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    pathSize--;\n}\n
preorder_traversal_iii_compact.kt
/* Preorder traversal: Example 3 */\nfun preOrder(root: TreeNode?) {\n    // Pruning\n    if (root == null || root._val == 3) {\n        return\n    }\n    // Attempt\n    path!!.add(root)\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Backtrack\n    path!!.removeAt(path!!.size - 1)\n}\n
preorder_traversal_iii_compact.rb
### Pre-order traversal: example 3 ###\ndef pre_order(root)\n  # Pruning\n  return if !root || root.val == 3\n\n  # Attempt\n  $path.append(root)\n\n  # Record solution\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Backtrack\n  $path.pop\nend\n

\"Pruning\" is a vivid term. As shown in the following figure, during the search process, we \"prune\" search branches that do not satisfy the constraints, avoiding many meaningless attempts and thus improving search efficiency.

Figure 13-3   Pruning according to constraints

","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1313-framework-code","level":2,"title":"13.1.3   Framework Code","text":"

Next, we attempt to extract the main framework of backtracking's \"attempt, backtrack, and pruning\", to improve code generality.

In the following framework code, state represents the current state of the problem, and choices represents the choices available in the current state:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def backtrack(state: State, choices: list[choice], res: list[state]):\n    \"\"\"Backtracking algorithm framework\"\"\"\n    # Check if it is a solution\n    if is_solution(state):\n        # Record the solution\n        record_solution(state, res)\n        # Stop searching\n        return\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: check if the choice is valid\n        if is_valid(state, choice):\n            # Attempt: make a choice and update the state\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice)\n
/* Backtracking algorithm framework */\nvoid backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (Choice choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nvoid backtrack(State state, List<Choice> choices, List<State> res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (Choice choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nvoid Backtrack(State state, List<Choice> choices, List<State> res) {\n    // Check if it is a solution\n    if (IsSolution(state)) {\n        // Record the solution\n        RecordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    foreach (Choice choice in choices) {\n        // Pruning: check if the choice is valid\n        if (IsValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            MakeChoice(state, choice);\n            Backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            UndoChoice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nfunc backtrack(state *State, choices []Choice, res *[]State) {\n    // Check if it is a solution\n    if isSolution(state) {\n        // Record the solution\n        recordSolution(state, res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for _, choice := range choices {\n        // Pruning: check if the choice is valid\n        if isValid(state, choice) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nfunc backtrack(state: inout State, choices: [Choice], res: inout [State]) {\n    // Check if it is a solution\n    if isSolution(state: state) {\n        // Record the solution\n        recordSolution(state: state, res: &res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if isValid(state: state, choice: choice) {\n            // Attempt: make a choice and update the state\n            makeChoice(state: &state, choice: choice)\n            backtrack(state: &state, choices: choices, res: &res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nfunction backtrack(state, choices, res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (let choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nfunction backtrack(state: State, choices: Choice[], res: State[]): void {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (let choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nvoid backtrack(State state, List<Choice>, List<State> res) {\n  // Check if it is a solution\n  if (isSolution(state)) {\n    // Record the solution\n    recordSolution(state, res);\n    // Stop searching\n    return;\n  }\n  // Traverse all choices\n  for (Choice choice in choices) {\n    // Pruning: check if the choice is valid\n    if (isValid(state, choice)) {\n      // Attempt: make a choice and update the state\n      makeChoice(state, choice);\n      backtrack(state, choices, res);\n      // Backtrack: undo the choice and restore to the previous state\n      undoChoice(state, choice);\n    }\n  }\n}\n
/* Backtracking algorithm framework */\nfn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {\n    // Check if it is a solution\n    if is_solution(state) {\n        // Record the solution\n        record_solution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if is_valid(state, choice) {\n            // Attempt: make a choice and update the state\n            make_choice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nvoid backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res, numRes);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < numChoices; i++) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, &choices[i])) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, &choices[i]);\n            backtrack(state, choices, numChoices, res, numRes);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, &choices[i]);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nfun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
### Backtracking algorithm framework ###\ndef backtrack(state, choices, res)\n    # Check if it is a solution\n    if is_solution?(state)\n        # Record the solution\n        record_solution(state, res)\n        return\n    end\n\n    # Traverse all choices\n    for choice in choices\n        # Pruning: check if the choice is valid\n        if is_valid?(state, choice)\n            # Attempt: make a choice and update the state\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice)\n        end\n    end\nend\n

Next, we solve Example 3 based on the framework code. The state state is the node traversal path, the choices choices are the left and right child nodes of the current node, and the result res is a list of paths:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_template.py
def is_solution(state: list[TreeNode]) -> bool:\n    \"\"\"Check if the current state is a solution\"\"\"\n    return state and state[-1].val == 7\n\ndef record_solution(state: list[TreeNode], res: list[list[TreeNode]]):\n    \"\"\"Record solution\"\"\"\n    res.append(list(state))\n\ndef is_valid(state: list[TreeNode], choice: TreeNode) -> bool:\n    \"\"\"Check if the choice is valid under the current state\"\"\"\n    return choice is not None and choice.val != 3\n\ndef make_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Update state\"\"\"\n    state.append(choice)\n\ndef undo_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Restore state\"\"\"\n    state.pop()\n\ndef backtrack(\n    state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]]\n):\n    \"\"\"Backtracking algorithm: Example 3\"\"\"\n    # Check if it is a solution\n    if is_solution(state):\n        # Record solution\n        record_solution(state, res)\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: check if the choice is valid\n        if is_valid(state, choice):\n            # Attempt: make choice, update state\n            make_choice(state, choice)\n            # Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res)\n            # Backtrack: undo choice, restore to previous state\n            undo_choice(state, choice)\n
preorder_traversal_iii_template.cpp
/* Check if the current state is a solution */\nbool isSolution(vector<TreeNode *> &state) {\n    return !state.empty() && state.back()->val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(vector<TreeNode *> &state, vector<vector<TreeNode *>> &res) {\n    res.push_back(state);\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(vector<TreeNode *> &state, TreeNode *choice) {\n    return choice != nullptr && choice->val != 3;\n}\n\n/* Update state */\nvoid makeChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.push_back(choice);\n}\n\n/* Restore state */\nvoid undoChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.pop_back();\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(vector<TreeNode *> &state, vector<TreeNode *> &choices, vector<vector<TreeNode *>> &res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (TreeNode *choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            vector<TreeNode *> nextChoices{choice->left, choice->right};\n            backtrack(state, nextChoices, res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.java
/* Check if the current state is a solution */\nboolean isSolution(List<TreeNode> state) {\n    return !state.isEmpty() && state.get(state.size() - 1).val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.add(new ArrayList<>(state));\n}\n\n/* Check if the choice is valid under the current state */\nboolean isValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid makeChoice(List<TreeNode> state, TreeNode choice) {\n    state.add(choice);\n}\n\n/* Restore state */\nvoid undoChoice(List<TreeNode> state, TreeNode choice) {\n    state.remove(state.size() - 1);\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (TreeNode choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, Arrays.asList(choice.left, choice.right), res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.cs
/* Check if the current state is a solution */\nbool IsSolution(List<TreeNode> state) {\n    return state.Count != 0 && state[^1].val == 7;\n}\n\n/* Record solution */\nvoid RecordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.Add(new List<TreeNode>(state));\n}\n\n/* Check if the choice is valid under the current state */\nbool IsValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid MakeChoice(List<TreeNode> state, TreeNode choice) {\n    state.Add(choice);\n}\n\n/* Restore state */\nvoid UndoChoice(List<TreeNode> state, TreeNode choice) {\n    state.RemoveAt(state.Count - 1);\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid Backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Check if it is a solution\n    if (IsSolution(state)) {\n        // Record solution\n        RecordSolution(state, res);\n    }\n    // Traverse all choices\n    foreach (TreeNode choice in choices) {\n        // Pruning: check if the choice is valid\n        if (IsValid(state, choice)) {\n            // Attempt: make choice, update state\n            MakeChoice(state, choice);\n            // Proceed to the next round of selection\n            Backtrack(state, [choice.left!, choice.right!], res);\n            // Backtrack: undo choice, restore to previous state\n            UndoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.go
/* Check if the current state is a solution */\nfunc isSolution(state *[]*TreeNode) bool {\n    return len(*state) != 0 && (*state)[len(*state)-1].Val == 7\n}\n\n/* Record solution */\nfunc recordSolution(state *[]*TreeNode, res *[][]*TreeNode) {\n    *res = append(*res, append([]*TreeNode{}, *state...))\n}\n\n/* Check if the choice is valid under the current state */\nfunc isValid(state *[]*TreeNode, choice *TreeNode) bool {\n    return choice != nil && choice.Val != 3\n}\n\n/* Update state */\nfunc makeChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = append(*state, choice)\n}\n\n/* Restore state */\nfunc undoChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = (*state)[:len(*state)-1]\n}\n\n/* Backtracking algorithm: Example 3 */\nfunc backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) {\n    // Check if it is a solution\n    if isSolution(state) {\n        // Record solution\n        recordSolution(state, res)\n    }\n    // Traverse all choices\n    for _, choice := range *choices {\n        // Pruning: check if the choice is valid\n        if isValid(state, choice) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice)\n            // Proceed to the next round of selection\n            temp := make([]*TreeNode, 0)\n            temp = append(temp, choice.Left, choice.Right)\n            backtrackIII(state, &temp, res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.swift
/* Check if the current state is a solution */\nfunc isSolution(state: [TreeNode]) -> Bool {\n    !state.isEmpty && state.last!.val == 7\n}\n\n/* Record solution */\nfunc recordSolution(state: [TreeNode], res: inout [[TreeNode]]) {\n    res.append(state)\n}\n\n/* Check if the choice is valid under the current state */\nfunc isValid(state: [TreeNode], choice: TreeNode?) -> Bool {\n    choice != nil && choice!.val != 3\n}\n\n/* Update state */\nfunc makeChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.append(choice)\n}\n\n/* Restore state */\nfunc undoChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.removeLast()\n}\n\n/* Backtracking algorithm: Example 3 */\nfunc backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) {\n    // Check if it is a solution\n    if isSolution(state: state) {\n        recordSolution(state: state, res: &res)\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if isValid(state: state, choice: choice) {\n            // Attempt: make choice, update state\n            makeChoice(state: &state, choice: choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.js
/* Check if the current state is a solution */\nfunction isSolution(state) {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Record solution */\nfunction recordSolution(state, res) {\n    res.push([...state]);\n}\n\n/* Check if the choice is valid under the current state */\nfunction isValid(state, choice) {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Update state */\nfunction makeChoice(state, choice) {\n    state.push(choice);\n}\n\n/* Restore state */\nfunction undoChoice(state) {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 3 */\nfunction backtrack(state, choices, res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state);\n        }\n    }\n}\n
preorder_traversal_iii_template.ts
/* Check if the current state is a solution */\nfunction isSolution(state: TreeNode[]): boolean {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Record solution */\nfunction recordSolution(state: TreeNode[], res: TreeNode[][]): void {\n    res.push([...state]);\n}\n\n/* Check if the choice is valid under the current state */\nfunction isValid(state: TreeNode[], choice: TreeNode): boolean {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Update state */\nfunction makeChoice(state: TreeNode[], choice: TreeNode): void {\n    state.push(choice);\n}\n\n/* Restore state */\nfunction undoChoice(state: TreeNode[]): void {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 3 */\nfunction backtrack(\n    state: TreeNode[],\n    choices: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state);\n        }\n    }\n}\n
preorder_traversal_iii_template.dart
/* Check if the current state is a solution */\nbool isSolution(List<TreeNode> state) {\n  return state.isNotEmpty && state.last.val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n  res.add(List.from(state));\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(List<TreeNode> state, TreeNode? choice) {\n  return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid makeChoice(List<TreeNode> state, TreeNode? choice) {\n  state.add(choice!);\n}\n\n/* Restore state */\nvoid undoChoice(List<TreeNode> state, TreeNode? choice) {\n  state.removeLast();\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(\n  List<TreeNode> state,\n  List<TreeNode?> choices,\n  List<List<TreeNode>> res,\n) {\n  // Check if it is a solution\n  if (isSolution(state)) {\n    // Record solution\n    recordSolution(state, res);\n  }\n  // Traverse all choices\n  for (TreeNode? choice in choices) {\n    // Pruning: check if the choice is valid\n    if (isValid(state, choice)) {\n      // Attempt: make choice, update state\n      makeChoice(state, choice);\n      // Proceed to the next round of selection\n      backtrack(state, [choice!.left, choice.right], res);\n      // Backtrack: undo choice, restore to previous state\n      undoChoice(state, choice);\n    }\n  }\n}\n
preorder_traversal_iii_template.rs
/* Check if the current state is a solution */\nfn is_solution(state: &mut Vec<Rc<RefCell<TreeNode>>>) -> bool {\n    return !state.is_empty() && state.last().unwrap().borrow().val == 7;\n}\n\n/* Record solution */\nfn record_solution(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    res.push(state.clone());\n}\n\n/* Check if the choice is valid under the current state */\nfn is_valid(_: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Option<&Rc<RefCell<TreeNode>>>) -> bool {\n    return choice.is_some() && choice.unwrap().borrow().val != 3;\n}\n\n/* Update state */\nfn make_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Rc<RefCell<TreeNode>>) {\n    state.push(choice);\n}\n\n/* Restore state */\nfn undo_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, _: Rc<RefCell<TreeNode>>) {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 3 */\nfn backtrack(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    choices: &Vec<Option<&Rc<RefCell<TreeNode>>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    // Check if it is a solution\n    if is_solution(state) {\n        // Record solution\n        record_solution(state, res);\n    }\n    // Traverse all choices\n    for &choice in choices.iter() {\n        // Pruning: check if the choice is valid\n        if is_valid(state, choice) {\n            // Attempt: make choice, update state\n            make_choice(state, choice.unwrap().clone());\n            // Proceed to the next round of selection\n            backtrack(\n                state,\n                &vec![\n                    choice.unwrap().borrow().left.as_ref(),\n                    choice.unwrap().borrow().right.as_ref(),\n                ],\n                res,\n            );\n            // Backtrack: undo choice, restore to previous state\n            undo_choice(state, choice.unwrap().clone());\n        }\n    }\n}\n
preorder_traversal_iii_template.c
/* Check if the current state is a solution */\nbool isSolution(void) {\n    return pathSize > 0 && path[pathSize - 1]->val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(void) {\n    for (int i = 0; i < pathSize; i++) {\n        res[resSize][i] = path[i];\n    }\n    resSize++;\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(TreeNode *choice) {\n    return choice != NULL && choice->val != 3;\n}\n\n/* Update state */\nvoid makeChoice(TreeNode *choice) {\n    path[pathSize++] = choice;\n}\n\n/* Restore state */\nvoid undoChoice(void) {\n    pathSize--;\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(TreeNode *choices[2]) {\n    // Check if it is a solution\n    if (isSolution()) {\n        // Record solution\n        recordSolution();\n    }\n    // Traverse all choices\n    for (int i = 0; i < 2; i++) {\n        TreeNode *choice = choices[i];\n        // Pruning: check if the choice is valid\n        if (isValid(choice)) {\n            // Attempt: make choice, update state\n            makeChoice(choice);\n            // Proceed to the next round of selection\n            TreeNode *nextChoices[2] = {choice->left, choice->right};\n            backtrack(nextChoices);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice();\n        }\n    }\n}\n
preorder_traversal_iii_template.kt
/* Check if the current state is a solution */\nfun isSolution(state: MutableList<TreeNode?>): Boolean {\n    return state.isNotEmpty() && state[state.size - 1]?._val == 7\n}\n\n/* Record solution */\nfun recordSolution(state: MutableList<TreeNode?>?, res: MutableList<MutableList<TreeNode?>?>) {\n    res.add(state!!.toMutableList())\n}\n\n/* Check if the choice is valid under the current state */\nfun isValid(state: MutableList<TreeNode?>?, choice: TreeNode?): Boolean {\n    return choice != null && choice._val != 3\n}\n\n/* Update state */\nfun makeChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.add(choice)\n}\n\n/* Restore state */\nfun undoChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.removeLast()\n}\n\n/* Backtracking algorithm: Example 3 */\nfun backtrack(\n    state: MutableList<TreeNode?>,\n    choices: MutableList<TreeNode?>,\n    res: MutableList<MutableList<TreeNode?>?>\n) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res)\n    }\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice)\n            // Proceed to the next round of selection\n            backtrack(state, mutableListOf(choice!!.left, choice.right), res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.rb
### Check if current state is solution ###\ndef is_solution?(state)\n  !state.empty? && state.last.val == 7\nend\n\n### Record solution ###\ndef record_solution(state, res)\n  res << state.dup\nend\n\n### Check if choice is valid in current state ###\ndef is_valid?(state, choice)\n  choice && choice.val != 3\nend\n\n### Update state ###\ndef make_choice(state, choice)\n  state << choice\nend\n\n### Restore state ###\ndef undo_choice(state, choice)\n  state.pop\nend\n\n### Backtracking: example 3 ###\ndef backtrack(state, choices, res)\n  # Check if it is a solution\n  record_solution(state, res) if is_solution?(state)\n\n  # Traverse all choices\n  for choice in choices\n    # Pruning: check if the choice is valid\n    if is_valid?(state, choice)\n      # Attempt: make choice, update state\n      make_choice(state, choice)\n      # Proceed to the next round of selection\n      backtrack(state, [choice.left, choice.right], res)\n      # Backtrack: undo choice, restore to previous state\n      undo_choice(state, choice)\n    end\n  end\nend\n

As per the problem statement, we should continue searching after finding a node with value \\(7\\). Therefore, we need to remove the return statement after recording the solution. The following figure compares the search process with and without the return statement.

Figure 13-4   Comparison of search process with and without return statement

Compared to code based on preorder traversal, code based on the backtracking algorithm framework appears more verbose, but has better generality. In fact, many backtracking problems can be solved within this framework. We only need to define state and choices for the specific problem and implement each method in the framework.

","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314-common-terminology","level":2,"title":"13.1.4   Common Terminology","text":"

To analyze algorithmic problems more clearly, we summarize the meanings of common terminology used in backtracking algorithms and provide corresponding examples from Example 3, as shown in the following table.

Table 13-1   Common Backtracking Algorithm Terminology

Term Definition Example 3 Solution (solution) A solution is an answer that satisfies the specific conditions of a problem; there may be one or more solutions All paths from root to nodes with value \\(7\\) that satisfy the constraint Constraint (constraint) A constraint is a condition in the problem that limits the feasibility of solutions, typically used for pruning Paths do not contain nodes with value \\(3\\) State (state) State represents the situation of a problem at a certain moment, including the choices already made The currently visited node path, i.e., the path list of nodes Attempt (attempt) An attempt is the process of exploring the solution space according to available choices, including making choices, updating state, and checking if it is a solution Recursively visit left (right) child nodes, add nodes to path, check if node value is \\(7\\) Backtrack (backtracking) Backtracking refers to undoing previous choices and returning to a previous state when encountering a state that does not satisfy constraints Stop searching when passing over leaf nodes, ending node visits, or encountering nodes with value \\(3\\); function returns Pruning (pruning) Pruning is a method of avoiding meaningless search paths according to problem characteristics and constraints, which can improve search efficiency When encountering a node with value \\(3\\), do not continue searching

Tip

The concepts of problem, solution, state, etc. are universal and are involved in divide-and-conquer, backtracking, dynamic programming, greedy and other algorithms.

","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1315-advantages-and-limitations","level":2,"title":"13.1.5   Advantages and Limitations","text":"

The backtracking algorithm is essentially a depth-first search algorithm that tries all possible solutions until it finds one that satisfies the conditions. The advantage of this approach is that it can find all possible solutions, and with reasonable pruning operations, it achieves high efficiency.

However, when dealing with large-scale or complex problems, the running efficiency of the backtracking algorithm may be unacceptable.

  • Time: The backtracking algorithm usually needs to traverse all possibilities in the solution space, and the time complexity can reach exponential or factorial order.
  • Space: During recursive calls, the current state needs to be saved (such as paths, auxiliary variables used for pruning, etc.), and when the depth is large, the space requirement can become very large.

Nevertheless, the backtracking algorithm is still the best solution for certain search problems and constraint satisfaction problems. For these problems, since we cannot predict which choices will generate valid solutions, we must traverse all possible choices. In this case, the key is how to optimize efficiency. There are two common efficiency optimization methods.

  • Pruning: Avoid searching paths that are guaranteed not to produce solutions, thereby saving time and space.
  • Heuristic search: Introduce certain strategies or estimation values during the search process to prioritize searching paths that are most likely to produce valid solutions.
","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316-typical-backtracking-examples","level":2,"title":"13.1.6   Typical Backtracking Examples","text":"

The backtracking algorithm can be used to solve many search problems, constraint satisfaction problems, and combinatorial optimization problems.

Search problems: The goal of these problems is to find solutions that satisfy specific conditions.

  • Permutation problem: Given a set, find all possible permutations and combinations.
  • Subset sum problem: Given a set and a target sum, find all subsets in the set whose elements sum to the target.
  • Tower of Hanoi: Given three pegs and a series of disks of different sizes, move all disks from one peg to another, moving only one disk at a time, and never placing a larger disk on a smaller disk.

Constraint satisfaction problems: The goal of these problems is to find solutions that satisfy all constraints.

  • N-Queens: Place \\(n\\) queens on an \\(n \\times n\\) chessboard such that they do not attack each other.
  • Sudoku: Fill numbers \\(1\\) to \\(9\\) in a \\(9 \\times 9\\) grid such that each row, column, and \\(3 \\times 3\\) subgrid contains no repeated digits.
  • Graph coloring: Given an undirected graph, color each vertex with the minimum number of colors such that adjacent vertices have different colors.

Combinatorial optimization problems: The goal of these problems is to find an optimal solution that satisfies certain conditions in a combinatorial space.

  • 0-1 Knapsack: Given a set of items and a knapsack, each item has a value and weight. Under the knapsack capacity constraint, select items to maximize total value.
  • Traveling Salesman Problem: Starting from a point in a graph, visit all other points exactly once and return to the starting point, finding the shortest path.
  • Maximum Clique: Given an undirected graph, find the largest complete subgraph, i.e., a subgraph where any two vertices are connected by an edge.

Note that for many combinatorial optimization problems, backtracking is not the optimal solution.

  • The 0-1 Knapsack problem is usually solved using dynamic programming to achieve higher time efficiency.
  • The Traveling Salesman Problem is a famous NP-Hard problem; common solutions include genetic algorithms and ant colony algorithms.
  • The Maximum Clique problem is a classical problem in graph theory and can be solved using heuristic algorithms such as greedy algorithms.
","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/","level":1,"title":"13.4   N-Queens Problem","text":"

Question

According to the rules of chess, a queen can attack pieces that share the same row, column, or diagonal line. Given \\(n\\) queens and an \\(n \\times n\\) chessboard, find a placement scheme such that no two queens can attack each other.

As shown in Figure 13-15, when \\(n = 4\\), there are two solutions that can be found. From the perspective of the backtracking algorithm, an \\(n \\times n\\) chessboard has \\(n^2\\) squares, which provide all the choices choices. During the process of placing queens one by one, the chessboard state changes continuously, and the chessboard at each moment represents the state state.

Figure 13-15   Solution to the 4-queens problem

Figure 13-16 illustrates the three constraints of this problem: multiple queens cannot be in the same row, the same column, or on the same diagonal. It is worth noting that diagonals are divided into two types: the main diagonal \\ and the anti-diagonal /.

Figure 13-16   Constraints of the n-queens problem

","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1-row-by-row-placement-strategy","level":3,"title":"1.   Row-By-Row Placement Strategy","text":"

Since both the number of queens and the number of rows on the chessboard are \\(n\\), we can easily derive a conclusion: each row of the chessboard allows and only allows exactly one queen to be placed.

This means we can adopt a row-by-row placement strategy: starting from the first row, place one queen in each row until the last row is completed.

Figure 13-17 shows the row-by-row placement process for the 4-queens problem. Due to space limitations, the figure only expands one search branch of the first row, and all schemes that do not satisfy the column constraint and diagonal constraints are pruned.

Figure 13-17   Row-by-row placement strategy

Essentially, the row-by-row placement strategy serves a pruning function, as it avoids all search branches where multiple queens appear in the same row.

","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2-column-and-diagonal-pruning","level":3,"title":"2.   Column and Diagonal Pruning","text":"

To satisfy the column constraint, we can use a boolean array cols of length \\(n\\) to record whether each column has a queen. Before each placement decision, we use cols to prune columns that already have queens, and dynamically update the state of cols during backtracking.

Tip

Please note that the origin of the matrix is located in the upper-left corner, where the row index increases from top to bottom, and the column index increases from left to right.

So how do we handle diagonal constraints? Consider a square on the chessboard with row and column indices \\((row, col)\\). If we select a specific main diagonal in the matrix, we find that all squares on that diagonal have the same difference between their row and column indices, meaning that \\(row - col\\) is a constant value for all squares on the main diagonal.

In other words, if two squares satisfy \\(row_1 - col_1 = row_2 - col_2\\), they must be on the same main diagonal. Using this pattern, we can use the array diags1 shown in Figure 13-18 to record whether there is a queen on each main diagonal.

Similarly, for all squares on an anti-diagonal, the sum \\(row + col\\) is a constant value. We can likewise use the array diags2 to handle anti-diagonal constraints.

Figure 13-18   Handling column and diagonal constraints

","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

Please note that in an \\(n\\)-dimensional square matrix, the range of \\(row - col\\) is \\([-n + 1, n - 1]\\), and the range of \\(row + col\\) is \\([0, 2n - 2]\\). Therefore, the number of both main diagonals and anti-diagonals is \\(2n - 1\\), meaning the length of both arrays diags1 and diags2 is \\(2n - 1\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby n_queens.py
def backtrack(\n    row: int,\n    n: int,\n    state: list[list[str]],\n    res: list[list[list[str]]],\n    cols: list[bool],\n    diags1: list[bool],\n    diags2: list[bool],\n):\n    \"\"\"Backtracking algorithm: N queens\"\"\"\n    # When all rows are placed, record the solution\n    if row == n:\n        res.append([list(row) for row in state])\n        return\n    # Traverse all columns\n    for col in range(n):\n        # Calculate the main diagonal and anti-diagonal corresponding to this cell\n        diag1 = row - col + n - 1\n        diag2 = row + col\n        # Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if not cols[col] and not diags1[diag1] and not diags2[diag2]:\n            # Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            cols[col] = diags1[diag1] = diags2[diag2] = True\n            # Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            # Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            cols[col] = diags1[diag1] = diags2[diag2] = False\n\ndef n_queens(n: int) -> list[list[list[str]]]:\n    \"\"\"Solve N queens\"\"\"\n    # Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    state = [[\"#\" for _ in range(n)] for _ in range(n)]\n    cols = [False] * n  # Record whether there is a queen in the column\n    diags1 = [False] * (2 * n - 1)  # Record whether there is a queen on the main diagonal\n    diags2 = [False] * (2 * n - 1)  # Record whether there is a queen on the anti-diagonal\n    res = []\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n
n_queens.cpp
/* Backtracking algorithm: N queens */\nvoid backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,\n               vector<bool> &diags1, vector<bool> &diags2) {\n    // When all rows are placed, record the solution\n    if (row == n) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nvector<vector<vector<string>>> nQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    vector<vector<string>> state(n, vector<string>(n, \"#\"));\n    vector<bool> cols(n, false);           // Record whether there is a queen in the column\n    vector<bool> diags1(2 * n - 1, false); // Record whether there is a queen on the main diagonal\n    vector<bool> diags2(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal\n    vector<vector<vector<string>>> res;\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.java
/* Backtracking algorithm: N queens */\nvoid backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,\n        boolean[] cols, boolean[] diags1, boolean[] diags2) {\n    // When all rows are placed, record the solution\n    if (row == n) {\n        List<List<String>> copyState = new ArrayList<>();\n        for (List<String> sRow : state) {\n            copyState.add(new ArrayList<>(sRow));\n        }\n        res.add(copyState);\n        return;\n    }\n    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state.get(row).set(col, \"Q\");\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state.get(row).set(col, \"#\");\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nList<List<List<String>>> nQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    List<List<String>> state = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<String> row = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            row.add(\"#\");\n        }\n        state.add(row);\n    }\n    boolean[] cols = new boolean[n]; // Record whether there is a queen in the column\n    boolean[] diags1 = new boolean[2 * n - 1]; // Record whether there is a queen on the main diagonal\n    boolean[] diags2 = new boolean[2 * n - 1]; // Record whether there is a queen on the anti-diagonal\n    List<List<List<String>>> res = new ArrayList<>();\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.cs
/* Backtracking algorithm: N queens */\nvoid Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,\n        bool[] cols, bool[] diags1, bool[] diags2) {\n    // When all rows are placed, record the solution\n    if (row == n) {\n        List<List<string>> copyState = [];\n        foreach (List<string> sRow in state) {\n            copyState.Add(new List<string>(sRow));\n        }\n        res.Add(copyState);\n        return;\n    }\n    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            Backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nList<List<List<string>>> NQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    List<List<string>> state = [];\n    for (int i = 0; i < n; i++) {\n        List<string> row = [];\n        for (int j = 0; j < n; j++) {\n            row.Add(\"#\");\n        }\n        state.Add(row);\n    }\n    bool[] cols = new bool[n]; // Record whether there is a queen in the column\n    bool[] diags1 = new bool[2 * n - 1]; // Record whether there is a queen on the main diagonal\n    bool[] diags2 = new bool[2 * n - 1]; // Record whether there is a queen on the anti-diagonal\n    List<List<List<string>>> res = [];\n\n    Backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.go
/* Backtracking algorithm: N queens */\nfunc backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {\n    // When all rows are placed, record the solution\n    if row == n {\n        newState := make([][]string, len(*state))\n        for i, _ := range newState {\n            newState[i] = make([]string, len((*state)[0]))\n            copy(newState[i], (*state)[i])\n\n        }\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all columns\n    for col := 0; col < n; col++ {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        diag1 := row - col + n - 1\n        diag2 := row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {\n            // Attempt: place the queen in this cell\n            (*state)[row][col] = \"Q\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true\n            // Place the next row\n            backtrack(row+1, n, state, res, cols, diags1, diags2)\n            // Backtrack: restore this cell to an empty cell\n            (*state)[row][col] = \"#\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false\n        }\n    }\n}\n\n/* Solve N queens */\nfunc nQueens(n int) [][][]string {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    state := make([][]string, n)\n    for i := 0; i < n; i++ {\n        row := make([]string, n)\n        for i := 0; i < n; i++ {\n            row[i] = \"#\"\n        }\n        state[i] = row\n    }\n    // Record whether there is a queen in the column\n    cols := make([]bool, n)\n    diags1 := make([]bool, 2*n-1)\n    diags2 := make([]bool, 2*n-1)\n    res := make([][][]string, 0)\n    backtrack(0, n, &state, &res, &cols, &diags1, &diags2)\n    return res\n}\n
n_queens.swift
/* Backtracking algorithm: N queens */\nfunc backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {\n    // When all rows are placed, record the solution\n    if row == n {\n        res.append(state)\n        return\n    }\n    // Traverse all columns\n    for col in 0 ..< n {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        let diag1 = row - col + n - 1\n        let diag2 = row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            cols[col] = true\n            diags1[diag1] = true\n            diags2[diag2] = true\n            // Place the next row\n            backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            cols[col] = false\n            diags1[diag1] = false\n            diags2[diag2] = false\n        }\n    }\n}\n\n/* Solve N queens */\nfunc nQueens(n: Int) -> [[[String]]] {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    var state = Array(repeating: Array(repeating: \"#\", count: n), count: n)\n    var cols = Array(repeating: false, count: n) // Record whether there is a queen in the column\n    var diags1 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the main diagonal\n    var diags2 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the anti-diagonal\n    var res: [[[String]]] = []\n\n    backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n\n    return res\n}\n
n_queens.js
/* Backtracking algorithm: N queens */\nfunction backtrack(row, n, state, res, cols, diags1, diags2) {\n    // When all rows are placed, record the solution\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Traverse all columns\n    for (let col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nfunction nQueens(n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Record whether there is a queen in the column\n    const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal\n    const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal\n    const res = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
n_queens.ts
/* Backtracking algorithm: N queens */\nfunction backtrack(\n    row: number,\n    n: number,\n    state: string[][],\n    res: string[][][],\n    cols: boolean[],\n    diags1: boolean[],\n    diags2: boolean[]\n): void {\n    // When all rows are placed, record the solution\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Traverse all columns\n    for (let col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nfunction nQueens(n: number): string[][][] {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Record whether there is a queen in the column\n    const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal\n    const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal\n    const res: string[][][] = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
n_queens.dart
/* Backtracking algorithm: N queens */\nvoid backtrack(\n  int row,\n  int n,\n  List<List<String>> state,\n  List<List<List<String>>> res,\n  List<bool> cols,\n  List<bool> diags1,\n  List<bool> diags2,\n) {\n  // When all rows are placed, record the solution\n  if (row == n) {\n    List<List<String>> copyState = [];\n    for (List<String> sRow in state) {\n      copyState.add(List.from(sRow));\n    }\n    res.add(copyState);\n    return;\n  }\n  // Traverse all columns\n  for (int col = 0; col < n; col++) {\n    // Calculate the main diagonal and anti-diagonal corresponding to this cell\n    int diag1 = row - col + n - 1;\n    int diag2 = row + col;\n    // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n    if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n      // Attempt: place the queen in this cell\n      state[row][col] = \"Q\";\n      cols[col] = true;\n      diags1[diag1] = true;\n      diags2[diag2] = true;\n      // Place the next row\n      backtrack(row + 1, n, state, res, cols, diags1, diags2);\n      // Backtrack: restore this cell to an empty cell\n      state[row][col] = \"#\";\n      cols[col] = false;\n      diags1[diag1] = false;\n      diags2[diag2] = false;\n    }\n  }\n}\n\n/* Solve N queens */\nList<List<List<String>>> nQueens(int n) {\n  // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n  List<List<String>> state = List.generate(n, (index) => List.filled(n, \"#\"));\n  List<bool> cols = List.filled(n, false); // Record whether there is a queen in the column\n  List<bool> diags1 = List.filled(2 * n - 1, false); // Record whether there is a queen on the main diagonal\n  List<bool> diags2 = List.filled(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal\n  List<List<List<String>>> res = [];\n\n  backtrack(0, n, state, res, cols, diags1, diags2);\n\n  return res;\n}\n
n_queens.rs
/* Backtracking algorithm: N queens */\nfn backtrack(\n    row: usize,\n    n: usize,\n    state: &mut Vec<Vec<String>>,\n    res: &mut Vec<Vec<Vec<String>>>,\n    cols: &mut [bool],\n    diags1: &mut [bool],\n    diags2: &mut [bool],\n) {\n    // When all rows are placed, record the solution\n    if row == n {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all columns\n    for col in 0..n {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        let diag1 = row + n - 1 - col;\n        let diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);\n        }\n    }\n}\n\n/* Solve N queens */\nfn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    let mut state: Vec<Vec<String>> = vec![vec![\"#\".to_string(); n]; n];\n    let mut cols = vec![false; n]; // Record whether there is a queen in the column\n    let mut diags1 = vec![false; 2 * n - 1]; // Record whether there is a queen on the main diagonal\n    let mut diags2 = vec![false; 2 * n - 1]; // Record whether there is a queen on the anti-diagonal\n    let mut res: Vec<Vec<Vec<String>>> = Vec::new();\n\n    backtrack(\n        0,\n        n,\n        &mut state,\n        &mut res,\n        &mut cols,\n        &mut diags1,\n        &mut diags2,\n    );\n\n    res\n}\n
n_queens.c
/* Backtracking algorithm: N queens */\nvoid backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE],\n               bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) {\n    // When all rows are placed, record the solution\n    if (row == n) {\n        res[*resSize] = (char **)malloc(sizeof(char *) * n);\n        for (int i = 0; i < n; ++i) {\n            res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1));\n            strcpy(res[*resSize][i], state[i]);\n        }\n        (*resSize)++;\n        return;\n    }\n    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nchar ***nQueens(int n, int *returnSize) {\n    char state[MAX_SIZE][MAX_SIZE];\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    for (int i = 0; i < n; ++i) {\n        for (int j = 0; j < n; ++j) {\n            state[i][j] = '#';\n        }\n        state[i][n] = '\\0';\n    }\n    bool cols[MAX_SIZE] = {false};           // Record whether there is a queen in the column\n    bool diags1[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the main diagonal\n    bool diags2[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the anti-diagonal\n\n    char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE);\n    *returnSize = 0;\n    backtrack(0, n, state, res, returnSize, cols, diags1, diags2);\n    return res;\n}\n
n_queens.kt
/* Backtracking algorithm: N queens */\nfun backtrack(\n    row: Int,\n    n: Int,\n    state: MutableList<MutableList<String>>,\n    res: MutableList<MutableList<MutableList<String>>?>,\n    cols: BooleanArray,\n    diags1: BooleanArray,\n    diags2: BooleanArray\n) {\n    // When all rows are placed, record the solution\n    if (row == n) {\n        val copyState = mutableListOf<MutableList<String>>()\n        for (sRow in state) {\n            copyState.add(sRow.toMutableList())\n        }\n        res.add(copyState)\n        return\n    }\n    // Traverse all columns\n    for (col in 0..<n) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        val diag1 = row - col + n - 1\n        val diag2 = row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            diags2[diag2] = true\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            diags2[diag2] = false\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n        }\n    }\n}\n\n/* Solve N queens */\nfun nQueens(n: Int): MutableList<MutableList<MutableList<String>>?> {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    val state = mutableListOf<MutableList<String>>()\n    for (i in 0..<n) {\n        val row = mutableListOf<String>()\n        for (j in 0..<n) {\n            row.add(\"#\")\n        }\n        state.add(row)\n    }\n    val cols = BooleanArray(n) // Record whether there is a queen in the column\n    val diags1 = BooleanArray(2 * n - 1) // Record whether there is a queen on the main diagonal\n    val diags2 = BooleanArray(2 * n - 1) // Record whether there is a queen on the anti-diagonal\n    val res = mutableListOf<MutableList<MutableList<String>>?>()\n\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n}\n
n_queens.rb
### Backtracking: n queens ###\ndef backtrack(row, n, state, res, cols, diags1, diags2)\n  # When all rows are placed, record the solution\n  if row == n\n    res << state.map { |row| row.dup }\n    return\n  end\n\n  # Traverse all columns\n  for col in 0...n\n    # Calculate the main diagonal and anti-diagonal corresponding to this cell\n    diag1 = row - col + n - 1\n    diag2 = row + col\n    # Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n    if !cols[col] && !diags1[diag1] && !diags2[diag2]\n      # Attempt: place the queen in this cell\n      state[row][col] = \"Q\"\n      cols[col] = diags1[diag1] = diags2[diag2] = true\n      # Place the next row\n      backtrack(row + 1, n, state, res, cols, diags1, diags2)\n      # Backtrack: restore this cell to an empty cell\n      state[row][col] = \"#\"\n      cols[col] = diags1[diag1] = diags2[diag2] = false\n    end\n  end\nend\n\n### Solve n queens ###\ndef n_queens(n)\n  # Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n  state = Array.new(n) { Array.new(n, \"#\") }\n  cols = Array.new(n, false) # Record whether there is a queen in the column\n  diags1 = Array.new(2 * n - 1, false) # Record whether there is a queen on the main diagonal\n  diags2 = Array.new(2 * n - 1, false) # Record whether there is a queen on the anti-diagonal\n  res = []\n  backtrack(0, n, state, res, cols, diags1, diags2)\n\n  res\nend\n

Placing \\(n\\) queens row by row, considering the column constraint, from the first row to the last row there are \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\) choices, using \\(O(n!)\\) time. When recording a solution, it is necessary to copy the matrix state and add it to res, and the copy operation uses \\(O(n^2)\\) time. Therefore, the overall time complexity is \\(O(n! \\cdot n^2)\\). In practice, pruning based on diagonal constraints can also significantly reduce the search space, so the search efficiency is often better than the time complexity mentioned above.

The array state uses \\(O(n^2)\\) space, and the arrays cols, diags1, and diags2 each use \\(O(n)\\) space. The maximum recursion depth is \\(n\\), using \\(O(n)\\) stack frame space. Therefore, the space complexity is \\(O(n^2)\\).

","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/","level":1,"title":"13.2   Permutations Problem","text":"

The permutations problem is a classic application of backtracking algorithms. It is defined as finding all possible arrangements of elements in a given collection (such as an array or string).

Table 13-2 shows several example datasets, including input arrays and their corresponding permutations.

Table 13-2   Permutations Examples

Input Array All Permutations \\([1]\\) \\([1]\\) \\([1, 2]\\) \\([1, 2], [2, 1]\\) \\([1, 2, 3]\\) \\([1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]\\)","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1321-case-with-distinct-elements","level":2,"title":"13.2.1   Case with Distinct Elements","text":"

Question

Given an integer array with no duplicate elements, return all possible permutations.

From the perspective of backtracking algorithms, we can imagine the process of generating permutations as the result of a series of choices. Suppose the input array is \\([1, 2, 3]\\). If we first choose \\(1\\), then choose \\(3\\), and finally choose \\(2\\), we obtain the permutation \\([1, 3, 2]\\). Backtracking means undoing a choice and then trying other choices.

From the perspective of backtracking code, the candidate set choices consists of all elements in the input array, and the state state is the elements that have been chosen so far. Note that each element can only be chosen once, therefore all elements in state should be unique.

As shown in Figure 13-5, we can unfold the search process into a recursion tree, where each node in the tree represents the current state state. Starting from the root node, after three rounds of choices, we reach a leaf node, and each leaf node corresponds to a permutation.

Figure 13-5   Recursion tree of permutations

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1-pruning-duplicate-choices","level":3,"title":"1.   Pruning Duplicate Choices","text":"

To ensure that each element is chosen only once, we consider introducing a boolean array selected, where selected[i] indicates whether choices[i] has been chosen. We implement the following pruning operation based on it.

  • After making a choice choice[i], we set selected[i] to \\(\\text{True}\\), indicating that it has been chosen.
  • When traversing the candidate list choices, we skip all nodes that have been chosen, which is pruning.

As shown in Figure 13-6, suppose we choose \\(1\\) in the first round, \\(3\\) in the second round, and \\(2\\) in the third round. Then we need to prune the branch of element \\(1\\) in the second round and prune the branches of elements \\(1\\) and \\(3\\) in the third round.

Figure 13-6   Pruning example of permutations

Observing the above figure, we find that this pruning operation reduces the search space size from \\(O(n^n)\\) to \\(O(n!)\\).

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

After understanding the above information, we can fill in the blanks in the template code. To shorten the overall code, we do not implement each function in the template separately, but instead unfold them in the backtrack() function:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_i.py
def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Permutations I\"\"\"\n    # When the state length equals the number of elements, record the solution\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Traverse all choices\n    for i, choice in enumerate(choices):\n        # Pruning: do not allow repeated selection of elements\n        if not selected[i]:\n            # Attempt: make choice, update state\n            selected[i] = True\n            state.append(choice)\n            # Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            # Backtrack: undo choice, restore to previous state\n            selected[i] = False\n            state.pop()\n\ndef permutations_i(nums: list[int]) -> list[list[int]]:\n    \"\"\"Permutations I\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
permutations_i.cpp
/* Backtracking algorithm: Permutations I */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push_back(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Permutations I */\nvector<vector<int>> permutationsI(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
permutations_i.java
/* Backtracking algorithm: Permutations I */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.add(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Permutations I */\nList<List<Integer>> permutationsI(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
permutations_i.cs
/* Backtracking algorithm: Permutations I */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.Add(choice);\n            // Proceed to the next round of selection\n            Backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Permutations I */\nList<List<int>> PermutationsI(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
permutations_i.go
/* Backtracking algorithm: Permutations I */\nfunc backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // When the state length equals the number of elements, record the solution\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Traverse all choices\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Pruning: do not allow repeated selection of elements\n        if !(*selected)[i] {\n            // Attempt: make choice, update state\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Proceed to the next round of selection\n            backtrackI(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Permutations I */\nfunc permutationsI(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackI(&state, &nums, &selected, &res)\n    return res\n}\n
permutations_i.swift
/* Backtracking algorithm: Permutations I */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // When the state length equals the number of elements, record the solution\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    for (i, choice) in choices.enumerated() {\n        // Pruning: do not allow repeated selection of elements\n        if !selected[i] {\n            // Attempt: make choice, update state\n            selected[i] = true\n            state.append(choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Permutations I */\nfunc permutationsI(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
permutations_i.js
/* Backtracking algorithm: Permutations I */\nfunction backtrack(state, choices, selected, res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations I */\nfunction permutationsI(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_i.ts
/* Backtracking algorithm: Permutations I */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations I */\nfunction permutationsI(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_i.dart
/* Backtracking algorithm: Permutations I */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // When the state length equals the number of elements, record the solution\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Pruning: do not allow repeated selection of elements\n    if (!selected[i]) {\n      // Attempt: make choice, update state\n      selected[i] = true;\n      state.add(choice);\n      // Proceed to the next round of selection\n      backtrack(state, choices, selected, res);\n      // Backtrack: undo choice, restore to previous state\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Permutations I */\nList<List<int>> permutationsI(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
permutations_i.rs
/* Backtracking algorithm: Permutations I */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // When the state length equals the number of elements, record the solution\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Traverse all choices\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if !selected[i] {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state.clone(), choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Permutations I */\nfn permutations_i(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new(); // State (subset)\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
permutations_i.c
/* Backtracking algorithm: Permutations I */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // When the state length equals the number of elements, record the solution\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Proceed to the next round of selection\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n        }\n    }\n}\n\n/* Permutations I */\nint **permutationsI(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
permutations_i.kt
/* Backtracking algorithm: Permutations I */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true\n            state.add(choice)\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Permutations I */\nfun permutationsI(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
permutations_i.rb
### Backtracking: permutations I ###\ndef backtrack(state, choices, selected, res)\n  # When the state length equals the number of elements, record the solution\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  choices.each_with_index do |choice, i|\n    # Pruning: do not allow repeated selection of elements\n    unless selected[i]\n      # Attempt: make choice, update state\n      selected[i] = true\n      state << choice\n      # Proceed to the next round of selection\n      backtrack(state, choices, selected, res)\n      # Backtrack: undo choice, restore to previous state\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Permutations I ###\ndef permutations_i(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1322-case-with-duplicate-elements","level":2,"title":"13.2.2   Case with Duplicate Elements","text":"

Question

Given an integer array that may contain duplicate elements, return all unique permutations.

Suppose the input array is \\([1, 1, 2]\\). To distinguish the two duplicate elements \\(1\\), we denote the second \\(1\\) as \\(\\hat{1}\\).

As shown in Figure 13-7, the method described above generates permutations where half are duplicates.

Figure 13-7   Duplicate permutations

So how do we remove duplicate permutations? The most direct approach is to use a hash set to directly deduplicate the permutation results. However, this is not elegant because the search branches that generate duplicate permutations are unnecessary and should be identified and pruned early, which can further improve algorithm efficiency.

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1-pruning-duplicate-elements","level":3,"title":"1.   Pruning Duplicate Elements","text":"

Observe Figure 13-8. In the first round, choosing \\(1\\) or choosing \\(\\hat{1}\\) is equivalent. All permutations generated under these two choices are duplicates. Therefore, we should prune \\(\\hat{1}\\).

Similarly, after choosing \\(2\\) in the first round, the \\(1\\) and \\(\\hat{1}\\) in the second round also produce duplicate branches, so the second round's \\(\\hat{1}\\) should also be pruned.

Essentially, our goal is to ensure that multiple equal elements are chosen only once in a certain round of choices.

Figure 13-8   Pruning duplicate permutations

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2-code-implementation_1","level":3,"title":"2.   Code Implementation","text":"

Building on the code from the previous problem, we consider opening a hash set duplicated in each round of choices to record which elements have been tried in this round, and prune duplicate elements:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_ii.py
def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Permutations II\"\"\"\n    # When the state length equals the number of elements, record the solution\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Traverse all choices\n    duplicated = set[int]()\n    for i, choice in enumerate(choices):\n        # Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if not selected[i] and choice not in duplicated:\n            # Attempt: make choice, update state\n            duplicated.add(choice)  # Record the selected element value\n            selected[i] = True\n            state.append(choice)\n            # Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            # Backtrack: undo choice, restore to previous state\n            selected[i] = False\n            state.pop()\n\ndef permutations_ii(nums: list[int]) -> list[list[int]]:\n    \"\"\"Permutations II\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
permutations_ii.cpp
/* Backtracking algorithm: Permutations II */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    unordered_set<int> duplicated;\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && duplicated.find(choice) == duplicated.end()) {\n            // Attempt: make choice, update state\n            duplicated.emplace(choice); // Record the selected element value\n            selected[i] = true;\n            state.push_back(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Permutations II */\nvector<vector<int>> permutationsII(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
permutations_ii.java
/* Backtracking algorithm: Permutations II */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Traverse all choices\n    Set<Integer> duplicated = new HashSet<Integer>();\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.add(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Permutations II */\nList<List<Integer>> permutationsII(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
permutations_ii.cs
/* Backtracking algorithm: Permutations II */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    HashSet<int> duplicated = [];\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.Contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.Add(choice); // Record the selected element value\n            selected[i] = true;\n            state.Add(choice);\n            // Proceed to the next round of selection\n            Backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Permutations II */\nList<List<int>> PermutationsII(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
permutations_ii.go
/* Backtracking algorithm: Permutations II */\nfunc backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // When the state length equals the number of elements, record the solution\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Traverse all choices\n    duplicated := make(map[int]struct{}, 0)\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if _, ok := duplicated[choice]; !ok && !(*selected)[i] {\n            // Attempt: make choice, update state\n            // Record the selected element value\n            duplicated[choice] = struct{}{}\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Proceed to the next round of selection\n            backtrackII(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Permutations II */\nfunc permutationsII(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackII(&state, &nums, &selected, &res)\n    return res\n}\n
permutations_ii.swift
/* Backtracking algorithm: Permutations II */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // When the state length equals the number of elements, record the solution\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    var duplicated: Set<Int> = []\n    for (i, choice) in choices.enumerated() {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if !selected[i], !duplicated.contains(choice) {\n            // Attempt: make choice, update state\n            duplicated.insert(choice) // Record the selected element value\n            selected[i] = true\n            state.append(choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Permutations II */\nfunc permutationsII(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
permutations_ii.js
/* Backtracking algorithm: Permutations II */\nfunction backtrack(state, choices, selected, res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations II */\nfunction permutationsII(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_ii.ts
/* Backtracking algorithm: Permutations II */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations II */\nfunction permutationsII(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_ii.dart
/* Backtracking algorithm: Permutations II */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // When the state length equals the number of elements, record the solution\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  Set<int> duplicated = {};\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n    if (!selected[i] && !duplicated.contains(choice)) {\n      // Attempt: make choice, update state\n      duplicated.add(choice); // Record the selected element value\n      selected[i] = true;\n      state.add(choice);\n      // Proceed to the next round of selection\n      backtrack(state, choices, selected, res);\n      // Backtrack: undo choice, restore to previous state\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Permutations II */\nList<List<int>> permutationsII(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
permutations_ii.rs
/* Backtracking algorithm: Permutations II */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // When the state length equals the number of elements, record the solution\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Traverse all choices\n    let mut duplicated = HashSet::<i32>::new();\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if !selected[i] && !duplicated.contains(&choice) {\n            // Attempt: make choice, update state\n            duplicated.insert(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state.clone(), choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Permutations II */\nfn permutations_ii(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new();\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
permutations_ii.c
/* Backtracking algorithm: Permutations II */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // When the state length equals the number of elements, record the solution\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // Traverse all choices\n    bool duplicated[MAX_SIZE] = {false};\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated[choice]) {\n            // Attempt: make choice, update state\n            duplicated[choice] = true; // Record the selected element value\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Proceed to the next round of selection\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n        }\n    }\n}\n\n/* Permutations II */\nint **permutationsII(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
permutations_ii.kt
/* Backtracking algorithm: Permutations II */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    val duplicated = HashSet<Int>()\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice) // Record the selected element value\n            selected[i] = true\n            state.add(choice)\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Permutations II */\nfun permutationsII(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
permutations_ii.rb
### Backtracking: permutations II ###\ndef backtrack(state, choices, selected, res)\n  # When the state length equals the number of elements, record the solution\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  duplicated = Set.new\n  choices.each_with_index do |choice, i|\n    # Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n    if !selected[i] && !duplicated.include?(choice)\n      # Attempt: make choice, update state\n      duplicated.add(choice)\n      selected[i] = true\n      state << choice\n      # Proceed to the next round of selection\n      backtrack(state, choices, selected, res)\n      # Backtrack: undo choice, restore to previous state\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Permutations II ###\ndef permutations_ii(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n

Assuming elements are pairwise distinct, there are \\(n!\\) (factorial) permutations of \\(n\\) elements. When recording results, we need to copy a list of length \\(n\\), using \\(O(n)\\) time. Therefore, the time complexity is \\(O(n! \\cdot n)\\).

The maximum recursion depth is \\(n\\), using \\(O(n)\\) stack frame space. selected uses \\(O(n)\\) space. At most \\(n\\) duplicated sets exist simultaneously, using \\(O(n^2)\\) space. Therefore, the space complexity is \\(O(n^2)\\).

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3-comparison-of-two-pruning-methods","level":3,"title":"3.   Comparison of Two Pruning Methods","text":"

Note that although both selected and duplicated are used for pruning, they have different objectives.

  • Pruning duplicate choices: There is only one selected throughout the entire search process. It records which elements are included in the current state, and its purpose is to prevent an element from appearing repeatedly in state.
  • Pruning duplicate elements: Each round of choices (each backtrack function call) contains a duplicated set. It records which elements have been chosen in this round's iteration (the for loop), and its purpose is to ensure that equal elements are chosen only once.

Figure 13-9 shows the effective scope of the two pruning conditions. Note that each node in the tree represents a choice, and the nodes on the path from the root to a leaf node form a permutation.

Figure 13-9   Effective scope of two pruning conditions

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/","level":1,"title":"13.3   Subset-Sum Problem","text":"","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1331-without-duplicate-elements","level":2,"title":"13.3.1   Without Duplicate Elements","text":"

Question

Given a positive integer array nums and a target positive integer target, find all possible combinations where the sum of elements in the combination equals target. The given array has no duplicate elements, and each element can be selected multiple times. Return these combinations in list form, where the list should not contain duplicate combinations.

For example, given the set \\(\\{3, 4, 5\\}\\) and target integer \\(9\\), the solutions are \\(\\{3, 3, 3\\}, \\{4, 5\\}\\). Note the following two points:

  • Elements in the input set can be selected repeatedly without limit.
  • Subsets do not distinguish element order; for example, \\(\\{4, 5\\}\\) and \\(\\{5, 4\\}\\) are the same subset.
","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1-reference-to-full-permutation-solution","level":3,"title":"1.   Reference to Full Permutation Solution","text":"

Similar to the full permutation problem, we can imagine the process of generating subsets as a series of choices, and update the \"sum of elements\" in real-time during the selection process. When the sum equals target, we record the subset to the result list.

Unlike the full permutation problem, elements in this problem's set can be selected unlimited times, so we do not need to use a selected boolean list to track whether an element has been selected. We can make minor modifications to the full permutation code and initially obtain the solution:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i_naive.py
def backtrack(\n    state: list[int],\n    target: int,\n    total: int,\n    choices: list[int],\n    res: list[list[int]],\n):\n    \"\"\"Backtracking algorithm: Subset sum I\"\"\"\n    # When the subset sum equals target, record the solution\n    if total == target:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    for i in range(len(choices)):\n        # Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target:\n            continue\n        # Attempt: make choice, update element sum total\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum I (including duplicate subsets)\"\"\"\n    state = []  # State (subset)\n    total = 0  # Subset sum\n    res = []  # Result list (subset list)\n    backtrack(state, target, total, nums, res)\n    return res\n
subset_sum_i_naive.cpp
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    for (size_t i = 0; i < choices.size(); i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nvector<vector<int>> subsetSumINaive(vector<int> &nums, int target) {\n    vector<int> state;       // State (subset)\n    int total = 0;           // Subset sum\n    vector<vector<int>> res; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.java
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<Integer>> subsetSumINaive(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    int total = 0; // Subset sum\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.cs
/* Backtracking algorithm: Subset sum I */\nvoid Backtrack(List<int> state, int target, int total, int[] choices, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.Length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<int>> SubsetSumINaive(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    int total = 0; // Subset sum\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.go
/* Backtracking algorithm: Subset sum I */\nfunc backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == total {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    for i := 0; i < len(*choices); i++ {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total+(*choices)[i] > target {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunc subsetSumINaive(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    total := 0              // Subset sum\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumINaive(total, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_i_naive.swift
/* Backtracking algorithm: Subset sum I */\nfunc backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if total == target {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    for i in choices.indices {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunc subsetSumINaive(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let total = 0 // Subset sum\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, total: total, choices: nums, res: &res)\n    return res\n}\n
subset_sum_i_naive.js
/* Backtracking algorithm: Subset sum I */\nfunction backtrack(state, target, total, choices, res) {\n    // When the subset sum equals target, record the solution\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    for (let i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunction subsetSumINaive(nums, target) {\n    const state = []; // State (subset)\n    const total = 0; // Subset sum\n    const res = []; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.ts
/* Backtracking algorithm: Subset sum I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    total: number,\n    choices: number[],\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    for (let i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunction subsetSumINaive(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    const total = 0; // Subset sum\n    const res = []; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.dart
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  int total,\n  List<int> choices,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (total == target) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  for (int i = 0; i < choices.length; i++) {\n    // Pruning: if the subset sum exceeds target, skip this choice\n    if (total + choices[i] > target) {\n      continue;\n    }\n    // Attempt: make choice, update element sum total\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target, total + choices[i], choices, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<int>> subsetSumINaive(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  int total = 0; // Sum of elements\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, total, nums, res);\n  return res;\n}\n
subset_sum_i_naive.rs
/* Backtracking algorithm: Subset sum I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    total: i32,\n    choices: &[i32],\n    res: &mut Vec<Vec<i32>>,\n) {\n    // When the subset sum equals target, record the solution\n    if total == target {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    for i in 0..choices.len() {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    let total = 0; // Subset sum\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, total, nums, &mut res);\n    res\n}\n
subset_sum_i_naive.c
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(int target, int total, int *choices, int choicesSize) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choicesSize; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state[stateSize++] = choices[i];\n        // Proceed to the next round of selection\n        backtrack(target, total + choices[i], choices, choicesSize);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nvoid subsetSumINaive(int *nums, int numsSize, int target) {\n    resSize = 0; // Initialize solution count to 0\n    backtrack(target, 0, nums, numsSize);\n}\n
subset_sum_i_naive.kt
/* Backtracking algorithm: Subset sum I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    total: Int,\n    choices: IntArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    for (i in choices.indices) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfun subsetSumINaive(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    val total = 0 // Subset sum\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, total, nums, res)\n    return res\n}\n
subset_sum_i_naive.rb
### Backtracking: subset sum I ###\ndef backtrack(state, target, total, choices, res)\n  # When the subset sum equals target, record the solution\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  for i in 0...choices.length\n    # Pruning: if the subset sum exceeds target, skip this choice\n    next if total + choices[i] > target\n    # Attempt: make choice, update element sum total\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target, total + choices[i], choices, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum I (with duplicate subsets) ###\ndef subset_sum_i_naive(nums, target)\n  state = [] # State (subset)\n  total = 0 # Subset sum\n  res = [] # Result list (subset list)\n  backtrack(state, target, total, nums, res)\n  res\nend\n

When we input array \\([3, 4, 5]\\) and target element \\(9\\) to the above code, the output is \\([3, 3, 3], [4, 5], [5, 4]\\). Although we successfully find all subsets that sum to \\(9\\), there are duplicate subsets \\([4, 5]\\) and \\([5, 4]\\).

This is because the search process distinguishes the order of selections, but subsets do not distinguish selection order. As shown in Figure 13-10, selecting 4 first and then 5 versus selecting 5 first and then 4 are different branches, but they correspond to the same subset.

Figure 13-10   Subset search and boundary pruning

To eliminate duplicate subsets, one straightforward idea is to deduplicate the result list. However, this approach is very inefficient for two reasons:

  • When there are many array elements, especially when target is large, the search process generates many duplicate subsets.
  • Comparing subsets (arrays) is very time-consuming, requiring sorting the arrays first, then comparing each element in them.
","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2-pruning-duplicate-subsets","level":3,"title":"2.   Pruning Duplicate Subsets","text":"

We consider deduplication through pruning during the search process. Observing Figure 13-11, duplicate subsets occur when array elements are selected in different orders, as in the following cases:

  1. When the first and second rounds select \\(3\\) and \\(4\\) respectively, all subsets containing these two elements are generated, denoted as \\([3, 4, \\dots]\\).
  2. Afterward, when the first round selects \\(4\\), the second round should skip \\(3\\), because the subset \\([4, 3, \\dots]\\) generated by this choice is completely duplicate with the subset generated in step 1.

In the search process, each level's choices are tried from left to right, so the rightmost branches are pruned more.

  1. The first two rounds select \\(3\\) and \\(5\\), generating subset \\([3, 5, \\dots]\\).
  2. The first two rounds select \\(4\\) and \\(5\\), generating subset \\([4, 5, \\dots]\\).
  3. If the first round selects \\(5\\), the second round should skip \\(3\\) and \\(4\\), because subsets \\([5, 3, \\dots]\\) and \\([5, 4, \\dots]\\) are completely duplicate with the subsets described in steps 1. and 2.

Figure 13-11   Different selection orders leading to duplicate subsets

In summary, given an input array \\([x_1, x_2, \\dots, x_n]\\), let the selection sequence in the search process be \\([x_{i_1}, x_{i_2}, \\dots, x_{i_m}]\\). This selection sequence must satisfy \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\); any selection sequence that does not satisfy this condition will cause duplicates and should be pruned.

","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

To implement this pruning, we initialize a variable start to indicate the starting point of traversal. After making choice \\(x_{i}\\), set the next round to start traversal from index \\(i\\). This ensures that the selection sequence satisfies \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\), guaranteeing subset uniqueness.

In addition, we have made the following two optimizations to the code:

  • Before starting the search, first sort the array nums. When traversing all choices, end the loop immediately when the subset sum exceeds target, because subsequent elements are larger, and their subset sums must exceed target.
  • Omit the element sum variable total and use subtraction on target to track the sum of elements. Record the solution when target equals \\(0\\).
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i.py
def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Subset sum I\"\"\"\n    # When the subset sum equals target, record the solution\n    if target == 0:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    # Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in range(start, len(choices)):\n        # Pruning 1: if the subset sum exceeds target, end the loop directly\n        # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0:\n            break\n        # Attempt: make choice, update target, start\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_i(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum I\"\"\"\n    state = []  # State (subset)\n    nums.sort()  # Sort nums\n    start = 0  # Start point for traversal\n    res = []  # Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n
subset_sum_i.cpp
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.size(); i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum I */\nvector<vector<int>> subsetSumI(vector<int> &nums, int target) {\n    vector<int> state;              // State (subset)\n    sort(nums.begin(), nums.end()); // Sort nums\n    int start = 0;                  // Start point for traversal\n    vector<vector<int>> res;        // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.java
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum I */\nList<List<Integer>> subsetSumI(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    Arrays.sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.cs
/* Backtracking algorithm: Subset sum I */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.Length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum I */\nList<List<int>> SubsetSumI(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    Array.Sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.go
/* Backtracking algorithm: Subset sum I */\nfunc backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i := start; i < len(*choices); i++ {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Attempt: make choice, update target, start\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum I */\nfunc subsetSumI(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    sort.Ints(nums)         // Sort nums\n    start := 0              // Start point for traversal\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumI(start, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_i.swift
/* Backtracking algorithm: Subset sum I */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in choices.indices.dropFirst(start) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Attempt: make choice, update target, start\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum I */\nfunc subsetSumI(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let nums = nums.sorted() // Sort nums\n    let start = 0 // Start point for traversal\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
subset_sum_i.js
/* Backtracking algorithm: Subset sum I */\nfunction backtrack(state, target, choices, start, res) {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfunction subsetSumI(nums, target) {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.ts
/* Backtracking algorithm: Subset sum I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfunction subsetSumI(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.dart
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  // Pruning 2: start traversing from start to avoid generating duplicate subsets\n  for (int i = start; i < choices.length; i++) {\n    // Pruning 1: if the subset sum exceeds target, end the loop directly\n    // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Attempt: make choice, update target, start\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum I */\nList<List<int>> subsetSumI(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  nums.sort(); // Sort nums\n  int start = 0; // Start point for traversal\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
subset_sum_i.rs
/* Backtracking algorithm: Subset sum I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in start..choices.len() {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfn subset_sum_i(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    nums.sort(); // Sort nums\n    let start = 0; // Start point for traversal\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
subset_sum_i.c
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        for (int i = 0; i < stateSize; ++i) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choicesSize; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Proceed to the next round of selection\n        backtrack(target - choices[i], choices, choicesSize, i);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum I */\nvoid subsetSumI(int *nums, int numsSize, int target) {\n    qsort(nums, numsSize, sizeof(int), cmp); // Sort nums\n    int start = 0;                           // Start point for traversal\n    backtrack(target, nums, numsSize, start);\n}\n
subset_sum_i.kt
/* Backtracking algorithm: Subset sum I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (i in start..<choices.size) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum I */\nfun subsetSumI(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    nums.sort() // Sort nums\n    val start = 0 // Start point for traversal\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
subset_sum_i.rb
### Backtracking: subset sum I ###\ndef backtrack(state, target, choices, start, res)\n  # When the subset sum equals target, record the solution\n  if target.zero?\n    res << state.dup\n    return\n  end\n  # Traverse all choices\n  # Pruning 2: start traversing from start to avoid generating duplicate subsets\n  for i in start...choices.length\n    # Pruning 1: if the subset sum exceeds target, end the loop directly\n    # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    break if target - choices[i] < 0\n    # Attempt: make choice, update target, start\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum I ###\ndef subset_sum_i(nums, target)\n  state = [] # State (subset)\n  nums.sort! # Sort nums\n  start = 0 # Start point for traversal\n  res = [] # Result list (subset list)\n  backtrack(state, target, nums, start, res)\n  res\nend\n

Figure 13-12 shows the complete backtracking process when array \\([3, 4, 5]\\) and target element \\(9\\) are input to the above code.

Figure 13-12   Subset-sum I backtracking process

","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1332-with-duplicate-elements-in-array","level":2,"title":"13.3.2   With Duplicate Elements in Array","text":"

Question

Given a positive integer array nums and a target positive integer target, find all possible combinations where the sum of elements in the combination equals target. The given array may contain duplicate elements, and each element can be selected at most once. Return these combinations in list form, where the list should not contain duplicate combinations.

Compared to the previous problem, the input array in this problem may contain duplicate elements, which introduces new challenges. For example, given array \\([4, \\hat{4}, 5]\\) and target element \\(9\\), the output of the existing code is \\([4, 5], [\\hat{4}, 5]\\), which contains duplicate subsets.

The reason for this duplication is that equal elements are selected multiple times in a certain round. In Figure 13-13, the first round has three choices, two of which are \\(4\\), creating two duplicate search branches that output duplicate subsets. Similarly, the two \\(4\\)'s in the second round also produce duplicate subsets.

Figure 13-13   Duplicate subsets caused by equal elements

","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1-pruning-equal-elements","level":3,"title":"1.   Pruning Equal Elements","text":"

To solve this problem, we need to limit equal elements to be selected only once in each round. The implementation is quite clever: since the array is already sorted, equal elements are adjacent. This means that in a certain round of selection, if the current element equals the element to its left, it means this element has already been selected, so we skip the current element directly.

At the same time, this problem specifies that each array element can only be selected once. Fortunately, we can also use the variable start to satisfy this constraint: after making choice \\(x_{i}\\), set the next round to start traversal from index \\(i + 1\\) onwards. This both eliminates duplicate subsets and avoids selecting elements multiple times.

","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_ii.py
def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Subset sum II\"\"\"\n    # When the subset sum equals target, record the solution\n    if target == 0:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    # Pruning 2: start traversing from start to avoid generating duplicate subsets\n    # Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in range(start, len(choices)):\n        # Pruning 1: if the subset sum exceeds target, end the loop directly\n        # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0:\n            break\n        # Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start and choices[i] == choices[i - 1]:\n            continue\n        # Attempt: make choice, update target, start\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum II\"\"\"\n    state = []  # State (subset)\n    nums.sort()  # Sort nums\n    start = 0  # Start point for traversal\n    res = []  # Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n
subset_sum_ii.cpp
/* Backtracking algorithm: Subset sum II */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.size(); i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum II */\nvector<vector<int>> subsetSumII(vector<int> &nums, int target) {\n    vector<int> state;              // State (subset)\n    sort(nums.begin(), nums.end()); // Sort nums\n    int start = 0;                  // Start point for traversal\n    vector<vector<int>> res;        // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.java
/* Backtracking algorithm: Subset sum II */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum II */\nList<List<Integer>> subsetSumII(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    Arrays.sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.cs
/* Backtracking algorithm: Subset sum II */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.Length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum II */\nList<List<int>> SubsetSumII(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    Array.Sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.go
/* Backtracking algorithm: Subset sum II */\nfunc backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i := start; i < len(*choices); i++ {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start && (*choices)[i] == (*choices)[i-1] {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum II */\nfunc subsetSumII(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    sort.Ints(nums)         // Sort nums\n    start := 0              // Start point for traversal\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumII(start, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_ii.swift
/* Backtracking algorithm: Subset sum II */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in choices.indices.dropFirst(start) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start, choices[i] == choices[i - 1] {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum II */\nfunc subsetSumII(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let nums = nums.sorted() // Sort nums\n    let start = 0 // Start point for traversal\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
subset_sum_ii.js
/* Backtracking algorithm: Subset sum II */\nfunction backtrack(state, target, choices, start, res) {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfunction subsetSumII(nums, target) {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.ts
/* Backtracking algorithm: Subset sum II */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfunction subsetSumII(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.dart
/* Backtracking algorithm: Subset sum II */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  // Pruning 2: start traversing from start to avoid generating duplicate subsets\n  // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n  for (int i = start; i < choices.length; i++) {\n    // Pruning 1: if the subset sum exceeds target, end the loop directly\n    // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n    if (i > start && choices[i] == choices[i - 1]) {\n      continue;\n    }\n    // Attempt: make choice, update target, start\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i + 1, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum II */\nList<List<int>> subsetSumII(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  nums.sort(); // Sort nums\n  int start = 0; // Start point for traversal\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
subset_sum_ii.rs
/* Backtracking algorithm: Subset sum II */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in start..choices.len() {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start && choices[i] == choices[i - 1] {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    nums.sort(); // Sort nums\n    let start = 0; // Start point for traversal\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
subset_sum_ii.c
/* Backtracking algorithm: Subset sum II */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choicesSize; i++) {\n        // Pruning 1: Skip if subset sum exceeds target\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Proceed to the next round of selection\n        backtrack(target - choices[i], choices, choicesSize, i + 1);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum II */\nvoid subsetSumII(int *nums, int numsSize, int target) {\n    // Sort nums\n    qsort(nums, numsSize, sizeof(int), cmp);\n    // Start backtracking\n    backtrack(target, nums, numsSize, 0);\n}\n
subset_sum_ii.kt
/* Backtracking algorithm: Subset sum II */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (i in start..<choices.size) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum II */\nfun subsetSumII(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    nums.sort() // Sort nums\n    val start = 0 // Start point for traversal\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
subset_sum_ii.rb
### Backtracking: subset sum II ###\ndef backtrack(state, target, choices, start, res)\n  # When the subset sum equals target, record the solution\n  if target.zero?\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  # Pruning 2: start traversing from start to avoid generating duplicate subsets\n  # Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n  for i in start...choices.length\n    # Pruning 1: if the subset sum exceeds target, end the loop directly\n    # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    break if target - choices[i] < 0\n    # Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n    next if i > start && choices[i] == choices[i - 1]\n    # Attempt: make choice, update target, start\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i + 1, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum II ###\ndef subset_sum_ii(nums, target)\n  state = [] # State (subset)\n  nums.sort! # Sort nums\n  start = 0 # Start point for traversal\n  res = [] # Result list (subset list)\n  backtrack(state, target, nums, start, res)\n  res\nend\n

Figure 13-14 shows the backtracking process for array \\([4, 4, 5]\\) and target element \\(9\\), which includes four types of pruning operations. Combine the illustration with the code comments to understand the entire search process and how each pruning operation works.

Figure 13-14   Subset-sum II backtracking process

","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/summary/","level":1,"title":"13.5   Summary","text":"","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_backtracking/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • The backtracking algorithm is fundamentally an exhaustive search method. It finds solutions that meet specified conditions by performing a depth-first traversal of the solution space. During the search process, when a solution satisfying the conditions is found, it is recorded. The search ends either after finding all solutions or when the traversal is complete.
  • The backtracking algorithm search process consists of two parts: attempting and backtracking. It tries various choices through depth-first search. When encountering situations that violate constraints, it reverts the previous choice, returns to the previous state, and continues exploring other options. Attempting and backtracking are operations in opposite directions.
  • Backtracking problems typically contain multiple constraints, which can be utilized to implement pruning operations. Pruning can terminate unnecessary search branches early, significantly improving search efficiency.
  • The backtracking algorithm is primarily used to solve search problems and constraint satisfaction problems. While combinatorial optimization problems can be solved with backtracking, there are often more efficient or better-performing solutions available.
  • The permutation problem aims to find all possible permutations of elements in a given set. We use an array to record whether each element has been selected, thereby pruning search branches that attempt to select the same element repeatedly, ensuring each element is selected exactly once.
  • In the permutation problem, if the set contains duplicate elements, the final result will contain duplicate permutations. We need to impose a constraint so that equal elements can only be selected once per round, which is typically achieved using a hash set.
  • The subset-sum problem aims to find all subsets of a given set that sum to a target value. Since the set is unordered but the search process outputs results in all orders, duplicate subsets are generated. We sort the data before backtracking and use a variable to indicate the starting point of each round's traversal, thereby pruning search branches that generate duplicate subsets.
  • For the subset-sum problem, equal elements in the array produce duplicate sets. We leverage the precondition that the array is sorted by checking whether adjacent elements are equal to implement pruning, ensuring that equal elements can only be selected once per round.
  • The \\(n\\) queens problem aims to find placements of \\(n\\) queens on an \\(n \\times n\\) chessboard such that no two queens can attack each other. The constraints of this problem include row constraints, column constraints, and main and anti-diagonal constraints. To satisfy row constraints, we adopt a row-by-row placement strategy, ensuring exactly one queen is placed in each row.
  • The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether each column has a queen, thereby indicating whether a selected cell is valid. For diagonal constraints, we use two arrays to separately record whether queens exist on each main or anti-diagonal. The challenge lies in finding the row-column index pattern that characterizes cells on the same main (anti-)diagonal.
","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_backtracking/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: How should we understand the relationship between backtracking and recursion?

Overall, backtracking is an \"algorithm strategy\", while recursion is more like a \"tool\".

  • The backtracking algorithm is typically implemented based on recursion. However, backtracking is one application scenario of recursion and represents the application of recursion in search problems.
  • The structure of recursion embodies the \"subproblem decomposition\" problem-solving paradigm, commonly used to solve problems involving divide-and-conquer, backtracking, and dynamic programming (memoized recursion).
","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"Chapter 2.   Complexity Analysis","text":"

Abstract

Complexity analysis is like a space-time guide in the vast universe of algorithms.

It leads us to explore deeply within the two dimensions of time and space, seeking more elegant solutions.

","path":["Chapter 2. Complexity Analysis","Chapter 2.   Complexity Analysis"],"tags":[]},{"location":"chapter_computational_complexity/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 2.1   Algorithm Efficiency Evaluation
  • 2.2   Iteration and Recursion
  • 2.3   Time Complexity
  • 2.4   Space Complexity
  • 2.5   Summary
","path":["Chapter 2. Complexity Analysis","Chapter 2.   Complexity Analysis"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   Iteration and Recursion","text":"

In algorithms, repeatedly executing a task is very common and closely related to complexity analysis. Therefore, before introducing time complexity and space complexity, let's first understand how to implement repeated task execution in programs, namely the two basic program control structures: iteration and recursion.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#221-iteration","level":2,"title":"2.2.1   Iteration","text":"

Iteration is a control structure for repeatedly executing a task. In iteration, a program repeatedly executes a segment of code under certain conditions until those conditions are no longer satisfied.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-for-loop","level":3,"title":"1.   For Loop","text":"

The for loop is one of the most common forms of iteration, suitable for use when the number of iterations is known in advance.

The following function implements the summation \\(1 + 2 + \\dots + n\\) based on a for loop, with the sum result recorded using the variable res. Note that in Python, range(a, b) corresponds to a \"left-closed, right-open\" interval, with the traversal range being \\(a, a + 1, \\dots, b-1\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def for_loop(n: int) -> int:\n    \"\"\"for loop\"\"\"\n    res = 0\n    # Sum 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        res += i\n    return res\n
iteration.cpp
/* for loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        res += i;\n    }\n    return res;\n}\n
iteration.java
/* for loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.cs
/* for loop */\nint ForLoop(int n) {\n    int res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.go
/* for loop */\nfunc forLoop(n int) int {\n    res := 0\n    // Sum 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        res += i\n    }\n    return res\n}\n
iteration.swift
/* for loop */\nfunc forLoop(n: Int) -> Int {\n    var res = 0\n    // Sum 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        res += i\n    }\n    return res\n}\n
iteration.js
/* for loop */\nfunction forLoop(n) {\n    let res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.ts
/* for loop */\nfunction forLoop(n: number): number {\n    let res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.dart
/* for loop */\nint forLoop(int n) {\n  int res = 0;\n  // Sum 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    res += i;\n  }\n  return res;\n}\n
iteration.rs
/* for loop */\nfn for_loop(n: i32) -> i32 {\n    let mut res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for i in 1..=n {\n        res += i;\n    }\n    res\n}\n
iteration.c
/* for loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.kt
/* for loop */\nfun forLoop(n: Int): Int {\n    var res = 0\n    // Sum 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        res += i\n    }\n    return res\n}\n
iteration.rb
### for loop ###\ndef for_loop(n)\n  res = 0\n\n  # Sum 1, 2, ..., n-1, n\n  for i in 1..n\n    res += i\n  end\n\n  res\nend\n

Figure 2-1 shows the flowchart of this summation function.

Figure 2-1   Flowchart of the summation function

The number of operations in this summation function is proportional to the input data size \\(n\\), or has a \"linear relationship\". In fact, time complexity describes precisely this \"linear relationship\". Related content will be introduced in detail in the next section.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-while-loop","level":3,"title":"2.   While Loop","text":"

Similar to the for loop, the while loop is also a method for implementing iteration. In a while loop, the program first checks the condition in each round; if the condition is true, it continues execution, otherwise it ends the loop.

Below we use a while loop to implement the summation \\(1 + 2 + \\dots + n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def while_loop(n: int) -> int:\n    \"\"\"while loop\"\"\"\n    res = 0\n    i = 1  # Initialize condition variable\n    # Sum 1, 2, ..., n-1, n\n    while i <= n:\n        res += i\n        i += 1  # Update condition variable\n    return res\n
iteration.cpp
/* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
iteration.java
/* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
iteration.cs
/* while loop */\nint WhileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i += 1; // Update condition variable\n    }\n    return res;\n}\n
iteration.go
/* while loop */\nfunc whileLoop(n int) int {\n    res := 0\n    // Initialize condition variable\n    i := 1\n    // Sum 1, 2, ..., n-1, n\n    for i <= n {\n        res += i\n        // Update condition variable\n        i++\n    }\n    return res\n}\n
iteration.swift
/* while loop */\nfunc whileLoop(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while i <= n {\n        res += i\n        i += 1 // Update condition variable\n    }\n    return res\n}\n
iteration.js
/* while loop */\nfunction whileLoop(n) {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
iteration.ts
/* while loop */\nfunction whileLoop(n: number): number {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
iteration.dart
/* while loop */\nint whileLoop(int n) {\n  int res = 0;\n  int i = 1; // Initialize condition variable\n  // Sum 1, 2, ..., n-1, n\n  while (i <= n) {\n    res += i;\n    i++; // Update condition variable\n  }\n  return res;\n}\n
iteration.rs
/* while loop */\nfn while_loop(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Initialize condition variable\n\n    // Sum 1, 2, ..., n-1, n\n    while i <= n {\n        res += i;\n        i += 1; // Update condition variable\n    }\n    res\n}\n
iteration.c
/* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
iteration.kt
/* while loop */\nfun whileLoop(n: Int): Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i\n        i++ // Update condition variable\n    }\n    return res\n}\n
iteration.rb
### while loop ###\ndef while_loop(n)\n  res = 0\n  i = 1 # Initialize condition variable\n\n  # Sum 1, 2, ..., n-1, n\n  while i <= n\n    res += i\n    i += 1 # Update condition variable\n  end\n\n  res\nend\n

The while loop has greater flexibility than the for loop. In a while loop, we can freely design the initialization and update steps of the condition variable.

For example, in the following code, the condition variable \\(i\\) is updated twice per round, which is not convenient to implement using a for loop:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def while_loop_ii(n: int) -> int:\n    \"\"\"while loop (two updates)\"\"\"\n    res = 0\n    i = 1  # Initialize condition variable\n    # Sum 1, 4, 10, ...\n    while i <= n:\n        res += i\n        # Update condition variable\n        i += 1\n        i *= 2\n    return res\n
iteration.cpp
/* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.java
/* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.cs
/* while loop (two updates) */\nint WhileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i += 1; \n        i *= 2;\n    }\n    return res;\n}\n
iteration.go
/* while loop (two updates) */\nfunc whileLoopII(n int) int {\n    res := 0\n    // Initialize condition variable\n    i := 1\n    // Sum 1, 4, 10, ...\n    for i <= n {\n        res += i\n        // Update condition variable\n        i++\n        i *= 2\n    }\n    return res\n}\n
iteration.swift
/* while loop (two updates) */\nfunc whileLoopII(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while i <= n {\n        res += i\n        // Update condition variable\n        i += 1\n        i *= 2\n    }\n    return res\n}\n
iteration.js
/* while loop (two updates) */\nfunction whileLoopII(n) {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.ts
/* while loop (two updates) */\nfunction whileLoopII(n: number): number {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.dart
/* while loop (two updates) */\nint whileLoopII(int n) {\n  int res = 0;\n  int i = 1; // Initialize condition variable\n  // Sum 1, 4, 10, ...\n  while (i <= n) {\n    res += i;\n    // Update condition variable\n    i++;\n    i *= 2;\n  }\n  return res;\n}\n
iteration.rs
/* while loop (two updates) */\nfn while_loop_ii(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Initialize condition variable\n\n    // Sum 1, 4, 10, ...\n    while i <= n {\n        res += i;\n        // Update condition variable\n        i += 1;\n        i *= 2;\n    }\n    res\n}\n
iteration.c
/* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.kt
/* while loop (two updates) */\nfun whileLoopII(n: Int): Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i\n        // Update condition variable\n        i++\n        i *= 2\n    }\n    return res\n}\n
iteration.rb
### while loop (two updates) ###\ndef while_loop_ii(n)\n  res = 0\n  i = 1 # Initialize condition variable\n\n  # Sum 1, 4, 10, ...\n  while i <= n\n    res += i\n    # Update condition variable\n    i += 1\n    i *= 2\n  end\n\n  res\nend\n

Overall, for loops have more compact code, while while loops are more flexible; both can implement iterative structures. The choice of which to use should be determined based on the requirements of the specific problem.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3-nested-loops","level":3,"title":"3.   Nested Loops","text":"

We can nest one loop structure inside another. Below is an example using for loops:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def nested_for_loop(n: int) -> str:\n    \"\"\"Nested for loop\"\"\"\n    res = \"\"\n    # Loop i = 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        # Loop j = 1, 2, ..., n-1, n\n        for j in range(1, n + 1):\n            res += f\"({i}, {j}), \"\n    return res\n
iteration.cpp
/* Nested for loop */\nstring nestedForLoop(int n) {\n    ostringstream res;\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; ++j) {\n            res << \"(\" << i << \", \" << j << \"), \";\n        }\n    }\n    return res.str();\n}\n
iteration.java
/* Nested for loop */\nString nestedForLoop(int n) {\n    StringBuilder res = new StringBuilder();\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            res.append(\"(\" + i + \", \" + j + \"), \");\n        }\n    }\n    return res.toString();\n}\n
iteration.cs
/* Nested for loop */\nstring NestedForLoop(int n) {\n    StringBuilder res = new();\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            res.Append($\"({i}, {j}), \");\n        }\n    }\n    return res.ToString();\n}\n
iteration.go
/* Nested for loop */\nfunc nestedForLoop(n int) string {\n    res := \"\"\n    // Loop i = 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= n; j++ {\n            // Loop j = 1, 2, ..., n-1, n\n            res += fmt.Sprintf(\"(%d, %d), \", i, j)\n        }\n    }\n    return res\n}\n
iteration.swift
/* Nested for loop */\nfunc nestedForLoop(n: Int) -> String {\n    var res = \"\"\n    // Loop i = 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        // Loop j = 1, 2, ..., n-1, n\n        for j in 1 ... n {\n            res.append(\"(\\(i), \\(j)), \")\n        }\n    }\n    return res\n}\n
iteration.js
/* Nested for loop */\nfunction nestedForLoop(n) {\n    let res = '';\n    // Loop i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
iteration.ts
/* Nested for loop */\nfunction nestedForLoop(n: number): string {\n    let res = '';\n    // Loop i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
iteration.dart
/* Nested for loop */\nString nestedForLoop(int n) {\n  String res = \"\";\n  // Loop i = 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    // Loop j = 1, 2, ..., n-1, n\n    for (int j = 1; j <= n; j++) {\n      res += \"($i, $j), \";\n    }\n  }\n  return res;\n}\n
iteration.rs
/* Nested for loop */\nfn nested_for_loop(n: i32) -> String {\n    let mut res = vec![];\n    // Loop i = 1, 2, ..., n-1, n\n    for i in 1..=n {\n        // Loop j = 1, 2, ..., n-1, n\n        for j in 1..=n {\n            res.push(format!(\"({}, {}), \", i, j));\n        }\n    }\n    res.join(\"\")\n}\n
iteration.c
/* Nested for loop */\nchar *nestedForLoop(int n) {\n    // n * n is the number of points, \"(i, j), \" string max length is 6+10*2, plus extra space for null character \\0\n    int size = n * n * 26 + 1;\n    char *res = malloc(size * sizeof(char));\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            char tmp[26];\n            snprintf(tmp, sizeof(tmp), \"(%d, %d), \", i, j);\n            strncat(res, tmp, size - strlen(res) - 1);\n        }\n    }\n    return res;\n}\n
iteration.kt
/* Nested for loop */\nfun nestedForLoop(n: Int): String {\n    val res = StringBuilder()\n    // Loop i = 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (j in 1..n) {\n            res.append(\" ($i, $j), \")\n        }\n    }\n    return res.toString()\n}\n
iteration.rb
### Nested for loop ###\ndef nested_for_loop(n)\n  res = \"\"\n\n  # Loop i = 1, 2, ..., n-1, n\n  for i in 1..n\n    # Loop j = 1, 2, ..., n-1, n\n    for j in 1..n\n      res += \"(#{i}, #{j}), \"\n    end\n  end\n\n  res\nend\n

Figure 2-2 shows the flowchart of this nested loop.

Figure 2-2   Flowchart of nested loops

In this case, the number of operations of the function is proportional to \\(n^2\\), or the algorithm's running time has a \"quadratic relationship\" with the input data size \\(n\\).

We can continue adding nested loops, where each nesting is a \"dimension increase\", raising the time complexity to \"cubic relationship\", \"quartic relationship\", and so on.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222-recursion","level":2,"title":"2.2.2   Recursion","text":"

Recursion is an algorithmic strategy that solves problems by having a function call itself. It mainly consists of two phases.

  1. Descend: The program continuously calls itself deeper, usually passing in smaller or more simplified parameters, until reaching a \"termination condition\".
  2. Ascend: After triggering the \"termination condition\", the program returns layer by layer from the deepest recursive function, aggregating the result of each layer.

From an implementation perspective, recursive code mainly consists of three elements.

  1. Termination condition: Used to determine when to switch from \"descending\" to \"ascending\".
  2. Recursive call: Corresponds to \"descending\", where the function calls itself, usually with smaller or more simplified parameters.
  3. Return result: Corresponds to \"ascending\", returning the result of the current recursion level to the previous layer.

Observe the following code. We only need to call the function recur(n) to complete the calculation of \\(1 + 2 + \\dots + n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def recur(n: int) -> int:\n    \"\"\"Recursion\"\"\"\n    # Termination condition\n    if n == 1:\n        return 1\n    # Recurse: recursive call\n    res = recur(n - 1)\n    # Return: return result\n    return n + res\n
recursion.cpp
/* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.java
/* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.cs
/* Recursion */\nint Recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = Recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.go
/* Recursion */\nfunc recur(n int) int {\n    // Termination condition\n    if n == 1 {\n        return 1\n    }\n    // Recurse: recursive call\n    res := recur(n - 1)\n    // Return: return result\n    return n + res\n}\n
recursion.swift
/* Recursion */\nfunc recur(n: Int) -> Int {\n    // Termination condition\n    if n == 1 {\n        return 1\n    }\n    // Recurse: recursive call\n    let res = recur(n: n - 1)\n    // Return: return result\n    return n + res\n}\n
recursion.js
/* Recursion */\nfunction recur(n) {\n    // Termination condition\n    if (n === 1) return 1;\n    // Recurse: recursive call\n    const res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.ts
/* Recursion */\nfunction recur(n: number): number {\n    // Termination condition\n    if (n === 1) return 1;\n    // Recurse: recursive call\n    const res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.dart
/* Recursion */\nint recur(int n) {\n  // Termination condition\n  if (n == 1) return 1;\n  // Recurse: recursive call\n  int res = recur(n - 1);\n  // Return: return result\n  return n + res;\n}\n
recursion.rs
/* Recursion */\nfn recur(n: i32) -> i32 {\n    // Termination condition\n    if n == 1 {\n        return 1;\n    }\n    // Recurse: recursive call\n    let res = recur(n - 1);\n    // Return: return result\n    n + res\n}\n
recursion.c
/* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.kt
/* Recursion */\nfun recur(n: Int): Int {\n    // Termination condition\n    if (n == 1)\n        return 1\n    // Descend: recursive call\n    val res = recur(n - 1)\n    // Return: return result\n    return n + res\n}\n
recursion.rb
### Recursion ###\ndef recur(n)\n  # Termination condition\n  return 1 if n == 1\n  # Recurse: recursive call\n  res = recur(n - 1)\n  # Return: return result\n  n + res\nend\n

Figure 2-3 shows the recursive process of this function.

Figure 2-3   Recursive process of the summation function

Although from a computational perspective, iteration and recursion can achieve the same results, they represent two completely different paradigms for thinking about and solving problems.

  • Iteration: Solves problems \"bottom-up\". Starting from the most basic steps, these steps are then repeatedly executed or accumulated until the task is complete.
  • Recursion: Solves problems \"top-down\". The original problem is decomposed into smaller subproblems that have the same form as the original problem. These subproblems continue to be decomposed into even smaller subproblems until reaching the base case (where the solution is known).

Taking the above summation function as an example, let the problem be \\(f(n) = 1 + 2 + \\dots + n\\).

  • Iteration: Simulates the summation process in a loop, traversing from \\(1\\) to \\(n\\), performing the summation operation in each round to obtain \\(f(n)\\).
  • Recursion: Decomposes the problem into the subproblem \\(f(n) = n + f(n-1)\\), continuously decomposing (recursively) until terminating at the base case \\(f(1) = 1\\).
","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-call-stack","level":3,"title":"1.   Call Stack","text":"

Each time a recursive function calls itself, the system allocates memory for the newly opened function to store local variables, call addresses, and other information. This leads to two consequences.

  • The function's context data is stored in a memory area called \"stack frame space\", which is not released until the function returns. Therefore, recursion usually consumes more memory space than iteration.
  • Recursive function calls incur additional overhead. Therefore, recursion is usually less time-efficient than loops.

As shown in Figure 2-4, before the termination condition is triggered, there are \\(n\\) unreturned recursive functions existing simultaneously, with a recursion depth of \\(n\\).

Figure 2-4   Recursion call depth

In practice, the recursion depth allowed by programming languages is usually limited, and excessively deep recursion may lead to stack overflow errors.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-tail-recursion","level":3,"title":"2.   Tail Recursion","text":"

Interestingly, if a function makes the recursive call as the very last step before returning, the function can be optimized by the compiler or interpreter to have space efficiency comparable to iteration. This case is called tail recursion.

  • Regular recursion: When a function returns to the previous level, it needs to continue executing code, so the system needs to save the context of the previous layer's call.
  • Tail recursion: The recursive call is the last operation before the function returns, meaning that after returning to the previous level, there is no need to continue executing other operations, so the system does not need to save the context of the previous layer's function.

Taking the calculation of \\(1 + 2 + \\dots + n\\) as an example, we can set the result variable res as a function parameter to implement tail recursion:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def tail_recur(n, res):\n    \"\"\"Tail recursion\"\"\"\n    # Termination condition\n    if n == 0:\n        return res\n    # Tail recursive call\n    return tail_recur(n - 1, res + n)\n
recursion.cpp
/* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
recursion.java
/* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
recursion.cs
/* Tail recursion */\nint TailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return TailRecur(n - 1, res + n);\n}\n
recursion.go
/* Tail recursion */\nfunc tailRecur(n int, res int) int {\n    // Termination condition\n    if n == 0 {\n        return res\n    }\n    // Tail recursive call\n    return tailRecur(n-1, res+n)\n}\n
recursion.swift
/* Tail recursion */\nfunc tailRecur(n: Int, res: Int) -> Int {\n    // Termination condition\n    if n == 0 {\n        return res\n    }\n    // Tail recursive call\n    return tailRecur(n: n - 1, res: res + n)\n}\n
recursion.js
/* Tail recursion */\nfunction tailRecur(n, res) {\n    // Termination condition\n    if (n === 0) return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
recursion.ts
/* Tail recursion */\nfunction tailRecur(n: number, res: number): number {\n    // Termination condition\n    if (n === 0) return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
recursion.dart
/* Tail recursion */\nint tailRecur(int n, int res) {\n  // Termination condition\n  if (n == 0) return res;\n  // Tail recursive call\n  return tailRecur(n - 1, res + n);\n}\n
recursion.rs
/* Tail recursion */\nfn tail_recur(n: i32, res: i32) -> i32 {\n    // Termination condition\n    if n == 0 {\n        return res;\n    }\n    // Tail recursive call\n    tail_recur(n - 1, res + n)\n}\n
recursion.c
/* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
recursion.kt
/* Tail recursion */\ntailrec fun tailRecur(n: Int, res: Int): Int {\n    // Add tailrec keyword to enable tail recursion optimization\n    // Termination condition\n    if (n == 0)\n        return res\n    // Tail recursive call\n    return tailRecur(n - 1, res + n)\n}\n
recursion.rb
### Tail recursion ###\ndef tail_recur(n, res)\n  # Termination condition\n  return res if n == 0\n  # Tail recursive call\n  tail_recur(n - 1, res + n)\nend\n

The execution process of tail recursion is shown in Figure 2-5. Comparing regular recursion and tail recursion, the execution point of the summation operation is different.

  • Regular recursion: The summation operation is performed during the \"ascending\" process, requiring an additional summation operation after each layer returns.
  • Tail recursion: The summation operation is performed during the \"descending\" process; the \"ascending\" process only needs to return layer by layer.

Figure 2-5   Tail recursion process

Tip

Please note that many compilers or interpreters do not support tail recursion optimization. For example, Python does not support tail recursion optimization by default, so even if a function is in tail recursive form, it may still encounter stack overflow issues.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3-recursion-tree","level":3,"title":"3.   Recursion Tree","text":"

When dealing with algorithmic problems related to \"divide and conquer\", recursion often provides a more intuitive approach and more readable code than iteration. Taking the \"Fibonacci sequence\" as an example.

Question

Given a Fibonacci sequence \\(0, 1, 1, 2, 3, 5, 8, 13, \\dots\\), find the \\(n\\)-th number in the sequence.

Let the \\(n\\)-th number of the Fibonacci sequence be \\(f(n)\\). Two conclusions can be easily obtained.

  • The first two numbers of the sequence are \\(f(1) = 0\\) and \\(f(2) = 1\\).
  • Each number in the sequence is the sum of the previous two numbers, i.e., \\(f(n) = f(n - 1) + f(n - 2)\\).

Following the recurrence relation to make recursive calls, with the first two numbers as termination conditions, we can write the recursive code. Calling fib(n) will give us the \\(n\\)-th number of the Fibonacci sequence:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def fib(n: int) -> int:\n    \"\"\"Fibonacci sequence: recursion\"\"\"\n    # Termination condition f(1) = 0, f(2) = 1\n    if n == 1 or n == 2:\n        return n - 1\n    # Recursive call f(n) = f(n-1) + f(n-2)\n    res = fib(n - 1) + fib(n - 2)\n    # Return result f(n)\n    return res\n
recursion.cpp
/* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.java
/* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.cs
/* Fibonacci sequence: recursion */\nint Fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = Fib(n - 1) + Fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.go
/* Fibonacci sequence: recursion */\nfunc fib(n int) int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    res := fib(n-1) + fib(n-2)\n    // Return result f(n)\n    return res\n}\n
recursion.swift
/* Fibonacci sequence: recursion */\nfunc fib(n: Int) -> Int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    let res = fib(n: n - 1) + fib(n: n - 2)\n    // Return result f(n)\n    return res\n}\n
recursion.js
/* Fibonacci sequence: recursion */\nfunction fib(n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.ts
/* Fibonacci sequence: recursion */\nfunction fib(n: number): number {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.dart
/* Fibonacci sequence: recursion */\nint fib(int n) {\n  // Termination condition f(1) = 0, f(2) = 1\n  if (n == 1 || n == 2) return n - 1;\n  // Recursive call f(n) = f(n-1) + f(n-2)\n  int res = fib(n - 1) + fib(n - 2);\n  // Return result f(n)\n  return res;\n}\n
recursion.rs
/* Fibonacci sequence: recursion */\nfn fib(n: i32) -> i32 {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1;\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    let res = fib(n - 1) + fib(n - 2);\n    // Return result\n    res\n}\n
recursion.c
/* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.kt
/* Fibonacci sequence: recursion */\nfun fib(n: Int): Int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    val res = fib(n - 1) + fib(n - 2)\n    // Return result f(n)\n    return res\n}\n
recursion.rb
### Fibonacci sequence: recursion ###\ndef fib(n)\n  # Termination condition f(1) = 0, f(2) = 1\n  return n - 1 if n == 1 || n == 2\n  # Recursive call f(n) = f(n-1) + f(n-2)\n  res = fib(n - 1) + fib(n - 2)\n  # Return result f(n)\n  res\nend\n

Observing the above code, we recursively call two functions within the function, meaning that one call produces two call branches. As shown in Figure 2-6, such continuous recursive calling will eventually produce a recursion tree with \\(n\\) levels.

Figure 2-6   Recursion tree of the Fibonacci sequence

Fundamentally, recursion embodies the paradigm of \"decomposing a problem into smaller subproblems\", and this divide-and-conquer strategy is crucial.

  • From an algorithmic perspective, many important algorithmic strategies such as searching, sorting, backtracking, divide and conquer, and dynamic programming directly or indirectly apply this way of thinking.
  • From a data structure perspective, recursion is naturally suited for handling problems related to linked lists, trees, and graphs, because they are well-suited for analysis using divide-and-conquer thinking.
","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223-comparison-of-the-two","level":2,"title":"2.2.3   Comparison of the Two","text":"

Summarizing the above content, as shown in Table 2-1, iteration and recursion differ in implementation, performance, and applicability.

Table 2-1   Comparison of iteration and recursion characteristics

Iteration Recursion Implementation Loop structure Function calls itself Time efficiency Generally more efficient, no function call overhead Each function call incurs overhead Memory usage Usually uses a fixed amount of memory space Accumulated function calls may use a large amount of stack frame space Suitable problems Suitable for simple loop tasks, with intuitive and readable code Suitable for subproblem decomposition, such as trees, graphs, divide and conquer, backtracking, etc., with concise and clear code structure

Tip

If you find the following content difficult to understand, you can review it after reading the \"Stack\" chapter.

What is the intrinsic relationship between iteration and recursion? Taking the above recursive function as an example, the summation operation is performed during the \"ascending\" phase of recursion. This means that the function called first actually completes its summation operation last, and this working mechanism is similar to the \"last-in, first-out\" principle of stacks.

In fact, recursive terminology such as \"call stack\" and \"stack frame space\" already hints at the close relationship between recursion and stacks.

  1. Descend: When a function is called, the system allocates a new stack frame on the \"call stack\" for that function to store the function's local variables, parameters, return address, and other data.
  2. Ascend: When the function completes execution and returns, the corresponding stack frame is removed from the \"call stack\", restoring the execution environment of the previous function.

Therefore, we can use an explicit stack to simulate the behavior of the call stack, thus transforming recursion into iterative form:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def for_loop_recur(n: int) -> int:\n    \"\"\"Simulate recursion using iteration\"\"\"\n    # Use an explicit stack to simulate the system call stack\n    stack = []\n    res = 0\n    # Recurse: recursive call\n    for i in range(n, 0, -1):\n        # Simulate \"recurse\" with \"push\"\n        stack.append(i)\n    # Return: return result\n    while stack:\n        # Simulate \"return\" with \"pop\"\n        res += stack.pop()\n    # res = 1+2+3+...+n\n    return res\n
recursion.cpp
/* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    stack<int> stack;\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (!stack.empty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.top();\n        stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.java
/* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    Stack<Integer> stack = new Stack<>();\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (!stack.isEmpty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.cs
/* Simulate recursion using iteration */\nint ForLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    Stack<int> stack = new();\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.Push(i);\n    }\n    // Return: return result\n    while (stack.Count > 0) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.Pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.go
/* Simulate recursion using iteration */\nfunc forLoopRecur(n int) int {\n    // Use an explicit stack to simulate the system call stack\n    stack := list.New()\n    res := 0\n    // Recurse: recursive call\n    for i := n; i > 0; i-- {\n        // Simulate \"recurse\" with \"push\"\n        stack.PushBack(i)\n    }\n    // Return: return result\n    for stack.Len() != 0 {\n        // Simulate \"return\" with \"pop\"\n        res += stack.Back().Value.(int)\n        stack.Remove(stack.Back())\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.swift
/* Simulate recursion using iteration */\nfunc forLoopRecur(n: Int) -> Int {\n    // Use an explicit stack to simulate the system call stack\n    var stack: [Int] = []\n    var res = 0\n    // Recurse: recursive call\n    for i in (1 ... n).reversed() {\n        // Simulate \"recurse\" with \"push\"\n        stack.append(i)\n    }\n    // Return: return result\n    while !stack.isEmpty {\n        // Simulate \"return\" with \"pop\"\n        res += stack.removeLast()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.js
/* Simulate recursion using iteration */\nfunction forLoopRecur(n) {\n    // Use an explicit stack to simulate the system call stack\n    const stack = [];\n    let res = 0;\n    // Recurse: recursive call\n    for (let i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (stack.length) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.ts
/* Simulate recursion using iteration */\nfunction forLoopRecur(n: number): number {\n    // Use an explicit stack to simulate the system call stack\n    const stack: number[] = [];\n    let res: number = 0;\n    // Recurse: recursive call\n    for (let i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (stack.length) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.dart
/* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n  // Use an explicit stack to simulate the system call stack\n  List<int> stack = [];\n  int res = 0;\n  // Recurse: recursive call\n  for (int i = n; i > 0; i--) {\n    // Simulate \"recurse\" with \"push\"\n    stack.add(i);\n  }\n  // Return: return result\n  while (!stack.isEmpty) {\n    // Simulate \"return\" with \"pop\"\n    res += stack.removeLast();\n  }\n  // res = 1+2+3+...+n\n  return res;\n}\n
recursion.rs
/* Simulate recursion using iteration */\nfn for_loop_recur(n: i32) -> i32 {\n    // Use an explicit stack to simulate the system call stack\n    let mut stack = Vec::new();\n    let mut res = 0;\n    // Recurse: recursive call\n    for i in (1..=n).rev() {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while !stack.is_empty() {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop().unwrap();\n    }\n    // res = 1+2+3+...+n\n    res\n}\n
recursion.c
/* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    int stack[1000]; // Use a large array to simulate stack\n    int top = -1;    // Stack top index\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack[1 + top++] = i;\n    }\n    // Return: return result\n    while (top >= 0) {\n        // Simulate \"return\" with \"pop\"\n        res += stack[top--];\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.kt
/* Simulate recursion using iteration */\nfun forLoopRecur(n: Int): Int {\n    // Use an explicit stack to simulate the system call stack\n    val stack = Stack<Int>()\n    var res = 0\n    // Descend: recursive call\n    for (i in n downTo 0) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i)\n    }\n    // Return: return result\n    while (stack.isNotEmpty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.rb
### Use iteration to simulate recursion ###\ndef for_loop_recur(n)\n  # Use an explicit stack to simulate the system call stack\n  stack = []\n  res = 0\n\n  # Recurse: recursive call\n  for i in n.downto(0)\n    # Simulate \"recurse\" with \"push\"\n    stack << i\n  end\n  # Return: return result\n  while !stack.empty?\n    res += stack.pop\n  end\n\n  # res = 1+2+3+...+n\n  res\nend\n

Observing the above code, when recursion is transformed into iteration, the code becomes more complex. Although iteration and recursion can be converted into each other in many cases, it may not be worthwhile to do so for the following two reasons.

  • The transformed code may be more difficult to understand and less readable.
  • For some complex problems, simulating the behavior of the system call stack can be very difficult.

In summary, choosing between iteration and recursion depends on the nature of the specific problem. In programming practice, it is crucial to weigh the pros and cons of both and choose the appropriate method based on the context.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   Algorithm Efficiency Evaluation","text":"

In algorithm design, we pursue the following two levels of objectives sequentially.

  1. Finding a solution to the problem: The algorithm must reliably obtain the correct solution within the specified input range.
  2. Seeking the optimal solution: Multiple solutions may exist for the same problem, and we hope to find an algorithm that is as efficient as possible.

In other words, under the premise of being able to solve the problem, algorithm efficiency has become the primary evaluation criterion for measuring the quality of algorithms. It includes the following two dimensions.

  • Time efficiency: The length of time the algorithm runs.
  • Space efficiency: The size of memory space the algorithm occupies.

In short, our goal is to design data structures and algorithms that are \"both fast and memory-efficient\". Effectively evaluating algorithm efficiency is crucial, because only in this way can we compare various algorithms and guide the algorithm design and optimization process.

Efficiency evaluation methods are mainly divided into two types: actual testing and theoretical estimation.

","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211-actual-testing","level":2,"title":"2.1.1   Actual Testing","text":"

Suppose we now have algorithm A and algorithm B, both of which can solve the same problem, and we need to compare the efficiency of these two algorithms. The most direct method is to find a computer, run these two algorithms, and monitor and record their running time and memory usage. This evaluation approach can reflect the real situation, but it also has considerable limitations.

On one hand, it is difficult to eliminate interference factors from the testing environment. Hardware configuration affects the performance of algorithms. For example, if an algorithm has a high degree of parallelism, it is more suitable for running on multi-core CPUs; if an algorithm has intensive memory operations, it will perform better on high-performance memory. In other words, the test results of an algorithm on different machines may be inconsistent. This means we need to test on various machines and calculate average efficiency, which is impractical.

On the other hand, conducting complete testing is very resource-intensive. As the input data volume changes, the algorithm will exhibit different efficiencies. For example, when the input data volume is small, the running time of algorithm A is shorter than algorithm B; but when the input data volume is large, the test results may be exactly the opposite. Therefore, to obtain convincing conclusions, we need to test input data of various scales, which requires a large amount of computational resources.

","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#212-theoretical-estimation","level":2,"title":"2.1.2   Theoretical Estimation","text":"

Since actual testing has considerable limitations, we can consider evaluating algorithm efficiency through calculations alone. This estimation method is called asymptotic complexity analysis, or complexity analysis for short.

Complexity analysis can reflect the relationship between the time and space resources required for algorithm execution and the input data scale. It describes the growth trend of the time and space required for algorithm execution as the input data scale increases. This definition is somewhat convoluted, so we can break it down into three key points to understand.

  • \"Time and space resources\" correspond to time complexity and space complexity, respectively.
  • \"As the input data scale increases\" means that complexity reflects the relationship between algorithm running efficiency and input data scale.
  • \"Growth trend of time and space\" indicates that complexity analysis focuses not on the specific values of running time or occupied space, but on how \"fast\" time or space grows.

Complexity analysis overcomes the drawbacks of the actual testing method, reflected in the following aspects.

  • It does not need to actually run the code, making it more environmentally friendly and energy-efficient.
  • It is independent of the testing environment, and the analysis results are applicable to all running platforms.
  • It can reflect algorithm efficiency at different data volumes, especially algorithm performance at large data volumes.

Tip

If you are still confused about the concept of complexity, don't worry—we will introduce it in detail in subsequent chapters.

Complexity analysis provides us with a \"ruler\" for evaluating algorithm efficiency, allowing us to measure the time and space resources required to execute a certain algorithm and compare the efficiency between different algorithms.

Complexity is a mathematical concept that may be relatively abstract for beginners, with a relatively high learning difficulty. From this perspective, complexity analysis may not be very suitable as the first content to be introduced. However, when we discuss the characteristics of a certain data structure or algorithm, it is difficult to avoid analyzing its running speed and space usage.

In summary, it is recommended that before diving deep into data structures and algorithms, you first establish a preliminary understanding of complexity analysis so that you can complete complexity analysis of simple algorithms.

","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/","level":1,"title":"2.4   Space Complexity","text":"

Space complexity measures the growth trend of memory space occupied by an algorithm as the data size increases. This concept is very similar to time complexity, except that \"running time\" is replaced with \"occupied memory space\".

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#241-algorithm-related-space","level":2,"title":"2.4.1   Algorithm-Related Space","text":"

The memory space used by an algorithm during execution mainly includes the following types.

  • Input space: Used to store the input data of the algorithm.
  • Temporary space: Used to store variables, objects, function contexts, and other data during the algorithm's execution.
  • Output space: Used to store the output data of the algorithm.

In general, the scope of space complexity statistics is \"temporary space\" plus \"output space\".

Temporary space can be further divided into three parts.

  • Temporary data: Used to save various constants, variables, objects, etc., during the algorithm's execution.
  • Stack frame space: Used to save the context data of called functions. The system creates a stack frame at the top of the stack each time a function is called, and the stack frame space is released after the function returns.
  • Instruction space: Used to save compiled program instructions, which are usually ignored in actual statistics.

When analyzing the space complexity of a program, we usually count three parts: temporary data, stack frame space, and output data, as shown in the following figure.

Figure 2-15   Algorithm-related space

The related code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class Node:\n    \"\"\"Class\"\"\"\n    def __init__(self, x: int):\n        self.val: int = x              # Node value\n        self.next: Node | None = None  # Reference to the next node\n\ndef function() -> int:\n    \"\"\"Function\"\"\"\n    # Perform some operations...\n    return 0\n\ndef algorithm(n) -> int:  # Input data\n    A = 0                 # Temporary data (constant, usually represented by uppercase letters)\n    b = 0                 # Temporary data (variable)\n    node = Node(0)        # Temporary data (object)\n    c = function()        # Stack frame space (function call)\n    return A + b + c      # Output data\n
/* Structure */\nstruct Node {\n    int val;\n    Node *next;\n    Node(int x) : val(x), next(nullptr) {}\n};\n\n/* Function */\nint func() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) {        // Input data\n    const int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node* node = new Node(0); // Temporary data (object)\n    int c = func();           // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
/* Class */\nclass Node {\n    int val;\n    Node next;\n    Node(int x) { val = x; }\n}\n\n/* Function */\nint function() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) {        // Input data\n    final int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node node = new Node(0);  // Temporary data (object)\n    int c = function();       // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
/* Class */\nclass Node(int x) {\n    int val = x;\n    Node next;\n}\n\n/* Function */\nint Function() {\n    // Perform some operations...\n    return 0;\n}\n\nint Algorithm(int n) {        // Input data\n    const int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node node = new(0);       // Temporary data (object)\n    int c = Function();       // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
/* Structure */\ntype node struct {\n    val  int\n    next *node\n}\n\n/* Create node structure */\nfunc newNode(val int) *node {\n    return &node{val: val}\n}\n\n/* Function */\nfunc function() int {\n    // Perform some operations...\n    return 0\n}\n\nfunc algorithm(n int) int { // Input data\n    const a = 0             // Temporary data (constant)\n    b := 0                  // Temporary data (variable)\n    newNode(0)              // Temporary data (object)\n    c := function()         // Stack frame space (function call)\n    return a + b + c        // Output data\n}\n
/* Class */\nclass Node {\n    var val: Int\n    var next: Node?\n\n    init(x: Int) {\n        val = x\n    }\n}\n\n/* Function */\nfunc function() -> Int {\n    // Perform some operations...\n    return 0\n}\n\nfunc algorithm(n: Int) -> Int { // Input data\n    let a = 0             // Temporary data (constant)\n    var b = 0             // Temporary data (variable)\n    let node = Node(x: 0) // Temporary data (object)\n    let c = function()    // Stack frame space (function call)\n    return a + b + c      // Output data\n}\n
/* Class */\nclass Node {\n    val;\n    next;\n    constructor(val) {\n        this.val = val === undefined ? 0 : val; // Node value\n        this.next = null;                       // Reference to the next node\n    }\n}\n\n/* Function */\nfunction constFunc() {\n    // Perform some operations\n    return 0;\n}\n\nfunction algorithm(n) {       // Input data\n    const a = 0;              // Temporary data (constant)\n    let b = 0;                // Temporary data (variable)\n    const node = new Node(0); // Temporary data (object)\n    const c = constFunc();    // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
/* Class */\nclass Node {\n    val: number;\n    next: Node | null;\n    constructor(val?: number) {\n        this.val = val === undefined ? 0 : val; // Node value\n        this.next = null;                       // Reference to the next node\n    }\n}\n\n/* Function */\nfunction constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n\nfunction algorithm(n: number): number { // Input data\n    const a = 0;                        // Temporary data (constant)\n    let b = 0;                          // Temporary data (variable)\n    const node = new Node(0);           // Temporary data (object)\n    const c = constFunc();              // Stack frame space (function call)\n    return a + b + c;                   // Output data\n}\n
/* Class */\nclass Node {\n  int val;\n  Node next;\n  Node(this.val, [this.next]);\n}\n\n/* Function */\nint function() {\n  // Perform some operations...\n  return 0;\n}\n\nint algorithm(int n) {  // Input data\n  const int a = 0;      // Temporary data (constant)\n  int b = 0;            // Temporary data (variable)\n  Node node = Node(0);  // Temporary data (object)\n  int c = function();   // Stack frame space (function call)\n  return a + b + c;     // Output data\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Structure */\nstruct Node {\n    val: i32,\n    next: Option<Rc<RefCell<Node>>>,\n}\n\n/* Create Node structure */\nimpl Node {\n    fn new(val: i32) -> Self {\n        Self { val: val, next: None }\n    }\n}\n\n/* Function */\nfn function() -> i32 {\n    // Perform some operations...\n    return 0;\n}\n\nfn algorithm(n: i32) -> i32 {       // Input data\n    const a: i32 = 0;               // Temporary data (constant)\n    let mut b = 0;                  // Temporary data (variable)\n    let node = Node::new(0);        // Temporary data (object)\n    let c = function();             // Stack frame space (function call)\n    return a + b + c;               // Output data\n}\n
/* Function */\nint func() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) { // Input data\n    const int a = 0;   // Temporary data (constant)\n    int b = 0;         // Temporary data (variable)\n    int c = func();    // Stack frame space (function call)\n    return a + b + c;  // Output data\n}\n
/* Class */\nclass Node(var _val: Int) {\n    var next: Node? = null\n}\n\n/* Function */\nfun function(): Int {\n    // Perform some operations...\n    return 0\n}\n\nfun algorithm(n: Int): Int { // Input data\n    val a = 0                // Temporary data (constant)\n    var b = 0                // Temporary data (variable)\n    val node = Node(0)       // Temporary data (object)\n    val c = function()       // Stack frame space (function call)\n    return a + b + c         // Output data\n}\n
### Class ###\nclass Node\n    attr_accessor :val      # Node value\n    attr_accessor :next     # Reference to the next node\n\n    def initialize(x)\n        @val = x\n    end\nend\n\n### Function ###\ndef function\n    # Perform some operations...\n    0\nend\n\n### Algorithm ###\ndef algorithm(n)        # Input data\n    a = 0               # Temporary data (constant)\n    b = 0               # Temporary data (variable)\n    node = Node.new(0)  # Temporary data (object)\n    c = function        # Stack frame space (function call)\n    a + b + c           # Output data\nend\n
","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#242-calculation-method","level":2,"title":"2.4.2   Calculation Method","text":"

The calculation method for space complexity is roughly the same as for time complexity, except that the statistical object is changed from \"number of operations\" to \"size of space used\".

Unlike time complexity, we usually only focus on the worst-case space complexity. This is because memory space is a hard requirement, and we must ensure that sufficient memory space is reserved for all input data.

Observe the following code. The \"worst case\" in worst-case space complexity has two meanings.

  1. Based on the worst input data: When \\(n < 10\\), the space complexity is \\(O(1)\\); but when \\(n > 10\\), the initialized array nums occupies \\(O(n)\\) space, so the worst-case space complexity is \\(O(n)\\).
  2. Based on the peak memory during algorithm execution: For example, before executing the last line, the program occupies \\(O(1)\\) space; when initializing the array nums, the program occupies \\(O(n)\\) space, so the worst-case space complexity is \\(O(n)\\).
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 0               # O(1)\n    b = [0] * 10000     # O(1)\n    if n > 10:\n        nums = [0] * n  # O(n)\n
void algorithm(int n) {\n    int a = 0;               // O(1)\n    vector<int> b(10000);    // O(1)\n    if (n > 10)\n        vector<int> nums(n); // O(n)\n}\n
void algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10)\n        int[] nums = new int[n]; // O(n)\n}\n
void Algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10) {\n        int[] nums = new int[n]; // O(n)\n    }\n}\n
func algorithm(n int) {\n    a := 0                      // O(1)\n    b := make([]int, 10000)     // O(1)\n    var nums []int\n    if n > 10 {\n        nums := make([]int, n)  // O(n)\n    }\n    fmt.Println(a, b, nums)\n}\n
func algorithm(n: Int) {\n    let a = 0 // O(1)\n    let b = Array(repeating: 0, count: 10000) // O(1)\n    if n > 10 {\n        let nums = Array(repeating: 0, count: n) // O(n)\n    }\n}\n
function algorithm(n) {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
function algorithm(n: number): void {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
void algorithm(int n) {\n  int a = 0;                            // O(1)\n  List<int> b = List.filled(10000, 0);  // O(1)\n  if (n > 10) {\n    List<int> nums = List.filled(n, 0); // O(n)\n  }\n}\n
fn algorithm(n: i32) {\n    let a = 0;                              // O(1)\n    let b = [0; 10000];                     // O(1)\n    if n > 10 {\n        let nums = vec![0; n as usize];     // O(n)\n    }\n}\n
void algorithm(int n) {\n    int a = 0;               // O(1)\n    int b[10000];            // O(1)\n    if (n > 10)\n        int nums[n] = {0};   // O(n)\n}\n
fun algorithm(n: Int) {\n    val a = 0                    // O(1)\n    val b = IntArray(10000)      // O(1)\n    if (n > 10) {\n        val nums = IntArray(n)   // O(n)\n    }\n}\n
def algorithm(n)\n    a = 0                           # O(1)\n    b = Array.new(10000)            # O(1)\n    nums = Array.new(n) if n > 10   # O(n)\nend\n

In recursive functions, it is necessary to count the stack frame space. Observe the following code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def function() -> int:\n    # Perform some operations\n    return 0\n\ndef loop(n: int):\n    \"\"\"Loop has space complexity of O(1)\"\"\"\n    for _ in range(n):\n        function()\n\ndef recur(n: int):\n    \"\"\"Recursion has space complexity of O(n)\"\"\"\n    if n == 1:\n        return\n    return recur(n - 1)\n
int func() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
int function() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
int Function() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid Loop(int n) {\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nint Recur(int n) {\n    if (n == 1) return 1;\n    return Recur(n - 1);\n}\n
func function() int {\n    // Perform some operations\n    return 0\n}\n\n/* Loop has space complexity of O(1) */\nfunc loop(n int) {\n    for i := 0; i < n; i++ {\n        function()\n    }\n}\n\n/* Recursion has space complexity of O(n) */\nfunc recur(n int) {\n    if n == 1 {\n        return\n    }\n    recur(n - 1)\n}\n
@discardableResult\nfunc function() -> Int {\n    // Perform some operations\n    return 0\n}\n\n/* Loop has space complexity of O(1) */\nfunc loop(n: Int) {\n    for _ in 0 ..< n {\n        function()\n    }\n}\n\n/* Recursion has space complexity of O(n) */\nfunc recur(n: Int) {\n    if n == 1 {\n        return\n    }\n    recur(n: n - 1)\n}\n
function constFunc() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfunction loop(n) {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfunction recur(n) {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
function constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfunction loop(n: number): void {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfunction recur(n: number): void {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
int function() {\n  // Perform some operations\n  return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n  for (int i = 0; i < n; i++) {\n    function();\n  }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n  if (n == 1) return;\n  recur(n - 1);\n}\n
fn function() -> i32 {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfn loop(n: i32) {\n    for i in 0..n {\n        function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfn recur(n: i32) {\n    if n == 1 {\n        return;\n    }\n    recur(n - 1);\n}\n
int func() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
fun function(): Int {\n    // Perform some operations\n    return 0\n}\n/* Loop has space complexity of O(1) */\nfun loop(n: Int) {\n    for (i in 0..<n) {\n        function()\n    }\n}\n/* Recursion has space complexity of O(n) */\nfun recur(n: Int) {\n    if (n == 1) return\n    return recur(n - 1)\n}\n
def function\n    # Perform some operations\n    0\nend\n\n### Loop has space complexity of O(1) ###\ndef loop(n)\n    (0...n).each { function }\nend\n\n### Recursion has space complexity of O(n) ###\ndef recur(n)\n    return if n == 1\n    recur(n - 1)\nend\n

The time complexity of both functions loop() and recur() is \\(O(n)\\), but their space complexities are different.

  • The function loop() calls function() \\(n\\) times in a loop. In each iteration, function() returns and releases its stack frame space, so the space complexity remains \\(O(1)\\).
  • The recursive function recur() has \\(n\\) unreturned recur() instances existing simultaneously during execution, thus occupying \\(O(n)\\) stack frame space.
","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243-common-types","level":2,"title":"2.4.3   Common Types","text":"

Let the input data size be \\(n\\). The following figure shows common types of space complexity (arranged from low to high).

\\[ \\begin{aligned} O(1) < O(\\log n) < O(n) < O(n^2) < O(2^n) \\newline \\text{Constant} < \\text{Logarithmic} < \\text{Linear} < \\text{Quadratic} < \\text{Exponential} \\end{aligned} \\]

Figure 2-16   Common types of space complexity

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#1-constant-order-o1","level":3,"title":"1.   Constant Order \\(O(1)\\)","text":"

Constant order is common in constants, variables, and objects whose quantity is independent of the input data size \\(n\\).

It should be noted that memory occupied by initializing variables or calling functions in a loop is released when entering the next iteration, so it does not accumulate space, and the space complexity remains \\(O(1)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def function() -> int:\n    \"\"\"Function\"\"\"\n    # Perform some operations\n    return 0\n\ndef constant(n: int):\n    \"\"\"Constant order\"\"\"\n    # Constants, variables, objects occupy O(1) space\n    a = 0\n    nums = [0] * 10000\n    node = ListNode(0)\n    # Variables in the loop occupy O(1) space\n    for _ in range(n):\n        c = 0\n    # Functions in the loop occupy O(1) space\n    for _ in range(n):\n        function()\n
space_complexity.cpp
/* Function */\nint func() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    const int a = 0;\n    int b = 0;\n    vector<int> nums(10000);\n    ListNode node(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
space_complexity.java
/* Function */\nint function() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    final int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n
space_complexity.cs
/* Function */\nint Function() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid Constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n
space_complexity.go
/* Function */\nfunc function() int {\n    // Perform some operations...\n    return 0\n}\n\n/* Constant order */\nfunc spaceConstant(n int) {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0\n    b := 0\n    nums := make([]int, 10000)\n    node := newNode(0)\n    // Variables in the loop occupy O(1) space\n    var c int\n    for i := 0; i < n; i++ {\n        c = 0\n    }\n    // Functions in the loop occupy O(1) space\n    for i := 0; i < n; i++ {\n        function()\n    }\n    b += 0\n    c += 0\n    nums[0] = 0\n    node.val = 0\n}\n
space_complexity.swift
/* Function */\n@discardableResult\nfunc function() -> Int {\n    // Perform some operations\n    return 0\n}\n\n/* Constant order */\nfunc constant(n: Int) {\n    // Constants, variables, objects occupy O(1) space\n    let a = 0\n    var b = 0\n    let nums = Array(repeating: 0, count: 10000)\n    let node = ListNode(x: 0)\n    // Variables in the loop occupy O(1) space\n    for _ in 0 ..< n {\n        let c = 0\n    }\n    // Functions in the loop occupy O(1) space\n    for _ in 0 ..< n {\n        function()\n    }\n}\n
space_complexity.js
/* Function */\nfunction constFunc() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nfunction constant(n) {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
space_complexity.ts
/* Function */\nfunction constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nfunction constant(n: number): void {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
space_complexity.dart
/* Function */\nint function() {\n  // Perform some operations\n  return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n  // Constants, variables, objects occupy O(1) space\n  final int a = 0;\n  int b = 0;\n  List<int> nums = List.filled(10000, 0);\n  ListNode node = ListNode(0);\n  // Variables in the loop occupy O(1) space\n  for (var i = 0; i < n; i++) {\n    int c = 0;\n  }\n  // Functions in the loop occupy O(1) space\n  for (var i = 0; i < n; i++) {\n    function();\n  }\n}\n
space_complexity.rs
/* Function */\nfn function() -> i32 {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\n#[allow(unused)]\nfn constant(n: i32) {\n    // Constants, variables, objects occupy O(1) space\n    const A: i32 = 0;\n    let b = 0;\n    let nums = vec![0; 10000];\n    let node = ListNode::new(0);\n    // Variables in the loop occupy O(1) space\n    for i in 0..n {\n        let c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for i in 0..n {\n        function();\n    }\n}\n
space_complexity.c
/* Function */\nint func() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    const int a = 0;\n    int b = 0;\n    int nums[1000];\n    ListNode *node = newListNode(0);\n    free(node);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
space_complexity.kt
/* Function */\nfun function(): Int {\n    // Perform some operations\n    return 0\n}\n\n/* Constant order */\nfun constant(n: Int) {\n    // Constants, variables, objects occupy O(1) space\n    val a = 0\n    var b = 0\n    val nums = Array(10000) { 0 }\n    val node = ListNode(0)\n    // Variables in the loop occupy O(1) space\n    for (i in 0..<n) {\n        val c = 0\n    }\n    // Functions in the loop occupy O(1) space\n    for (i in 0..<n) {\n        function()\n    }\n}\n
space_complexity.rb
### Function ###\ndef function\n  # Perform some operations\n  0\nend\n\n### Constant time ###\ndef constant(n)\n  # Constants, variables, objects occupy O(1) space\n  a = 0\n  nums = [0] * 10000\n  node = ListNode.new\n\n  # Variables in the loop occupy O(1) space\n  (0...n).each { c = 0 }\n  # Functions in the loop occupy O(1) space\n  (0...n).each { function }\nend\n
","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#2-linear-order-on","level":3,"title":"2.   Linear Order \\(O(n)\\)","text":"

Linear order is common in arrays, linked lists, stacks, queues, etc., where the number of elements is proportional to \\(n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def linear(n: int):\n    \"\"\"Linear order\"\"\"\n    # A list of length n occupies O(n) space\n    nums = [0] * n\n    # A hash table of length n occupies O(n) space\n    hmap = dict[int, str]()\n    for i in range(n):\n        hmap[i] = str(i)\n
space_complexity.cpp
/* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    vector<int> nums(n);\n    // A list of length n occupies O(n) space\n    vector<ListNode> nodes;\n    for (int i = 0; i < n; i++) {\n        nodes.push_back(ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\n    unordered_map<int, string> map;\n    for (int i = 0; i < n; i++) {\n        map[i] = to_string(i);\n    }\n}\n
space_complexity.java
/* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    int[] nums = new int[n];\n    // A list of length n occupies O(n) space\n    List<ListNode> nodes = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        nodes.add(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\n    Map<Integer, String> map = new HashMap<>();\n    for (int i = 0; i < n; i++) {\n        map.put(i, String.valueOf(i));\n    }\n}\n
space_complexity.cs
/* Linear order */\nvoid Linear(int n) {\n    // Array of length n uses O(n) space\n    int[] nums = new int[n];\n    // A list of length n occupies O(n) space\n    List<ListNode> nodes = [];\n    for (int i = 0; i < n; i++) {\n        nodes.Add(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\n    Dictionary<int, string> map = [];\n    for (int i = 0; i < n; i++) {\n        map.Add(i, i.ToString());\n    }\n}\n
space_complexity.go
/* Linear order */\nfunc spaceLinear(n int) {\n    // Array of length n uses O(n) space\n    _ = make([]int, n)\n    // A list of length n occupies O(n) space\n    var nodes []*node\n    for i := 0; i < n; i++ {\n        nodes = append(nodes, newNode(i))\n    }\n    // A hash table of length n occupies O(n) space\n    m := make(map[int]string, n)\n    for i := 0; i < n; i++ {\n        m[i] = strconv.Itoa(i)\n    }\n}\n
space_complexity.swift
/* Linear order */\nfunc linear(n: Int) {\n    // Array of length n uses O(n) space\n    let nums = Array(repeating: 0, count: n)\n    // A list of length n occupies O(n) space\n    let nodes = (0 ..< n).map { ListNode(x: $0) }\n    // A hash table of length n occupies O(n) space\n    let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, \"\\($0)\") })\n}\n
space_complexity.js
/* Linear order */\nfunction linear(n) {\n    // Array of length n uses O(n) space\n    const nums = new Array(n);\n    // A list of length n occupies O(n) space\n    const nodes = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
space_complexity.ts
/* Linear order */\nfunction linear(n: number): void {\n    // Array of length n uses O(n) space\n    const nums = new Array(n);\n    // A list of length n occupies O(n) space\n    const nodes: ListNode[] = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
space_complexity.dart
/* Linear order */\nvoid linear(int n) {\n  // Array of length n uses O(n) space\n  List<int> nums = List.filled(n, 0);\n  // A list of length n occupies O(n) space\n  List<ListNode> nodes = [];\n  for (var i = 0; i < n; i++) {\n    nodes.add(ListNode(i));\n  }\n  // A hash table of length n occupies O(n) space\n  Map<int, String> map = HashMap();\n  for (var i = 0; i < n; i++) {\n    map.putIfAbsent(i, () => i.toString());\n  }\n}\n
space_complexity.rs
/* Linear order */\n#[allow(unused)]\nfn linear(n: i32) {\n    // Array of length n uses O(n) space\n    let mut nums = vec![0; n as usize];\n    // A list of length n occupies O(n) space\n    let mut nodes = Vec::new();\n    for i in 0..n {\n        nodes.push(ListNode::new(i))\n    }\n    // A hash table of length n occupies O(n) space\n    let mut map = HashMap::new();\n    for i in 0..n {\n        map.insert(i, i.to_string());\n    }\n}\n
space_complexity.c
/* Hash table */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Implemented using uthash.h\n} HashTable;\n\n/* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    int *nums = malloc(sizeof(int) * n);\n    free(nums);\n\n    // A list of length n occupies O(n) space\n    ListNode **nodes = malloc(sizeof(ListNode *) * n);\n    for (int i = 0; i < n; i++) {\n        nodes[i] = newListNode(i);\n    }\n    // Memory release\n    for (int i = 0; i < n; i++) {\n        free(nodes[i]);\n    }\n    free(nodes);\n\n    // A hash table of length n occupies O(n) space\n    HashTable *h = NULL;\n    for (int i = 0; i < n; i++) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = i;\n        tmp->val = i;\n        HASH_ADD_INT(h, key, tmp);\n    }\n\n    // Memory release\n    HashTable *curr, *tmp;\n    HASH_ITER(hh, h, curr, tmp) {\n        HASH_DEL(h, curr);\n        free(curr);\n    }\n}\n
space_complexity.kt
/* Linear order */\nfun linear(n: Int) {\n    // Array of length n uses O(n) space\n    val nums = Array(n) { 0 }\n    // A list of length n occupies O(n) space\n    val nodes = mutableListOf<ListNode>()\n    for (i in 0..<n) {\n        nodes.add(ListNode(i))\n    }\n    // A hash table of length n occupies O(n) space\n    val map = mutableMapOf<Int, String>()\n    for (i in 0..<n) {\n        map[i] = i.toString()\n    }\n}\n
space_complexity.rb
### Linear time ###\ndef linear(n)\n  # A list of length n occupies O(n) space\n  nums = Array.new(n, 0)\n\n  # A hash table of length n occupies O(n) space\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n

As shown in the following figure, the recursion depth of this function is \\(n\\), meaning that there are \\(n\\) unreturned linear_recur() functions existing simultaneously, using \\(O(n)\\) stack frame space:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def linear_recur(n: int):\n    \"\"\"Linear order (recursive implementation)\"\"\"\n    print(\"Recursion n =\", n)\n    if n == 1:\n        return\n    linear_recur(n - 1)\n
space_complexity.cpp
/* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    cout << \"Recursion n = \" << n << endl;\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.java
/* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    System.out.println(\"Recursion n = \" + n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.cs
/* Linear order (recursive implementation) */\nvoid LinearRecur(int n) {\n    Console.WriteLine(\"Recursion n = \" + n);\n    if (n == 1) return;\n    LinearRecur(n - 1);\n}\n
space_complexity.go
/* Linear order (recursive implementation) */\nfunc spaceLinearRecur(n int) {\n    fmt.Println(\"Recursion n =\", n)\n    if n == 1 {\n        return\n    }\n    spaceLinearRecur(n - 1)\n}\n
space_complexity.swift
/* Linear order (recursive implementation) */\nfunc linearRecur(n: Int) {\n    print(\"Recursion n = \\(n)\")\n    if n == 1 {\n        return\n    }\n    linearRecur(n: n - 1)\n}\n
space_complexity.js
/* Linear order (recursive implementation) */\nfunction linearRecur(n) {\n    console.log(`Recursion n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
space_complexity.ts
/* Linear order (recursive implementation) */\nfunction linearRecur(n: number): void {\n    console.log(`Recursion n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
space_complexity.dart
/* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n  print('Recursion n = $n');\n  if (n == 1) return;\n  linearRecur(n - 1);\n}\n
space_complexity.rs
/* Linear order (recursive implementation) */\nfn linear_recur(n: i32) {\n    println!(\"Recursion n = {}\", n);\n    if n == 1 {\n        return;\n    };\n    linear_recur(n - 1);\n}\n
space_complexity.c
/* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    printf(\"Recursion n = %d\\r\\n\", n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.kt
/* Linear order (recursive implementation) */\nfun linearRecur(n: Int) {\n    println(\"Recursion n = $n\")\n    if (n == 1)\n        return\n    linearRecur(n - 1)\n}\n
space_complexity.rb
### Linear space (recursive) ###\ndef linear_recur(n)\n  puts \"Recursion n = #{n}\"\n  return if n == 1\n  linear_recur(n - 1)\nend\n

Figure 2-17   Linear order space complexity generated by recursive function

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#3-quadratic-order-on2","level":3,"title":"3.   Quadratic Order \\(O(n^2)\\)","text":"

Quadratic order is common in matrices and graphs, where the number of elements is quadratically related to \\(n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def quadratic(n: int):\n    \"\"\"Quadratic order\"\"\"\n    # A 2D list occupies O(n^2) space\n    num_matrix = [[0] * n for _ in range(n)]\n
space_complexity.cpp
/* Exponential order */\nvoid quadratic(int n) {\n    // 2D list uses O(n^2) space\n    vector<vector<int>> numMatrix;\n    for (int i = 0; i < n; i++) {\n        vector<int> tmp;\n        for (int j = 0; j < n; j++) {\n            tmp.push_back(0);\n        }\n        numMatrix.push_back(tmp);\n    }\n}\n
space_complexity.java
/* Exponential order */\nvoid quadratic(int n) {\n    // Matrix uses O(n^2) space\n    int[][] numMatrix = new int[n][n];\n    // 2D list uses O(n^2) space\n    List<List<Integer>> numList = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<Integer> tmp = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            tmp.add(0);\n        }\n        numList.add(tmp);\n    }\n}\n
space_complexity.cs
/* Exponential order */\nvoid Quadratic(int n) {\n    // Matrix uses O(n^2) space\n    int[,] numMatrix = new int[n, n];\n    // 2D list uses O(n^2) space\n    List<List<int>> numList = [];\n    for (int i = 0; i < n; i++) {\n        List<int> tmp = [];\n        for (int j = 0; j < n; j++) {\n            tmp.Add(0);\n        }\n        numList.Add(tmp);\n    }\n}\n
space_complexity.go
/* Exponential order */\nfunc spaceQuadratic(n int) {\n    // Matrix uses O(n^2) space\n    numMatrix := make([][]int, n)\n    for i := 0; i < n; i++ {\n        numMatrix[i] = make([]int, n)\n    }\n}\n
space_complexity.swift
/* Exponential order */\nfunc quadratic(n: Int) {\n    // 2D list uses O(n^2) space\n    let numList = Array(repeating: Array(repeating: 0, count: n), count: n)\n}\n
space_complexity.js
/* Exponential order */\nfunction quadratic(n) {\n    // Matrix uses O(n^2) space\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 2D list uses O(n^2) space\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
space_complexity.ts
/* Exponential order */\nfunction quadratic(n: number): void {\n    // Matrix uses O(n^2) space\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 2D list uses O(n^2) space\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
space_complexity.dart
/* Exponential order */\nvoid quadratic(int n) {\n  // Matrix uses O(n^2) space\n  List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));\n  // 2D list uses O(n^2) space\n  List<List<int>> numList = [];\n  for (var i = 0; i < n; i++) {\n    List<int> tmp = [];\n    for (int j = 0; j < n; j++) {\n      tmp.add(0);\n    }\n    numList.add(tmp);\n  }\n}\n
space_complexity.rs
/* Exponential order */\n#[allow(unused)]\nfn quadratic(n: i32) {\n    // Matrix uses O(n^2) space\n    let num_matrix = vec![vec![0; n as usize]; n as usize];\n    // 2D list uses O(n^2) space\n    let mut num_list = Vec::new();\n    for i in 0..n {\n        let mut tmp = Vec::new();\n        for j in 0..n {\n            tmp.push(0);\n        }\n        num_list.push(tmp);\n    }\n}\n
space_complexity.c
/* Exponential order */\nvoid quadratic(int n) {\n    // 2D list uses O(n^2) space\n    int **numMatrix = malloc(sizeof(int *) * n);\n    for (int i = 0; i < n; i++) {\n        int *tmp = malloc(sizeof(int) * n);\n        for (int j = 0; j < n; j++) {\n            tmp[j] = 0;\n        }\n        numMatrix[i] = tmp;\n    }\n\n    // Memory release\n    for (int i = 0; i < n; i++) {\n        free(numMatrix[i]);\n    }\n    free(numMatrix);\n}\n
space_complexity.kt
/* Exponential order */\nfun quadratic(n: Int) {\n    // Matrix uses O(n^2) space\n    val numMatrix = arrayOfNulls<Array<Int>?>(n)\n    // 2D list uses O(n^2) space\n    val numList = mutableListOf<MutableList<Int>>()\n    for (i in 0..<n) {\n        val tmp = mutableListOf<Int>()\n        for (j in 0..<n) {\n            tmp.add(0)\n        }\n        numList.add(tmp)\n    }\n}\n
space_complexity.rb
### Quadratic time ###\ndef quadratic(n)\n  # 2D list uses O(n^2) space\n  Array.new(n) { Array.new(n, 0) }\nend\n

As shown in the following figure, the recursion depth of this function is \\(n\\), and an array is initialized in each recursive function with lengths of \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\), with an average length of \\(n / 2\\), thus occupying \\(O(n^2)\\) space overall:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def quadratic_recur(n: int) -> int:\n    \"\"\"Quadratic order (recursive implementation)\"\"\"\n    if n <= 0:\n        return 0\n    # Array nums length is n, n-1, ..., 2, 1\n    nums = [0] * n\n    return quadratic_recur(n - 1)\n
space_complexity.cpp
/* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    vector<int> nums(n);\n    cout << \"In recursion n = \" << n << \", nums length = \" << nums.size() << endl;\n    return quadraticRecur(n - 1);\n}\n
space_complexity.java
/* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    // Array nums has length n, n-1, ..., 2, 1\n    int[] nums = new int[n];\n    System.out.println(\"In recursion n = \" + n + \", nums length = \" + nums.length);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.cs
/* Quadratic order (recursive implementation) */\nint QuadraticRecur(int n) {\n    if (n <= 0) return 0;\n    int[] nums = new int[n];\n    Console.WriteLine(\"Recursion n = \" + n + \", nums length = \" + nums.Length);\n    return QuadraticRecur(n - 1);\n}\n
space_complexity.go
/* Quadratic order (recursive implementation) */\nfunc spaceQuadraticRecur(n int) int {\n    if n <= 0 {\n        return 0\n    }\n    nums := make([]int, n)\n    fmt.Printf(\"In recursion n = %d, nums length = %d \\n\", n, len(nums))\n    return spaceQuadraticRecur(n - 1)\n}\n
space_complexity.swift
/* Quadratic order (recursive implementation) */\n@discardableResult\nfunc quadraticRecur(n: Int) -> Int {\n    if n <= 0 {\n        return 0\n    }\n    // Array nums has length n, n-1, ..., 2, 1\n    let nums = Array(repeating: 0, count: n)\n    print(\"In recursion n = \\(n), nums length = \\(nums.count)\")\n    return quadraticRecur(n: n - 1)\n}\n
space_complexity.js
/* Quadratic order (recursive implementation) */\nfunction quadraticRecur(n) {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`In recursion n = ${n}, nums length = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.ts
/* Quadratic order (recursive implementation) */\nfunction quadraticRecur(n: number): number {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`In recursion n = ${n}, nums length = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.dart
/* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n  if (n <= 0) return 0;\n  List<int> nums = List.filled(n, 0);\n  print('In recursion n = $n, nums length = ${nums.length}');\n  return quadraticRecur(n - 1);\n}\n
space_complexity.rs
/* Quadratic order (recursive implementation) */\nfn quadratic_recur(n: i32) -> i32 {\n    if n <= 0 {\n        return 0;\n    };\n    // Array nums has length n, n-1, ..., 2, 1\n    let nums = vec![0; n as usize];\n    println!(\"In recursion n = {}, nums length = {}\", n, nums.len());\n    return quadratic_recur(n - 1);\n}\n
space_complexity.c
/* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    int *nums = malloc(sizeof(int) * n);\n    printf(\"In recursion n = %d, nums length = %d\\r\\n\", n, n);\n    int res = quadraticRecur(n - 1);\n    free(nums);\n    return res;\n}\n
space_complexity.kt
/* Quadratic order (recursive implementation) */\ntailrec fun quadraticRecur(n: Int): Int {\n    if (n <= 0)\n        return 0\n    // Array nums has length n, n-1, ..., 2, 1\n    val nums = Array(n) { 0 }\n    println(\"In recursion n = $n, nums length = ${nums.size}\")\n    return quadraticRecur(n - 1)\n}\n
space_complexity.rb
### Quadratic space (recursive) ###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # Array nums has length n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n

Figure 2-18   Quadratic order space complexity generated by recursive function

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#4-exponential-order-o2n","level":3,"title":"4.   Exponential Order \\(O(2^n)\\)","text":"

Exponential order is common in binary trees. Observe the following figure: a \"full binary tree\" with \\(n\\) levels has \\(2^n - 1\\) nodes, occupying \\(O(2^n)\\) space:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def build_tree(n: int) -> TreeNode | None:\n    \"\"\"Exponential order (build full binary tree)\"\"\"\n    if n == 0:\n        return None\n    root = TreeNode(0)\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n    return root\n
space_complexity.cpp
/* Driver Code */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return nullptr;\n    TreeNode *root = new TreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.java
/* Driver Code */\nTreeNode buildTree(int n) {\n    if (n == 0)\n        return null;\n    TreeNode root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.cs
/* Driver Code */\nTreeNode? BuildTree(int n) {\n    if (n == 0) return null;\n    TreeNode root = new(0) {\n        left = BuildTree(n - 1),\n        right = BuildTree(n - 1)\n    };\n    return root;\n}\n
space_complexity.go
/* Driver Code */\nfunc buildTree(n int) *TreeNode {\n    if n == 0 {\n        return nil\n    }\n    root := NewTreeNode(0)\n    root.Left = buildTree(n - 1)\n    root.Right = buildTree(n - 1)\n    return root\n}\n
space_complexity.swift
/* Driver Code */\nfunc buildTree(n: Int) -> TreeNode? {\n    if n == 0 {\n        return nil\n    }\n    let root = TreeNode(x: 0)\n    root.left = buildTree(n: n - 1)\n    root.right = buildTree(n: n - 1)\n    return root\n}\n
space_complexity.js
/* Driver Code */\nfunction buildTree(n) {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.ts
/* Driver Code */\nfunction buildTree(n: number): TreeNode | null {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.dart
/* Driver Code */\nTreeNode? buildTree(int n) {\n  if (n == 0) return null;\n  TreeNode root = TreeNode(0);\n  root.left = buildTree(n - 1);\n  root.right = buildTree(n - 1);\n  return root;\n}\n
space_complexity.rs
/* Driver Code */\nfn build_tree(n: i32) -> Option<Rc<RefCell<TreeNode>>> {\n    if n == 0 {\n        return None;\n    };\n    let root = TreeNode::new(0);\n    root.borrow_mut().left = build_tree(n - 1);\n    root.borrow_mut().right = build_tree(n - 1);\n    return Some(root);\n}\n
space_complexity.c
/* Driver Code */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return NULL;\n    TreeNode *root = newTreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.kt
/* Driver Code */\nfun buildTree(n: Int): TreeNode? {\n    if (n == 0)\n        return null\n    val root = TreeNode(0)\n    root.left = buildTree(n - 1)\n    root.right = buildTree(n - 1)\n    return root\n}\n
space_complexity.rb
### Exponential space (build full binary tree) ###\ndef build_tree(n)\n  return if n == 0\n\n  TreeNode.new.tap do |root|\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n  end\nend\n

Figure 2-19   Exponential order space complexity generated by full binary tree

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#5-logarithmic-order-olog-n","level":3,"title":"5.   Logarithmic Order \\(O(\\log n)\\)","text":"

Logarithmic order is common in divide-and-conquer algorithms. For example, merge sort: given an input array of length \\(n\\), each recursion divides the array in half from the midpoint, forming a recursion tree of height \\(\\log n\\), using \\(O(\\log n)\\) stack frame space.

Another example is converting a number to a string. Given a positive integer \\(n\\), it has \\(\\lfloor \\log_{10} n \\rfloor + 1\\) digits, i.e., the corresponding string length is \\(\\lfloor \\log_{10} n \\rfloor + 1\\), so the space complexity is \\(O(\\log_{10} n + 1) = O(\\log n)\\).

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#244-trading-time-for-space","level":2,"title":"2.4.4   Trading Time for Space","text":"

Ideally, we hope that both the time complexity and space complexity of an algorithm can reach optimal. However, in practice, optimizing both time complexity and space complexity simultaneously is usually very difficult.

Reducing time complexity usually comes at the cost of increasing space complexity, and vice versa. The approach of sacrificing memory space to improve algorithm execution speed is called \"trading space for time\"; conversely, it is called \"trading time for space\".

The choice of which approach depends on which aspect we value more. In most cases, time is more precious than space, so \"trading space for time\" is usually the more common strategy. Of course, when the data volume is very large, controlling space complexity is also very important.

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/summary/","level":1,"title":"2.5   Summary","text":"","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"

Algorithm Efficiency Assessment

  • Time efficiency and space efficiency are the two primary evaluation metrics for measuring algorithm performance.
  • We can evaluate algorithm efficiency through actual testing, but it is difficult to eliminate the influence of the testing environment, and it consumes substantial computational resources.
  • Complexity analysis can eliminate the drawbacks of actual testing, with results applicable to all running platforms, and it can reveal algorithm efficiency under different data scales.

Time Complexity

  • Time complexity is used to measure the trend of algorithm runtime as data volume increases. It can effectively evaluate algorithm efficiency, but may fail in certain situations, such as when the input data volume is small or when time complexities are identical, making it impossible to precisely compare algorithm efficiency.
  • Worst-case time complexity is represented using Big \\(O\\) notation, corresponding to the asymptotic upper bound of a function, reflecting the growth level of the number of operations \\(T(n)\\) as \\(n\\) approaches positive infinity.
  • Deriving time complexity involves two steps: first, counting the number of operations, then determining the asymptotic upper bound.
  • Common time complexities arranged from low to high include \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n \\log n)\\), \\(O(n^2)\\), \\(O(2^n)\\), and \\(O(n!)\\).
  • The time complexity of some algorithms is not fixed, but rather depends on the distribution of input data. Time complexity is divided into worst-case, best-case, and average-case time complexity. Best-case time complexity is rarely used because input data generally needs to satisfy strict conditions to achieve the best case.
  • Average time complexity reflects the algorithm's runtime efficiency under random data input, and is closest to the algorithm's performance in practical applications. Calculating average time complexity requires statistical analysis of input data distribution and the combined mathematical expectation.

Space Complexity

  • Space complexity serves a similar purpose to time complexity, used to measure the trend of algorithm memory usage as data volume increases.
  • The memory space related to algorithm execution can be divided into input space, temporary space, and output space. Typically, input space is not included in space complexity calculations. Temporary space can be divided into temporary data, stack frame space, and instruction space, where stack frame space usually affects space complexity only in recursive functions.
  • We typically only focus on worst-case space complexity, which is the space complexity of an algorithm under worst-case input data and worst-case runtime.
  • Common space complexities arranged from low to high include \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n^2)\\), and \\(O(2^n)\\).
","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Is the space complexity of tail recursion \\(O(1)\\)?

Theoretically, the space complexity of tail recursive functions can be optimized to \\(O(1)\\). However, most programming languages (such as Java, Python, C++, Go, C#, etc.) do not support automatic tail recursion optimization, so the space complexity is generally considered to be \\(O(n)\\).

Q: What is the difference between the terms function and method?

A function can be executed independently, with all parameters passed explicitly. A method is associated with an object, is implicitly passed to the object that invokes it, and can operate on data contained in class instances.

The following examples use several common programming languages for illustration.

  • C is a procedural programming language without object-oriented concepts, so it only has functions. However, we can simulate object-oriented programming by creating structures (struct), and functions associated with structures are equivalent to methods in other programming languages.
  • Java and C# are object-oriented programming languages where code blocks (methods) are typically part of a class. Static methods behave like functions because they are bound to the class and cannot access specific instance variables.
  • C++ and Python support both procedural programming (functions) and object-oriented programming (methods).

Q: Does the diagram for \"common space complexity types\" reflect the absolute size of occupied space?

No, the diagram shows space complexity, which reflects growth trends rather than the absolute size of occupied space.

Assuming \\(n = 8\\), you might find that the values of each curve do not correspond to the functions. This is because each curve contains a constant term used to compress the value range into a visually comfortable range.

In practice, because we generally do not know what the \"constant term\" complexity of each method is, we usually cannot select the optimal solution for \\(n = 8\\) based on complexity alone. But for \\(n = 8^5\\), the choice is straightforward, as the growth trend already dominates.

Q: Are there situations where algorithms are designed to sacrifice time (or space) based on actual use cases?

In practical applications, most situations choose to sacrifice space for time. For example, with database indexes, we typically choose to build B+ trees or hash indexes, occupying substantial memory space in exchange for efficient queries of \\(O(\\log n)\\) or even \\(O(1)\\).

In scenarios where space resources are precious, time may be sacrificed for space. For example, in embedded development, device memory is precious, and engineers may forgo using hash tables and choose to use array sequential search to save memory usage, at the cost of slower searches.

","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   Time Complexity","text":"

Runtime can intuitively and accurately reflect the efficiency of an algorithm. If we want to accurately estimate the runtime of a piece of code, how should we proceed?

  1. Determine the running platform, including hardware configuration, programming language, system environment, etc., as these factors all affect code execution efficiency.
  2. Evaluate the runtime required for various computational operations, for example, an addition operation + requires 1 ns, a multiplication operation * requires 10 ns, a print operation print() requires 5 ns, etc.
  3. Count all computational operations in the code, and sum the execution times of all operations to obtain the runtime.

For example, in the following code, the input data size is \\(n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# On a certain running platform\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # Loop n times\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
// On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
// On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
// On a certain running platform\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
// On a certain running platform\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // Loop n times\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
// On a certain running platform\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // Loop n times\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
// On a certain running platform\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // Loop n times\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
// On a certain running platform\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // Loop n times\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
// On a certain running platform\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // Loop n times\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
// On a certain running platform\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // Loop n times\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
// On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
// On a certain running platform\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // Loop n times\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
# On a certain running platform\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # Loop n times\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

According to the above method, the algorithm's runtime can be obtained as \\((6n + 12)\\) ns:

\\[ 1 + 1 + 10 + (1 + 5) \\times n = 6n + 12 \\]

In reality, however, counting an algorithm's runtime is neither reasonable nor realistic. First, we do not want to tie the estimated time to the running platform, because algorithms need to run on various different platforms. Second, it is difficult to know the runtime of each type of operation, which brings great difficulty to the estimation process.

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#231-counting-time-growth-trends","level":2,"title":"2.3.1   Counting Time Growth Trends","text":"

Time complexity analysis does not count the algorithm's runtime, but rather counts the growth trend of the algorithm's runtime as the data volume increases.

The concept of \"time growth trend\" is rather abstract; let us understand it through an example. Suppose the input data size is \\(n\\), and given three algorithms A, B, and C:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# Time complexity of algorithm A: constant order\ndef algorithm_A(n: int):\n    print(0)\n# Time complexity of algorithm B: linear order\ndef algorithm_B(n: int):\n    for _ in range(n):\n        print(0)\n# Time complexity of algorithm C: constant order\ndef algorithm_C(n: int):\n    for _ in range(1000000):\n        print(0)\n
// Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    cout << 0 << endl;\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        cout << 0 << endl;\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        cout << 0 << endl;\n    }\n}\n
// Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    System.out.println(0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        System.out.println(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        System.out.println(0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nvoid AlgorithmA(int n) {\n    Console.WriteLine(0);\n}\n// Time complexity of algorithm B: linear order\nvoid AlgorithmB(int n) {\n    for (int i = 0; i < n; i++) {\n        Console.WriteLine(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid AlgorithmC(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        Console.WriteLine(0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nfunc algorithm_A(n int) {\n    fmt.Println(0)\n}\n// Time complexity of algorithm B: linear order\nfunc algorithm_B(n int) {\n    for i := 0; i < n; i++ {\n        fmt.Println(0)\n    }\n}\n// Time complexity of algorithm C: constant order\nfunc algorithm_C(n int) {\n    for i := 0; i < 1000000; i++ {\n        fmt.Println(0)\n    }\n}\n
// Time complexity of algorithm A: constant order\nfunc algorithmA(n: Int) {\n    print(0)\n}\n\n// Time complexity of algorithm B: linear order\nfunc algorithmB(n: Int) {\n    for _ in 0 ..< n {\n        print(0)\n    }\n}\n\n// Time complexity of algorithm C: constant order\nfunc algorithmC(n: Int) {\n    for _ in 0 ..< 1_000_000 {\n        print(0)\n    }\n}\n
// Time complexity of algorithm A: constant order\nfunction algorithm_A(n) {\n    console.log(0);\n}\n// Time complexity of algorithm B: linear order\nfunction algorithm_B(n) {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfunction algorithm_C(n) {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nfunction algorithm_A(n: number): void {\n    console.log(0);\n}\n// Time complexity of algorithm B: linear order\nfunction algorithm_B(n: number): void {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfunction algorithm_C(n: number): void {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nvoid algorithmA(int n) {\n  print(0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithmB(int n) {\n  for (int i = 0; i < n; i++) {\n    print(0);\n  }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithmC(int n) {\n  for (int i = 0; i < 1000000; i++) {\n    print(0);\n  }\n}\n
// Time complexity of algorithm A: constant order\nfn algorithm_A(n: i32) {\n    println!(\"{}\", 0);\n}\n// Time complexity of algorithm B: linear order\nfn algorithm_B(n: i32) {\n    for _ in 0..n {\n        println!(\"{}\", 0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfn algorithm_C(n: i32) {\n    for _ in 0..1000000 {\n        println!(\"{}\", 0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    printf(\"%d\", 0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        printf(\"%d\", 0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        printf(\"%d\", 0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nfun algoritm_A(n: Int) {\n    println(0)\n}\n// Time complexity of algorithm B: linear order\nfun algorithm_B(n: Int) {\n    for (i in 0..<n){\n        println(0)\n    }\n}\n// Time complexity of algorithm C: constant order\nfun algorithm_C(n: Int) {\n    for (i in 0..<1000000) {\n        println(0)\n    }\n}\n
# Time complexity of algorithm A: constant order\ndef algorithm_A(n)\n    puts 0\nend\n\n# Time complexity of algorithm B: linear order\ndef algorithm_B(n)\n    (0...n).each { puts 0 }\nend\n\n# Time complexity of algorithm C: constant order\ndef algorithm_C(n)\n    (0...1_000_000).each { puts 0 }\nend\n

Figure 2-7 shows the time complexity of the above three algorithm functions.

  • Algorithm A has only \\(1\\) print operation, and the algorithm's runtime does not grow as \\(n\\) increases. We call the time complexity of this algorithm \"constant order\".
  • In algorithm B, the print operation needs to loop \\(n\\) times, and the algorithm's runtime grows linearly as \\(n\\) increases. The time complexity of this algorithm is called \"linear order\".
  • In algorithm C, the print operation needs to loop \\(1000000\\) times. Although the runtime is very long, it is independent of the input data size \\(n\\). Therefore, the time complexity of C is the same as A, still \"constant order\".

Figure 2-7   Time growth trends of algorithms A, B, and C

Compared to directly counting the algorithm's runtime, what are the characteristics of time complexity analysis?

  • Time complexity can effectively evaluate algorithm efficiency. For example, the runtime of algorithm B grows linearly; when \\(n > 1\\) it is slower than algorithm A, and when \\(n > 1000000\\) it is slower than algorithm C. In fact, as long as the input data size \\(n\\) is sufficiently large, an algorithm with \"constant order\" complexity will always be superior to one with \"linear order\" complexity, which is precisely the meaning of time growth trend.
  • The derivation method for time complexity is simpler. Obviously, the running platform and the types of computational operations are both unrelated to the growth trend of the algorithm's runtime. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same \"unit time\", thus simplifying \"counting computational operation runtime\" to \"counting the number of computational operations\", which greatly reduces the difficulty of estimation.
  • Time complexity also has certain limitations. For example, although algorithms A and C have the same time complexity, their actual runtimes differ significantly. Similarly, although algorithm B has a higher time complexity than C, when the input data size \\(n\\) is small, algorithm B is clearly superior to algorithm C. In such cases, it is often difficult to judge the efficiency of algorithms based solely on time complexity. Of course, despite the above issues, complexity analysis remains the most effective and commonly used method for evaluating algorithm efficiency.
","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#232-asymptotic-upper-bound-of-functions","level":2,"title":"2.3.2   Asymptotic Upper Bound of Functions","text":"

Given a function with input size \\(n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 1      # +1\n    a = a + 1  # +1\n    a = a * 2  # +1\n    # Loop n times\n    for i in range(n):  # +1\n        print(0)        # +1\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Loop n times\n    for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\n        cout << 0 << endl;    // +1\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Loop n times\n    for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\n        System.out.println(0);    // +1\n    }\n}\n
void Algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Loop n times\n    for (int i = 0; i < n; i++) {   // +1 (i++ is executed each round)\n        Console.WriteLine(0);   // +1\n    }\n}\n
func algorithm(n int) {\n    a := 1      // +1\n    a = a + 1   // +1\n    a = a * 2   // +1\n    // Loop n times\n    for i := 0; i < n; i++ {   // +1\n        fmt.Println(a)         // +1\n    }\n}\n
func algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // Loop n times\n    for _ in 0 ..< n { // +1\n        print(0) // +1\n    }\n}\n
function algorithm(n) {\n    var a = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // Loop n times\n    for(let i = 0; i < n; i++){ // +1 (i++ is executed each round)\n        console.log(0); // +1\n    }\n}\n
function algorithm(n: number): void{\n    var a: number = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // Loop n times\n    for(let i = 0; i < n; i++){ // +1 (i++ is executed each round)\n        console.log(0); // +1\n    }\n}\n
void algorithm(int n) {\n  int a = 1; // +1\n  a = a + 1; // +1\n  a = a * 2; // +1\n  // Loop n times\n  for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\n    print(0); // +1\n  }\n}\n
fn algorithm(n: i32) {\n    let mut a = 1;   // +1\n    a = a + 1;      // +1\n    a = a * 2;      // +1\n\n    // Loop n times\n    for _ in 0..n { // +1 (i++ is executed each round)\n        println!(\"{}\", 0); // +1\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Loop n times\n    for (int i = 0; i < n; i++) {   // +1 (i++ is executed each round)\n        printf(\"%d\", 0);            // +1\n    }\n}\n
fun algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // Loop n times\n    for (i in 0..<n) { // +1 (i++ is executed each round)\n        println(0) // +1\n    }\n}\n
def algorithm(n)\n    a = 1       # +1\n    a = a + 1   # +1\n    a = a * 2   # +1\n    # Loop n times\n    (0...n).each do # +1\n        puts 0      # +1\n    end\nend\n

Let the number of operations of the algorithm be a function of the input data size \\(n\\), denoted as \\(T(n)\\). Then the number of operations of the above function is:

\\[ T(n) = 3 + 2n \\]

\\(T(n)\\) is a linear function, indicating that its runtime growth trend is linear, and therefore its time complexity is linear order.

We denote the time complexity of linear order as \\(O(n)\\). This mathematical symbol is called big-\\(O\\) notation, representing the asymptotic upper bound of the function \\(T(n)\\).

Time complexity analysis essentially calculates the asymptotic upper bound of \"the number of operations \\(T(n)\\)\", which has a clear mathematical definition.

Asymptotic upper bound of functions

If there exist positive real numbers \\(c\\) and \\(n_0\\) such that for all \\(n > n_0\\), we have \\(T(n) \\leq c \\cdot f(n)\\), then \\(f(n)\\) can be considered as an asymptotic upper bound of \\(T(n)\\), denoted as \\(T(n) = O(f(n))\\).

As shown in Figure 2-8, calculating the asymptotic upper bound is to find a function \\(f(n)\\) such that when \\(n\\) tends to infinity, \\(T(n)\\) and \\(f(n)\\) are at the same growth level, differing only by a constant coefficient \\(c\\).

Figure 2-8   Asymptotic upper bound of a function

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#233-derivation-method","level":2,"title":"2.3.3   Derivation Method","text":"

The asymptotic upper bound has a bit of mathematical flavor. If you feel you haven't fully understood it, don't worry. We can first master the derivation method, and gradually grasp its mathematical meaning through continuous practice.

According to the definition, after determining \\(f(n)\\), we can obtain the time complexity \\(O(f(n))\\). So how do we determine the asymptotic upper bound \\(f(n)\\)? Overall, it is divided into two steps: first count the number of operations, then determine the asymptotic upper bound.

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-step-1-count-the-number-of-operations","level":3,"title":"1.   Step 1: Count the Number of Operations","text":"

For code, count from top to bottom line by line. However, since the constant coefficient \\(c\\) in \\(c \\cdot f(n)\\) above can be of any size, coefficients and constant terms in the number of operations \\(T(n)\\) can all be ignored. According to this principle, the following counting simplification techniques can be summarized.

  1. Ignore constants in \\(T(n)\\). Because they are all independent of \\(n\\), they do not affect time complexity.
  2. Omit all coefficients. For example, looping \\(2n\\) times, \\(5n + 1\\) times, etc., can all be simplified as \\(n\\) times, because the coefficient before \\(n\\) does not affect time complexity.
  3. Use multiplication for nested loops. The total number of operations equals the product of the number of operations in the outer and inner loops, with each layer of loop still able to apply techniques 1. and 2. separately.

Given a function, we can use the above techniques to count the number of operations:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 1      # +0 (Technique 1)\n    a = a + n  # +0 (Technique 1)\n    # +n (Technique 2)\n    for i in range(5 * n + 1):\n        print(0)\n    # +n*n (Technique 3)\n    for i in range(2 * n):\n        for j in range(n + 1):\n            print(0)\n
void algorithm(int n) {\n    int a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        cout << 0 << endl;\n    }\n    // +n*n (Technique 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            cout << 0 << endl;\n        }\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        System.out.println(0);\n    }\n    // +n*n (Technique 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            System.out.println(0);\n        }\n    }\n}\n
void Algorithm(int n) {\n    int a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        Console.WriteLine(0);\n    }\n    // +n*n (Technique 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            Console.WriteLine(0);\n        }\n    }\n}\n
func algorithm(n int) {\n    a := 1     // +0 (Technique 1)\n    a = a + n  // +0 (Technique 1)\n    // +n (Technique 2)\n    for i := 0; i < 5 * n + 1; i++ {\n        fmt.Println(0)\n    }\n    // +n*n (Technique 3)\n    for i := 0; i < 2 * n; i++ {\n        for j := 0; j < n + 1; j++ {\n            fmt.Println(0)\n        }\n    }\n}\n
func algorithm(n: Int) {\n    var a = 1 // +0 (Technique 1)\n    a = a + n // +0 (Technique 1)\n    // +n (Technique 2)\n    for _ in 0 ..< (5 * n + 1) {\n        print(0)\n    }\n    // +n*n (Technique 3)\n    for _ in 0 ..< (2 * n) {\n        for _ in 0 ..< (n + 1) {\n            print(0)\n        }\n    }\n}\n
function algorithm(n) {\n    let a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (Technique 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
function algorithm(n: number): void {\n    let a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (Technique 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
void algorithm(int n) {\n  int a = 1; // +0 (Technique 1)\n  a = a + n; // +0 (Technique 1)\n  // +n (Technique 2)\n  for (int i = 0; i < 5 * n + 1; i++) {\n    print(0);\n  }\n  // +n*n (Technique 3)\n  for (int i = 0; i < 2 * n; i++) {\n    for (int j = 0; j < n + 1; j++) {\n      print(0);\n    }\n  }\n}\n
fn algorithm(n: i32) {\n    let mut a = 1;     // +0 (Technique 1)\n    a = a + n;        // +0 (Technique 1)\n\n    // +n (Technique 2)\n    for i in 0..(5 * n + 1) {\n        println!(\"{}\", 0);\n    }\n\n    // +n*n (Technique 3)\n    for i in 0..(2 * n) {\n        for j in 0..(n + 1) {\n            println!(\"{}\", 0);\n        }\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        printf(\"%d\", 0);\n    }\n    // +n*n (Technique 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            printf(\"%d\", 0);\n        }\n    }\n}\n
fun algorithm(n: Int) {\n    var a = 1   // +0 (Technique 1)\n    a = a + n   // +0 (Technique 1)\n    // +n (Technique 2)\n    for (i in 0..<5 * n + 1) {\n        println(0)\n    }\n    // +n*n (Technique 3)\n    for (i in 0..<2 * n) {\n        for (j in 0..<n + 1) {\n            println(0)\n        }\n    }\n}\n
def algorithm(n)\n    a = 1       # +0 (Technique 1)\n    a = a + n   # +0 (Technique 1)\n    # +n (Technique 2)\n    (0...(5 * n + 1)).each do { puts 0 }\n    # +n*n (Technique 3)\n    (0...(2 * n)).each do\n        (0...(n + 1)).each do { puts 0 }\n    end\nend\n

The following formula shows the counting results before and after using the above techniques; both derive a time complexity of \\(O(n^2)\\).

\\[ \\begin{aligned} T(n) & = 2n(n + 1) + (5n + 1) + 2 & \\text{Complete count (-.-|||)} \\newline & = 2n^2 + 7n + 3 \\newline T(n) & = n^2 + n & \\text{Simplified count (o.O)} \\end{aligned} \\]","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-step-2-determine-the-asymptotic-upper-bound","level":3,"title":"2.   Step 2: Determine the Asymptotic Upper Bound","text":"

Time complexity is determined by the highest-order term in \\(T(n)\\). This is because as \\(n\\) tends to infinity, the highest-order term will play a dominant role, and the influence of other terms can be ignored.

Table 2-2 shows some examples, where some exaggerated values are used to emphasize the conclusion that \"coefficients cannot shake the order\". When \\(n\\) tends to infinity, these constants become insignificant.

Table 2-2   Time complexities corresponding to different numbers of operations

Number of Operations \\(T(n)\\) Time Complexity \\(O(f(n))\\) \\(100000\\) \\(O(1)\\) \\(3n + 2\\) \\(O(n)\\) \\(2n^2 + 3n + 2\\) \\(O(n^2)\\) \\(n^3 + 10000n^2\\) \\(O(n^3)\\) \\(2^n + 10000n^{10000}\\) \\(O(2^n)\\)","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#234-common-types","level":2,"title":"2.3.4   Common Types","text":"

Let the input data size be \\(n\\). Common time complexity types are shown in Figure 2-9 (arranged in order from low to high).

\\[ \\begin{aligned} O(1) < O(\\log n) < O(n) < O(n \\log n) < O(n^2) < O(2^n) < O(n!) \\newline \\text{Constant order} < \\text{Logarithmic order} < \\text{Linear order} < \\text{Linearithmic order} < \\text{Quadratic order} < \\text{Exponential order} < \\text{Factorial order} \\end{aligned} \\]

Figure 2-9   Common time complexity types

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-constant-order-o1","level":3,"title":"1.   Constant Order \\(O(1)\\)","text":"

The number of operations in constant order is independent of the input data size \\(n\\), meaning it does not change as \\(n\\) changes.

In the following function, although the number of operations size may be large, since it is independent of the input data size \\(n\\), the time complexity remains \\(O(1)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def constant(n: int) -> int:\n    \"\"\"Constant order\"\"\"\n    count = 0\n    size = 100000\n    for _ in range(size):\n        count += 1\n    return count\n
time_complexity.cpp
/* Constant order */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.java
/* Constant order */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.cs
/* Constant order */\nint Constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.go
/* Constant order */\nfunc constant(n int) int {\n    count := 0\n    size := 100000\n    for i := 0; i < size; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* Constant order */\nfunc constant(n: Int) -> Int {\n    var count = 0\n    let size = 100_000\n    for _ in 0 ..< size {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* Constant order */\nfunction constant(n) {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
time_complexity.ts
/* Constant order */\nfunction constant(n: number): number {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
time_complexity.dart
/* Constant order */\nint constant(int n) {\n  int count = 0;\n  int size = 100000;\n  for (var i = 0; i < size; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* Constant order */\nfn constant(n: i32) -> i32 {\n    _ = n;\n    let mut count = 0;\n    let size = 100_000;\n    for _ in 0..size {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* Constant order */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    int i = 0;\n    for (int i = 0; i < size; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* Constant order */\nfun constant(n: Int): Int {\n    var count = 0\n    val size = 100000\n    for (i in 0..<size)\n        count++\n    return count\n}\n
time_complexity.rb
### Constant time ###\ndef constant(n)\n  count = 0\n  size = 100000\n\n  (0...size).each { count += 1 }\n\n  count\nend\n
","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-linear-order-on","level":3,"title":"2.   Linear Order \\(O(n)\\)","text":"

The number of operations in linear order grows linearly relative to the input data size \\(n\\). Linear order typically appears in single-layer loops:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def linear(n: int) -> int:\n    \"\"\"Linear order\"\"\"\n    count = 0\n    for _ in range(n):\n        count += 1\n    return count\n
time_complexity.cpp
/* Linear order */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.java
/* Linear order */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.cs
/* Linear order */\nint Linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.go
/* Linear order */\nfunc linear(n int) int {\n    count := 0\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* Linear order */\nfunc linear(n: Int) -> Int {\n    var count = 0\n    for _ in 0 ..< n {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* Linear order */\nfunction linear(n) {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
time_complexity.ts
/* Linear order */\nfunction linear(n: number): number {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
time_complexity.dart
/* Linear order */\nint linear(int n) {\n  int count = 0;\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* Linear order */\nfn linear(n: i32) -> i32 {\n    let mut count = 0;\n    for _ in 0..n {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* Linear order */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* Linear order */\nfun linear(n: Int): Int {\n    var count = 0\n    for (i in 0..<n)\n        count++\n    return count\n}\n
time_complexity.rb
### Linear time ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n

Operations such as traversing arrays and traversing linked lists have a time complexity of \\(O(n)\\), where \\(n\\) is the length of the array or linked list:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def array_traversal(nums: list[int]) -> int:\n    \"\"\"Linear order (traversing array)\"\"\"\n    count = 0\n    # Number of iterations is proportional to the array length\n    for num in nums:\n        count += 1\n    return count\n
time_complexity.cpp
/* Linear order (traversing array) */\nint arrayTraversal(vector<int> &nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* Linear order (traversing array) */\nint arrayTraversal(int[] nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* Linear order (traversing array) */\nint ArrayTraversal(int[] nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    foreach (int num in nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* Linear order (traversing array) */\nfunc arrayTraversal(nums []int) int {\n    count := 0\n    // Number of iterations is proportional to the array length\n    for range nums {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* Linear order (traversing array) */\nfunc arrayTraversal(nums: [Int]) -> Int {\n    var count = 0\n    // Number of iterations is proportional to the array length\n    for _ in nums {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* Linear order (traversing array) */\nfunction arrayTraversal(nums) {\n    let count = 0;\n    // Number of iterations is proportional to the array length\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* Linear order (traversing array) */\nfunction arrayTraversal(nums: number[]): number {\n    let count = 0;\n    // Number of iterations is proportional to the array length\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* Linear order (traversing array) */\nint arrayTraversal(List<int> nums) {\n  int count = 0;\n  // Number of iterations is proportional to the array length\n  for (var _num in nums) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* Linear order (traversing array) */\nfn array_traversal(nums: &[i32]) -> i32 {\n    let mut count = 0;\n    // Number of iterations is proportional to the array length\n    for _ in nums {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* Linear order (traversing array) */\nint arrayTraversal(int *nums, int n) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* Linear order (traversing array) */\nfun arrayTraversal(nums: IntArray): Int {\n    var count = 0\n    // Number of iterations is proportional to the array length\n    for (num in nums) {\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### Linear time (array traversal) ###\ndef array_traversal(nums)\n  count = 0\n\n  # Number of iterations is proportional to the array length\n  for num in nums\n    count += 1\n  end\n\n  count\nend\n

It is worth noting that the input data size \\(n\\) should be determined according to the type of input data. For example, in the first example, the variable \\(n\\) is the input data size; in the second example, the array length \\(n\\) is the data size.

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#3-quadratic-order-on2","level":3,"title":"3.   Quadratic Order \\(O(n^2)\\)","text":"

The number of operations in quadratic order grows quadratically relative to the input data size \\(n\\). Quadratic order typically appears in nested loops, where both the outer and inner loops have a time complexity of \\(O(n)\\), resulting in an overall time complexity of \\(O(n^2)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def quadratic(n: int) -> int:\n    \"\"\"Quadratic order\"\"\"\n    count = 0\n    # Number of iterations is quadratically related to the data size n\n    for i in range(n):\n        for j in range(n):\n            count += 1\n    return count\n
time_complexity.cpp
/* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.java
/* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.cs
/* Exponential order */\nint Quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.go
/* Exponential order */\nfunc quadratic(n int) int {\n    count := 0\n    // Number of iterations is quadratically related to the data size n\n    for i := 0; i < n; i++ {\n        for j := 0; j < n; j++ {\n            count++\n        }\n    }\n    return count\n}\n
time_complexity.swift
/* Exponential order */\nfunc quadratic(n: Int) -> Int {\n    var count = 0\n    // Number of iterations is quadratically related to the data size n\n    for _ in 0 ..< n {\n        for _ in 0 ..< n {\n            count += 1\n        }\n    }\n    return count\n}\n
time_complexity.js
/* Exponential order */\nfunction quadratic(n) {\n    let count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.ts
/* Exponential order */\nfunction quadratic(n: number): number {\n    let count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.dart
/* Exponential order */\nint quadratic(int n) {\n  int count = 0;\n  // Number of iterations is quadratically related to the data size n\n  for (int i = 0; i < n; i++) {\n    for (int j = 0; j < n; j++) {\n      count++;\n    }\n  }\n  return count;\n}\n
time_complexity.rs
/* Exponential order */\nfn quadratic(n: i32) -> i32 {\n    let mut count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for _ in 0..n {\n        for _ in 0..n {\n            count += 1;\n        }\n    }\n    count\n}\n
time_complexity.c
/* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.kt
/* Exponential order */\nfun quadratic(n: Int): Int {\n    var count = 0\n    // Number of iterations is quadratically related to the data size n\n    for (i in 0..<n) {\n        for (j in 0..<n) {\n            count++\n        }\n    }\n    return count\n}\n
time_complexity.rb
### Quadratic time ###\ndef quadratic(n)\n  count = 0\n\n  # Number of iterations is quadratically related to the data size n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n

Figure 2-10 compares constant order, linear order, and quadratic order time complexities.

Figure 2-10   Time complexities of constant, linear, and quadratic orders

Taking bubble sort as an example, the outer loop executes \\(n - 1\\) times, and the inner loop executes \\(n-1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), \\(1\\) times, averaging \\(n / 2\\) times, resulting in a time complexity of \\(O((n - 1) n / 2) = O(n^2)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def bubble_sort(nums: list[int]) -> int:\n    \"\"\"Quadratic order (bubble sort)\"\"\"\n    count = 0  # Counter\n    # Outer loop: unsorted range is [0, i]\n    for i in range(len(nums) - 1, 0, -1):\n        # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                tmp: int = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3  # Element swap includes 3 unit operations\n    return count\n
time_complexity.cpp
/* Quadratic order (bubble sort) */\nint bubbleSort(vector<int> &nums) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.java
/* Quadratic order (bubble sort) */\nint bubbleSort(int[] nums) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.cs
/* Quadratic order (bubble sort) */\nint BubbleSort(int[] nums) {\n    int count = 0;  // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                count += 3;  // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.go
/* Quadratic order (bubble sort) */\nfunc bubbleSort(nums []int) int {\n    count := 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                tmp := nums[j]\n                nums[j] = nums[j+1]\n                nums[j+1] = tmp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
time_complexity.swift
/* Quadratic order (bubble sort) */\nfunc bubbleSort(nums: inout [Int]) -> Int {\n    var count = 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
time_complexity.js
/* Quadratic order (bubble sort) */\nfunction bubbleSort(nums) {\n    let count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.ts
/* Quadratic order (bubble sort) */\nfunction bubbleSort(nums: number[]): number {\n    let count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.dart
/* Quadratic order (bubble sort) */\nint bubbleSort(List<int> nums) {\n  int count = 0; // Counter\n  // Outer loop: unsorted range is [0, i]\n  for (var i = nums.length - 1; i > 0; i--) {\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (var j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        count += 3; // Element swap includes 3 unit operations\n      }\n    }\n  }\n  return count;\n}\n
time_complexity.rs
/* Quadratic order (bubble sort) */\nfn bubble_sort(nums: &mut [i32]) -> i32 {\n    let mut count = 0; // Counter\n\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    count\n}\n
time_complexity.c
/* Quadratic order (bubble sort) */\nint bubbleSort(int *nums, int n) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = n - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.kt
/* Quadratic order (bubble sort) */\nfun bubbleSort(nums: IntArray): Int {\n    var count = 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
time_complexity.rb
### Quadratic time (bubble sort) ###\ndef bubble_sort(nums)\n  count = 0  # Counter\n\n  # Outer loop: unsorted range is [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Element swap includes 3 unit operations\n      end\n    end\n  end\n\n  count\nend\n
","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#4-exponential-order-o2n","level":3,"title":"4.   Exponential Order \\(O(2^n)\\)","text":"

Biological \"cell division\" is a typical example of exponential order growth: the initial state is \\(1\\) cell, after one round of division it becomes \\(2\\), after two rounds it becomes \\(4\\), and so on; after \\(n\\) rounds of division there are \\(2^n\\) cells.

Figure 2-11 and the following code simulate the cell division process, with a time complexity of \\(O(2^n)\\). Note that the input \\(n\\) represents the number of division rounds, and the return value count represents the total number of divisions.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def exponential(n: int) -> int:\n    \"\"\"Exponential order (loop implementation)\"\"\"\n    count = 0\n    base = 1\n    # Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in range(n):\n        for _ in range(base):\n            count += 1\n        base *= 2\n    # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n
time_complexity.cpp
/* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.java
/* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.cs
/* Exponential order (loop implementation) */\nint Exponential(int n) {\n    int count = 0, bas = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.go
/* Exponential order (loop implementation) */\nfunc exponential(n int) int {\n    count, base := 0, 1\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for i := 0; i < n; i++ {\n        for j := 0; j < base; j++ {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.swift
/* Exponential order (loop implementation) */\nfunc exponential(n: Int) -> Int {\n    var count = 0\n    var base = 1\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in 0 ..< n {\n        for _ in 0 ..< base {\n            count += 1\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.js
/* Exponential order (loop implementation) */\nfunction exponential(n) {\n    let count = 0,\n        base = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.ts
/* Exponential order (loop implementation) */\nfunction exponential(n: number): number {\n    let count = 0,\n        base = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.dart
/* Exponential order (loop implementation) */\nint exponential(int n) {\n  int count = 0, base = 1;\n  // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n  for (var i = 0; i < n; i++) {\n    for (var j = 0; j < base; j++) {\n      count++;\n    }\n    base *= 2;\n  }\n  // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  return count;\n}\n
time_complexity.rs
/* Exponential order (loop implementation) */\nfn exponential(n: i32) -> i32 {\n    let mut count = 0;\n    let mut base = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in 0..n {\n        for _ in 0..base {\n            count += 1\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    count\n}\n
time_complexity.c
/* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0;\n    int bas = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.kt
/* Exponential order (loop implementation) */\nfun exponential(n: Int): Int {\n    var count = 0\n    var base = 1\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (i in 0..<n) {\n        for (j in 0..<base) {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.rb
### Exponential time (iterative) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n

Figure 2-11   Time complexity of exponential order

In actual algorithms, exponential order often appears in recursive functions. For example, in the following code, it recursively splits in two, stopping after \\(n\\) splits:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def exp_recur(n: int) -> int:\n    \"\"\"Exponential order (recursive implementation)\"\"\"\n    if n == 1:\n        return 1\n    return exp_recur(n - 1) + exp_recur(n - 1) + 1\n
time_complexity.cpp
/* Exponential order (recursive implementation) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.java
/* Exponential order (recursive implementation) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.cs
/* Exponential order (recursive implementation) */\nint ExpRecur(int n) {\n    if (n == 1) return 1;\n    return ExpRecur(n - 1) + ExpRecur(n - 1) + 1;\n}\n
time_complexity.go
/* Exponential order (recursive implementation) */\nfunc expRecur(n int) int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n-1) + expRecur(n-1) + 1\n}\n
time_complexity.swift
/* Exponential order (recursive implementation) */\nfunc expRecur(n: Int) -> Int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n: n - 1) + expRecur(n: n - 1) + 1\n}\n
time_complexity.js
/* Exponential order (recursive implementation) */\nfunction expRecur(n) {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.ts
/* Exponential order (recursive implementation) */\nfunction expRecur(n: number): number {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.dart
/* Exponential order (recursive implementation) */\nint expRecur(int n) {\n  if (n == 1) return 1;\n  return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.rs
/* Exponential order (recursive implementation) */\nfn exp_recur(n: i32) -> i32 {\n    if n == 1 {\n        return 1;\n    }\n    exp_recur(n - 1) + exp_recur(n - 1) + 1\n}\n
time_complexity.c
/* Exponential order (recursive implementation) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.kt
/* Exponential order (recursive implementation) */\nfun expRecur(n: Int): Int {\n    if (n == 1) {\n        return 1\n    }\n    return expRecur(n - 1) + expRecur(n - 1) + 1\n}\n
time_complexity.rb
### Exponential time (recursive) ###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n

Exponential order growth is very rapid and is common in exhaustive methods (brute force search, backtracking, etc.). For problems with large data scales, exponential order is unacceptable and typically requires dynamic programming or greedy algorithms to solve.

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#5-logarithmic-order-olog-n","level":3,"title":"5.   Logarithmic Order \\(O(\\log n)\\)","text":"

In contrast to exponential order, logarithmic order reflects the situation of \"reducing to half each round\". Let the input data size be \\(n\\). Since it is reduced to half each round, the number of loops is \\(\\log_2 n\\), which is the inverse function of \\(2^n\\).

Figure 2-12 and the following code simulate the process of \"reducing to half each round\", with a time complexity of \\(O(\\log_2 n)\\), abbreviated as \\(O(\\log n)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def logarithmic(n: int) -> int:\n    \"\"\"Logarithmic order (loop implementation)\"\"\"\n    count = 0\n    while n > 1:\n        n = n / 2\n        count += 1\n    return count\n
time_complexity.cpp
/* Logarithmic order (loop implementation) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* Logarithmic order (loop implementation) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* Logarithmic order (loop implementation) */\nint Logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n /= 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* Logarithmic order (loop implementation) */\nfunc logarithmic(n int) int {\n    count := 0\n    for n > 1 {\n        n = n / 2\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* Logarithmic order (loop implementation) */\nfunc logarithmic(n: Int) -> Int {\n    var count = 0\n    var n = n\n    while n > 1 {\n        n = n / 2\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* Logarithmic order (loop implementation) */\nfunction logarithmic(n) {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* Logarithmic order (loop implementation) */\nfunction logarithmic(n: number): number {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* Logarithmic order (loop implementation) */\nint logarithmic(int n) {\n  int count = 0;\n  while (n > 1) {\n    n = n ~/ 2;\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* Logarithmic order (loop implementation) */\nfn logarithmic(mut n: i32) -> i32 {\n    let mut count = 0;\n    while n > 1 {\n        n = n / 2;\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* Logarithmic order (loop implementation) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* Logarithmic order (loop implementation) */\nfun logarithmic(n: Int): Int {\n    var n1 = n\n    var count = 0\n    while (n1 > 1) {\n        n1 /= 2\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### Logarithmic time (iterative) ###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n

Figure 2-12   Time complexity of logarithmic order

Like exponential order, logarithmic order also commonly appears in recursive functions. The following code forms a recursion tree of height \\(\\log_2 n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def log_recur(n: int) -> int:\n    \"\"\"Logarithmic order (recursive implementation)\"\"\"\n    if n <= 1:\n        return 0\n    return log_recur(n / 2) + 1\n
time_complexity.cpp
/* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.java
/* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.cs
/* Logarithmic order (recursive implementation) */\nint LogRecur(int n) {\n    if (n <= 1) return 0;\n    return LogRecur(n / 2) + 1;\n}\n
time_complexity.go
/* Logarithmic order (recursive implementation) */\nfunc logRecur(n int) int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n/2) + 1\n}\n
time_complexity.swift
/* Logarithmic order (recursive implementation) */\nfunc logRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n: n / 2) + 1\n}\n
time_complexity.js
/* Logarithmic order (recursive implementation) */\nfunction logRecur(n) {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.ts
/* Logarithmic order (recursive implementation) */\nfunction logRecur(n: number): number {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.dart
/* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n  if (n <= 1) return 0;\n  return logRecur(n ~/ 2) + 1;\n}\n
time_complexity.rs
/* Logarithmic order (recursive implementation) */\nfn log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 0;\n    }\n    log_recur(n / 2) + 1\n}\n
time_complexity.c
/* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.kt
/* Logarithmic order (recursive implementation) */\nfun logRecur(n: Int): Int {\n    if (n <= 1)\n        return 0\n    return logRecur(n / 2) + 1\n}\n
time_complexity.rb
### Logarithmic time (recursive) ###\ndef log_recur(n)\n  return 0 unless n > 1\n  log_recur(n / 2) + 1\nend\n

Logarithmic order commonly appears in algorithms based on the divide-and-conquer strategy, embodying the algorithmic thinking of \"dividing into many\" and \"simplifying complexity\". It grows slowly and is the ideal time complexity second only to constant order.

What is the base of \\(O(\\log n)\\)?

To be precise, \"dividing into \\(m\\)\" corresponds to a time complexity of \\(O(\\log_m n)\\). And through the logarithmic base change formula, we can obtain time complexities with different bases that are equal:

\\[ O(\\log_m n) = O(\\log_k n / \\log_k m) = O(\\log_k n) \\]

That is to say, the base \\(m\\) can be converted without affecting the complexity. Therefore, we usually omit the base \\(m\\) and denote logarithmic order simply as \\(O(\\log n)\\).

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#6-linearithmic-order-on-log-n","level":3,"title":"6.   Linearithmic Order \\(O(n \\log n)\\)","text":"

Linearithmic order commonly appears in nested loops, where the time complexities of the two layers of loops are \\(O(\\log n)\\) and \\(O(n)\\) respectively. The relevant code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def linear_log_recur(n: int) -> int:\n    \"\"\"Linearithmic order\"\"\"\n    if n <= 1:\n        return 1\n    # Divide into two, the scale of subproblems is reduced by half\n    count = linear_log_recur(n // 2) + linear_log_recur(n // 2)\n    # Current subproblem contains n operations\n    for _ in range(n):\n        count += 1\n    return count\n
time_complexity.cpp
/* Linearithmic order */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* Linearithmic order */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* Linearithmic order */\nint LinearLogRecur(int n) {\n    if (n <= 1) return 1;\n    int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* Linearithmic order */\nfunc linearLogRecur(n int) int {\n    if n <= 1 {\n        return 1\n    }\n    count := linearLogRecur(n/2) + linearLogRecur(n/2)\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* Linearithmic order */\nfunc linearLogRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 1\n    }\n    var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2)\n    for _ in stride(from: 0, to: n, by: 1) {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* Linearithmic order */\nfunction linearLogRecur(n) {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* Linearithmic order */\nfunction linearLogRecur(n: number): number {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* Linearithmic order */\nint linearLogRecur(int n) {\n  if (n <= 1) return 1;\n  int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2);\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* Linearithmic order */\nfn linear_log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 1;\n    }\n    let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2);\n    for _ in 0..n {\n        count += 1;\n    }\n    return count;\n}\n
time_complexity.c
/* Linearithmic order */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* Linearithmic order */\nfun linearLogRecur(n: Int): Int {\n    if (n <= 1)\n        return 1\n    var count = linearLogRecur(n / 2) + linearLogRecur(n / 2)\n    for (i in 0..<n) {\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### Linearithmic time ###\ndef linear_log_recur(n)\n  return 1 unless n > 1\n\n  count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n  (0...n).each { count += 1 }\n\n  count\nend\n

Figure 2-13 shows how linearithmic order is generated. Each level of the binary tree has a total of \\(n\\) operations, and the tree has \\(\\log_2 n + 1\\) levels, resulting in a time complexity of \\(O(n \\log n)\\).

Figure 2-13   Time complexity of linearithmic order

Mainstream sorting algorithms typically have a time complexity of \\(O(n \\log n)\\), such as quicksort, merge sort, and heap sort.

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#7-factorial-order-on","level":3,"title":"7.   Factorial Order \\(O(n!)\\)","text":"

Factorial order corresponds to the mathematical \"permutation\" problem. Given \\(n\\) distinct elements, find all possible permutation schemes; the number of schemes is:

\\[ n! = n \\times (n - 1) \\times (n - 2) \\times \\dots \\times 2 \\times 1 \\]

Factorials are typically implemented using recursion. As shown in Figure 2-14 and the following code, the first level splits into \\(n\\) branches, the second level splits into \\(n - 1\\) branches, and so on, until the \\(n\\)-th level when splitting stops:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def factorial_recur(n: int) -> int:\n    \"\"\"Factorial order (recursive implementation)\"\"\"\n    if n == 0:\n        return 1\n    count = 0\n    # Split from 1 into n\n    for _ in range(n):\n        count += factorial_recur(n - 1)\n    return count\n
time_complexity.cpp
/* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.java
/* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.cs
/* Factorial order (recursive implementation) */\nint FactorialRecur(int n) {\n    if (n == 0) return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += FactorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.go
/* Factorial order (recursive implementation) */\nfunc factorialRecur(n int) int {\n    if n == 0 {\n        return 1\n    }\n    count := 0\n    // Split from 1 into n\n    for i := 0; i < n; i++ {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
time_complexity.swift
/* Factorial order (recursive implementation) */\nfunc factorialRecur(n: Int) -> Int {\n    if n == 0 {\n        return 1\n    }\n    var count = 0\n    // Split from 1 into n\n    for _ in 0 ..< n {\n        count += factorialRecur(n: n - 1)\n    }\n    return count\n}\n
time_complexity.js
/* Factorial order (recursive implementation) */\nfunction factorialRecur(n) {\n    if (n === 0) return 1;\n    let count = 0;\n    // Split from 1 into n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.ts
/* Factorial order (recursive implementation) */\nfunction factorialRecur(n: number): number {\n    if (n === 0) return 1;\n    let count = 0;\n    // Split from 1 into n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.dart
/* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n  if (n == 0) return 1;\n  int count = 0;\n  // Split from 1 into n\n  for (var i = 0; i < n; i++) {\n    count += factorialRecur(n - 1);\n  }\n  return count;\n}\n
time_complexity.rs
/* Factorial order (recursive implementation) */\nfn factorial_recur(n: i32) -> i32 {\n    if n == 0 {\n        return 1;\n    }\n    let mut count = 0;\n    // Split from 1 into n\n    for _ in 0..n {\n        count += factorial_recur(n - 1);\n    }\n    count\n}\n
time_complexity.c
/* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.kt
/* Factorial order (recursive implementation) */\nfun factorialRecur(n: Int): Int {\n    if (n == 0)\n        return 1\n    var count = 0\n    // Split from 1 into n\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
time_complexity.rb
### Factorial time (recursive) ###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # Split from 1 into n\n  (0...n).each { count += factorial_recur(n - 1) }\n\n  count\nend\n

Figure 2-14   Time complexity of factorial order

Note that because when \\(n \\geq 4\\) we always have \\(n! > 2^n\\), factorial order grows faster than exponential order, and is also unacceptable for large \\(n\\).

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#235-worst-best-and-average-time-complexities","level":2,"title":"2.3.5   Worst, Best, and Average Time Complexities","text":"

The time efficiency of an algorithm is often not fixed, but is related to the distribution of the input data. Suppose we input an array nums of length \\(n\\), where nums consists of numbers from \\(1\\) to \\(n\\), with each number appearing only once, but the element order is randomly shuffled. The task is to return the index of element \\(1\\). We can draw the following conclusions.

  • When nums = [?, ?, ..., 1], i.e., when the last element is \\(1\\), it requires a complete traversal of the array, reaching worst-case time complexity \\(O(n)\\).
  • When nums = [1, ?, ?, ...], i.e., when the first element is \\(1\\), no matter how long the array is, there is no need to continue traversing, reaching best-case time complexity \\(\\Omega(1)\\).

The \"worst-case time complexity\" corresponds to the function's asymptotic upper bound, denoted using big-\\(O\\) notation. Correspondingly, the \"best-case time complexity\" corresponds to the function's asymptotic lower bound, denoted using \\(\\Omega\\) notation:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby worst_best_time_complexity.py
def random_numbers(n: int) -> list[int]:\n    \"\"\"Generate an array with elements: 1, 2, ..., n, shuffled in order\"\"\"\n    # Generate array nums =: 1, 2, 3, ..., n\n    nums = [i for i in range(1, n + 1)]\n    # Randomly shuffle array elements\n    random.shuffle(nums)\n    return nums\n\ndef find_one(nums: list[int]) -> int:\n    \"\"\"Find the index of number 1 in array nums\"\"\"\n    for i in range(len(nums)):\n        # When element 1 is at the head of the array, best time complexity O(1) is achieved\n        # When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1:\n            return i\n    return -1\n
worst_best_time_complexity.cpp
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nvector<int> randomNumbers(int n) {\n    vector<int> nums(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Use system time to generate random seed\n    unsigned seed = chrono::system_clock::now().time_since_epoch().count();\n    // Randomly shuffle array elements\n    shuffle(nums.begin(), nums.end(), default_random_engine(seed));\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(vector<int> &nums) {\n    for (int i = 0; i < nums.size(); i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.java
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint[] randomNumbers(int n) {\n    Integer[] nums = new Integer[n];\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\n    Collections.shuffle(Arrays.asList(nums));\n    // Integer[] -> int[]\n    int[] res = new int[n];\n    for (int i = 0; i < n; i++) {\n        res[i] = nums[i];\n    }\n    return res;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(int[] nums) {\n    for (int i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.cs
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint[] RandomNumbers(int n) {\n    int[] nums = new int[n];\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n\n    // Randomly shuffle array elements\n    for (int i = 0; i < nums.Length; i++) {\n        int index = new Random().Next(i, nums.Length);\n        (nums[i], nums[index]) = (nums[index], nums[i]);\n    }\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint FindOne(int[] nums) {\n    for (int i = 0; i < nums.Length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.go
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunc randomNumbers(n int) []int {\n    nums := make([]int, n)\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for i := 0; i < n; i++ {\n        nums[i] = i + 1\n    }\n    // Randomly shuffle array elements\n    rand.Shuffle(len(nums), func(i, j int) {\n        nums[i], nums[j] = nums[j], nums[i]\n    })\n    return nums\n}\n\n/* Find the index of number 1 in array nums */\nfunc findOne(nums []int) int {\n    for i := 0; i < len(nums); i++ {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
worst_best_time_complexity.swift
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunc randomNumbers(n: Int) -> [Int] {\n    // Generate array nums = { 1, 2, 3, ..., n }\n    var nums = Array(1 ... n)\n    // Randomly shuffle array elements\n    nums.shuffle()\n    return nums\n}\n\n/* Find the index of number 1 in array nums */\nfunc findOne(nums: [Int]) -> Int {\n    for i in nums.indices {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
worst_best_time_complexity.js
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunction randomNumbers(n) {\n    const nums = Array(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nfunction findOne(nums) {\n    for (let i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
worst_best_time_complexity.ts
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunction randomNumbers(n: number): number[] {\n    const nums = Array(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nfunction findOne(nums: number[]): number {\n    for (let i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
worst_best_time_complexity.dart
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nList<int> randomNumbers(int n) {\n  final nums = List.filled(n, 0);\n  // Generate array nums = { 1, 2, 3, ..., n }\n  for (var i = 0; i < n; i++) {\n    nums[i] = i + 1;\n  }\n  // Randomly shuffle array elements\n  nums.shuffle();\n\n  return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(List<int> nums) {\n  for (var i = 0; i < nums.length; i++) {\n    // When element 1 is at the head of the array, best time complexity O(1) is achieved\n    // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n    if (nums[i] == 1) return i;\n  }\n\n  return -1;\n}\n
worst_best_time_complexity.rs
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfn random_numbers(n: i32) -> Vec<i32> {\n    // Generate array nums = { 1, 2, 3, ..., n }\n    let mut nums = (1..=n).collect::<Vec<i32>>();\n    // Randomly shuffle array elements\n    nums.shuffle(&mut thread_rng());\n    nums\n}\n\n/* Find the index of number 1 in array nums */\nfn find_one(nums: &[i32]) -> Option<usize> {\n    for i in 0..nums.len() {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return Some(i);\n        }\n    }\n    None\n}\n
worst_best_time_complexity.c
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint *randomNumbers(int n) {\n    // Allocate heap memory (create 1D variable-length array: n elements of type int)\n    int *nums = (int *)malloc(n * sizeof(int));\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\n    for (int i = n - 1; i > 0; i--) {\n        int j = rand() % (i + 1);\n        int temp = nums[i];\n        nums[i] = nums[j];\n        nums[j] = temp;\n    }\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(int *nums, int n) {\n    for (int i = 0; i < n; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.kt
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfun randomNumbers(n: Int): Array<Int?> {\n    val nums = IntArray(n)\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (i in 0..<n) {\n        nums[i] = i + 1\n    }\n    // Randomly shuffle array elements\n    nums.shuffle()\n    val res = arrayOfNulls<Int>(n)\n    for (i in 0..<n) {\n        res[i] = nums[i]\n    }\n    return res\n}\n\n/* Find the index of number 1 in array nums */\nfun findOne(nums: Array<Int?>): Int {\n    for (i in nums.indices) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i\n    }\n    return -1\n}\n
worst_best_time_complexity.rb
### Generate array with elements: 1, 2, ..., n, shuffled ###\ndef random_numbers(n)\n  # Generate array nums =: 1, 2, 3, ..., n\n  nums = Array.new(n) { |i| i + 1 }\n  # Randomly shuffle array elements\n  nums.shuffle!\nend\n\n### Find index of number 1 in array nums ###\ndef find_one(nums)\n  for i in 0...nums.length\n    # When element 1 is at the head of the array, best time complexity O(1) is achieved\n    # When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n    return i if nums[i] == 1\n  end\n\n  -1\nend\n

It is worth noting that we rarely use best-case time complexity in practice, because it can usually only be achieved with a very small probability and may be somewhat misleading. The worst-case time complexity is more practical because it gives a safety value for efficiency, allowing us to use the algorithm with confidence.

From the above example, we can see that both worst-case and best-case time complexities only occur under \"special data distributions\", which may have a very small probability of occurrence and may not truly reflect the algorithm's running efficiency. In contrast, average time complexity can reflect the algorithm's running efficiency under random input data, denoted using the \\(\\Theta\\) notation.

For some algorithms, we can simply derive the average case under random data distribution. For example, in the above example, since the input array is shuffled, the probability of element \\(1\\) appearing at any index is equal, so the algorithm's average number of loops is half the array length \\(n / 2\\), giving an average time complexity of \\(\\Theta(n / 2) = \\Theta(n)\\).

But for more complex algorithms, calculating average time complexity is often quite difficult, because it is hard to analyze the overall mathematical expectation under data distribution. In this case, we usually use worst-case time complexity as the criterion for judging algorithm efficiency.

Why is the \\(\\Theta\\) symbol rarely seen?

This may be because the \\(O\\) symbol is too catchy, so we often use it to represent average time complexity. But strictly speaking, this practice is not standard. In this book and other materials, if you encounter expressions like \"average time complexity \\(O(n)\\)\", please understand it directly as \\(\\Theta(n)\\).

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_data_structure/","level":1,"title":"Chapter 3.   Data Structures","text":"

Abstract

Data structure is like a sturdy and diverse framework.

It provides a blueprint for the orderly organization of data, upon which algorithms come to life.

","path":["Chapter 3. Data Structures","Chapter 3.   Data Structures"],"tags":[]},{"location":"chapter_data_structure/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 3.1   Classification of Data Structures
  • 3.2   Basic Data Types
  • 3.3   Number Encoding *
  • 3.4   Character Encoding *
  • 3.5   Summary
","path":["Chapter 3. Data Structures","Chapter 3.   Data Structures"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   Basic Data Types","text":"

When we talk about data in computers, we think of various forms such as text, images, videos, audio, 3D models, and more. Although these data are organized in different ways, they are all composed of various basic data types.

Basic data types are types that the CPU can directly operate on, and they are directly used in algorithms, mainly including the following.

  • Integer types byte, short, int, long.
  • Floating-point types float, double, used to represent decimal numbers.
  • Character type char, used to represent letters, punctuation marks, and even emojis in various languages.
  • Boolean type bool, used to represent \"yes\" and \"no\" judgments.

Basic data types are stored in binary form in computers. One binary bit is \\(1\\) bit. In most modern operating systems, \\(1\\) byte consists of \\(8\\) bits.

The range of values for basic data types depends on the size of the space they occupy. Below is an example using Java.

  • Integer type byte occupies \\(1\\) byte = \\(8\\) bits, and can represent \\(2^{8}\\) numbers.
  • Integer type int occupies \\(4\\) bytes = \\(32\\) bits, and can represent \\(2^{32}\\) numbers.

The following table lists the space occupied, value ranges, and default values of various basic data types in Java. You don't need to memorize this table; a general understanding is sufficient, and you can refer to it when needed.

Table 3-1   Space occupied and value ranges of basic data types

Type Symbol Space Occupied Minimum Value Maximum Value Default Value Integer byte 1 byte \\(-2^7\\) (\\(-128\\)) \\(2^7 - 1\\) (\\(127\\)) \\(0\\) short 2 bytes \\(-2^{15}\\) \\(2^{15} - 1\\) \\(0\\) int 4 bytes \\(-2^{31}\\) \\(2^{31} - 1\\) \\(0\\) long 8 bytes \\(-2^{63}\\) \\(2^{63} - 1\\) \\(0\\) Float float 4 bytes \\(1.175 \\times 10^{-38}\\) \\(3.403 \\times 10^{38}\\) \\(0.0\\text{f}\\) double 8 bytes \\(2.225 \\times 10^{-308}\\) \\(1.798 \\times 10^{308}\\) \\(0.0\\) Character char 2 bytes \\(0\\) \\(2^{16} - 1\\) \\(0\\) Boolean bool 1 byte \\(\\text{false}\\) \\(\\text{true}\\) \\(\\text{false}\\)

Please note that the above table is specific to Java's basic data types. Each programming language has its own data type definitions, and their space occupied, value ranges, and default values may vary.

  • In Python, the integer type int can be of any size, limited only by available memory; the floating-point type float is double-precision 64-bit; there is no char type, a single character is actually a string str of length 1.
  • C and C++ do not explicitly specify the size of basic data types, which varies by implementation and platform. The above table follows the LP64 data model, which is used in Unix 64-bit operating systems including Linux and macOS.
  • The size of character char is 1 byte in C and C++, and in most programming languages it depends on the specific character encoding method, as detailed in the \"Character Encoding\" section.
  • Even though representing a boolean value requires only 1 bit (\\(0\\) or \\(1\\)), it is usually stored as 1 byte in memory. This is because modern computer CPUs typically use 1 byte as the minimum addressable memory unit.

So, what is the relationship between basic data types and data structures? We know that data structures are ways of organizing and storing data in computers. The subject of this statement is \"structure\", not \"data\".

If we want to represent \"a row of numbers\", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and order relationships of numbers, but the content stored—whether integer int, floating-point float, or character char—is unrelated to the \"data structure\".

In other words, basic data types provide the \"content type\" of data, while data structures provide the \"organization method\" of data. For example, in the following code, we use the same data structure (array) to store and represent different basic data types, including int, float, char, bool, etc.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# Initialize arrays using various basic data types\nnumbers: list[int] = [0] * 5\ndecimals: list[float] = [0.0] * 5\n# In Python, characters are actually strings of length 1\ncharacters: list[str] = ['0'] * 5\nbools: list[bool] = [False] * 5\n# Python lists can freely store various basic data types and object references\ndata = [0, 0.0, 'a', False, ListNode(0)]\n
// Initialize arrays using various basic data types\nint numbers[5];\nfloat decimals[5];\nchar characters[5];\nbool bools[5];\n
// Initialize arrays using various basic data types\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nboolean[] bools = new boolean[5];\n
// Initialize arrays using various basic data types\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nbool[] bools = new bool[5];\n
// Initialize arrays using various basic data types\nvar numbers = [5]int{}\nvar decimals = [5]float64{}\nvar characters = [5]byte{}\nvar bools = [5]bool{}\n
// Initialize arrays using various basic data types\nlet numbers = Array(repeating: 0, count: 5)\nlet decimals = Array(repeating: 0.0, count: 5)\nlet characters: [Character] = Array(repeating: \"a\", count: 5)\nlet bools = Array(repeating: false, count: 5)\n
// JavaScript arrays can freely store various basic data types and objects\nconst array = [0, 0.0, 'a', false];\n
// Initialize arrays using various basic data types\nconst numbers: number[] = [];\nconst characters: string[] = [];\nconst bools: boolean[] = [];\n
// Initialize arrays using various basic data types\nList<int> numbers = List.filled(5, 0);\nList<double> decimals = List.filled(5, 0.0);\nList<String> characters = List.filled(5, 'a');\nList<bool> bools = List.filled(5, false);\n
// Initialize arrays using various basic data types\nlet numbers: Vec<i32> = vec![0; 5];\nlet decimals: Vec<f32> = vec![0.0; 5];\nlet characters: Vec<char> = vec!['0'; 5];\nlet bools: Vec<bool> = vec![false; 5];\n
// Initialize arrays using various basic data types\nint numbers[10];\nfloat decimals[10];\nchar characters[10];\nbool bools[10];\n
// Initialize arrays using various basic data types\nval numbers = IntArray(5)\nval decinals = FloatArray(5)\nval characters = CharArray(5)\nval bools = BooleanArray(5)\n
# Ruby lists can freely store various basic data types and object references\ndata = [0, 0.0, 'a', false, ListNode(0)]\n
Visualized Execution

https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["Chapter 3. Data Structures","3.2   Basic Data Types"],"tags":[]},{"location":"chapter_data_structure/character_encoding/","level":1,"title":"3.4   Character Encoding *","text":"

In computers, all data is stored in binary form, and character char is no exception. To represent characters, we need to establish a \"character set\" that defines a one-to-one correspondence between each character and binary numbers. With a character set, computers can convert binary numbers to characters by looking up the table.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#341-ascii-character-set","level":2,"title":"3.4.1   Ascii Character Set","text":"

ASCII code is the earliest character set, with the full name American Standard Code for Information Interchange. It uses 7 binary bits (the lower 7 bits of one byte) to represent a character, and can represent a maximum of 128 different characters. As shown in Figure 3-6, ASCII code includes uppercase and lowercase English letters, numbers 0 ~ 9, some punctuation marks, and some control characters (such as newline and tab).

Figure 3-6   ASCII code

However, ASCII code can only represent English. With the globalization of computers, a character set called EASCII that can represent more languages emerged. It expands from the 7-bit basis of ASCII to 8 bits, and can represent 256 different characters.

Worldwide, a batch of EASCII character sets suitable for different regions have appeared successively. The first 128 characters of these character sets are unified as ASCII code, and the last 128 characters are defined differently to adapt to the needs of different languages.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#342-gbk-character-set","level":2,"title":"3.4.2   Gbk Character Set","text":"

Later, people found that EASCII code still cannot meet the character quantity requirements of many languages. For example, there are nearly one hundred thousand Chinese characters, and several thousand are used daily. In 1980, the China National Standardization Administration released the GB2312 character set, which included 6,763 Chinese characters, basically meeting the needs for computer processing of Chinese characters.

However, GB2312 cannot handle some rare characters and traditional Chinese characters. The GBK character set is an extension based on GB2312, which includes a total of 21,886 Chinese characters. In the GBK encoding scheme, ASCII characters are represented using one byte, and Chinese characters are represented using two bytes.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode-character-set","level":2,"title":"3.4.3   Unicode Character Set","text":"

With the vigorous development of computer technology, character sets and encoding standards flourished, which brought many problems. On the one hand, these character sets generally only define characters for specific languages and cannot work normally in multilingual environments. On the other hand, multiple character set standards exist for the same language, and if two computers use different encoding standards, garbled characters will appear during information transmission.

Researchers of that era thought: If a sufficiently complete character set is released that includes all languages and symbols in the world, wouldn't it be possible to solve cross-language environment and garbled character problems? Driven by this idea, a large and comprehensive character set, Unicode, was born.

Unicode is called \"统一码\" (Unified Code) in Chinese and can theoretically accommodate over one million characters. It is committed to including characters from around the world into a unified character set, providing a universal character set to handle and display various language texts, reducing garbled character problems caused by different encoding standards.

Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages. In the vast Unicode character set, commonly used characters occupy 2 bytes, and some rare characters occupy 3 bytes or even 4 bytes.

Unicode is a universal character set that essentially assigns a number (called a \"code point\") to each character, but it does not specify how to store these character code points in computers. We can't help but ask: when Unicode code points of multiple lengths appear simultaneously in a text, how does the system parse the characters? For example, given an encoding with a length of 2 bytes, how does the system determine whether it is one 2-byte character or two 1-byte characters?

For the above problem, a straightforward solution is to store all characters as equal-length encodings. As shown in Figure 3-7, each character in \"Hello\" occupies 1 byte, and each character in \"算法\" (algorithm) occupies 2 bytes. We can encode all characters in \"Hello 算法\" as 2 bytes in length by padding the high bits with 0. In this way, the system can parse one character every 2 bytes and restore the content of this phrase.

Figure 3-7   Unicode encoding example

However, ASCII code has already proven to us that encoding English only requires 1 byte. If the above scheme is adopted, the size of English text will be twice that under ASCII encoding, which is very wasteful of memory space. Therefore, we need a more efficient Unicode encoding method.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#344-utf-8-encoding","level":2,"title":"3.4.4   Utf-8 Encoding","text":"

Currently, UTF-8 has become the most widely used Unicode encoding method internationally. It is a variable-length encoding that uses 1 to 4 bytes to represent a character, depending on the complexity of the character. ASCII characters only require 1 byte, Latin and Greek letters require 2 bytes, commonly used Chinese characters require 3 bytes, and some other rare characters require 4 bytes.

The encoding rules of UTF-8 are not complicated and can be divided into the following two cases.

  • For 1-byte characters, set the highest bit to \\(0\\), and set the remaining 7 bits to the Unicode code point. It is worth noting that ASCII characters occupy the first 128 code points in the Unicode character set. That is to say, UTF-8 encoding is backward compatible with ASCII code. This means we can use UTF-8 to parse very old ASCII code text.
  • For characters with a length of \\(n\\) bytes (where \\(n > 1\\)), set the highest \\(n\\) bits of the first byte to \\(1\\), and set the \\((n + 1)\\)-th bit to \\(0\\); starting from the second byte, set the highest 2 bits of each byte to \\(10\\); use all remaining bits to fill in the Unicode code point of the character.

Figure 3-8 shows the UTF-8 encoding corresponding to \"Hello算法\". It can be observed that since the highest \\(n\\) bits are all set to \\(1\\), the system can parse the length of the character as \\(n\\) by reading the number of highest bits that are \\(1\\).

But why set the highest 2 bits of all other bytes to \\(10\\)? In fact, this \\(10\\) can serve as a check symbol. Assuming the system starts parsing text from an incorrect byte, the \\(10\\) at the beginning of the byte can help the system quickly determine an anomaly.

The reason for using \\(10\\) as a check symbol is that under UTF-8 encoding rules, it is impossible for a character's highest two bits to be \\(10\\). This conclusion can be proven by contradiction: assuming the highest two bits of a character are \\(10\\), it means the length of the character is \\(1\\), corresponding to ASCII code. However, the highest bit of ASCII code should be \\(0\\), which contradicts the assumption.

Figure 3-8   UTF-8 encoding example

In addition to UTF-8, common encoding methods also include the following two.

  • UTF-16 encoding: Uses 2 or 4 bytes to represent a character. All ASCII characters and commonly used non-English characters are represented with 2 bytes; a few characters need to use 4 bytes. For 2-byte characters, UTF-16 encoding is equal to the Unicode code point.
  • UTF-32 encoding: Every character uses 4 bytes. This means that UTF-32 takes up more space than UTF-8 and UTF-16, especially for text with a high proportion of ASCII characters.

From the perspective of storage space occupation, using UTF-8 to represent English characters is very efficient because it only requires 1 byte; using UTF-16 encoding for some non-English characters (such as Chinese) will be more efficient because it only requires 2 bytes, while UTF-8 may require 3 bytes.

From a compatibility perspective, UTF-8 has the best universality, and many tools and libraries support UTF-8 first.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#345-character-encoding-in-programming-languages","level":2,"title":"3.4.5   Character Encoding in Programming Languages","text":"

For most past programming languages, strings during program execution use fixed-length encodings such as UTF-16 or UTF-32. Under fixed-length encoding, we can treat strings as arrays for processing, and this approach has the following advantages.

  • Random access: UTF-16 encoded strings can be easily accessed randomly. UTF-8 is a variable-length encoding. To find the \\(i\\)-th character, we need to traverse from the beginning of the string to the \\(i\\)-th character, which requires \\(O(n)\\) time.
  • Character counting: Similar to random access, calculating the length of a UTF-16 encoded string is also an \\(O(1)\\) operation. However, calculating the length of a UTF-8 encoded string requires traversing the entire string.
  • String operations: Many string operations (such as splitting, joining, inserting, deleting, etc.) on UTF-16 encoded strings are easier to perform. Performing these operations on UTF-8 encoded strings usually requires additional calculations to ensure that invalid UTF-8 encoding is not generated.

In fact, the design of character encoding schemes for programming languages is a very interesting topic involving many factors.

  • Java's String type uses UTF-16 encoding, with each character occupying 2 bytes. This is because at the beginning of Java language design, people believed that 16 bits were sufficient to represent all possible characters. However, this was an incorrect judgment. Later, the Unicode specification expanded beyond 16 bits, so characters in Java may now be represented by a pair of 16-bit values (called \"surrogate pairs\").
  • The strings of JavaScript and TypeScript use UTF-16 encoding for reasons similar to Java. When Netscape first introduced the JavaScript language in 1995, Unicode was still in its early stages of development, and at that time, using 16-bit encoding was sufficient to represent all Unicode characters.
  • C# uses UTF-16 encoding mainly because the .NET platform was designed by Microsoft, and many of Microsoft's technologies (including the Windows operating system) extensively use UTF-16 encoding.

Due to the underestimation of character quantities by the above programming languages, they had to adopt the \"surrogate pair\" method to represent Unicode characters with lengths exceeding 16 bits. This is a reluctant compromise. On the one hand, in strings containing surrogate pairs, one character may occupy 2 bytes or 4 bytes, thus losing the advantage of fixed-length encoding. On the other hand, handling surrogate pairs requires additional code, which increases the complexity and difficulty of debugging in programming.

For the above reasons, some programming languages have proposed different encoding schemes.

  • Python's str uses Unicode encoding and adopts a flexible string representation where the stored character length depends on the largest Unicode code point in the string. If all characters in the string are ASCII characters, each character occupies 1 byte; if there are characters exceeding the ASCII range but all within the Basic Multilingual Plane (BMP), each character occupies 2 bytes; if there are characters exceeding the BMP, each character occupies 4 bytes.
  • Go language's string type uses UTF-8 encoding internally. Go language also provides the rune type, which is used to represent a single Unicode code point.
  • Rust language's str and String types use UTF-8 encoding internally. Rust also provides the char type for representing a single Unicode code point.

It should be noted that the above discussion is about how strings are stored in programming languages, which is different from how strings are stored in files or transmitted over networks. In file storage or network transmission, we usually encode strings into UTF-8 format to achieve optimal compatibility and space efficiency.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   Classification of Data Structures","text":"

Common data structures include arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. They can be classified from two dimensions: \"logical structure\" and \"physical structure\".

","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311-logical-structure-linear-and-non-linear","level":2,"title":"3.1.1   Logical Structure: Linear and Non-Linear","text":"

Logical structure reveals the logical relationships between data elements. In arrays and linked lists, data is arranged in a certain order, embodying the linear relationship between data; while in trees, data is arranged hierarchically from top to bottom, showing the derived relationship between \"ancestors\" and \"descendants\"; graphs are composed of nodes and edges, reflecting complex network relationships.

As shown in Figure 3-1, logical structures can be divided into two major categories: \"linear\" and \"non-linear\". Linear structures are more intuitive, indicating that data is linearly arranged in logical relationships; non-linear structures are the opposite, arranged non-linearly.

  • Linear data structures: Arrays, linked lists, stacks, queues, hash tables, where elements have a one-to-one sequential relationship.
  • Non-linear data structures: Trees, heaps, graphs, hash tables.

Non-linear data structures can be further divided into tree structures and network structures.

  • Tree structures: Trees, heaps, hash tables, where elements have a one-to-many relationship.
  • Network structures: Graphs, where elements have a many-to-many relationship.

Figure 3-1   Linear and non-linear data structures

","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312-physical-structure-contiguous-and-dispersed","level":2,"title":"3.1.2   Physical Structure: Contiguous and Dispersed","text":"

When an algorithm program runs, the data being processed is mainly stored in memory. Figure 3-2 shows a computer memory stick, where each black square contains a memory space. We can imagine memory as a huge Excel spreadsheet, where each cell can store a certain amount of data.

The system accesses data at the target location through memory addresses. As shown in Figure 3-2, the computer assigns a number to each cell in the spreadsheet according to specific rules, ensuring that each memory space has a unique memory address. With these addresses, the program can access data in memory.

Figure 3-2   Memory stick, memory space, memory address

Tip

It is worth noting that comparing memory to an Excel spreadsheet is a simplified analogy. The actual working mechanism of memory is quite complex, involving concepts such as address space, memory management, cache mechanisms, virtual memory, and physical memory.

Memory is a shared resource for all programs. When a block of memory is occupied by a program, it usually cannot be used by other programs at the same time. Therefore, in the design of data structures and algorithms, memory resources are an important consideration. For example, the peak memory occupied by an algorithm should not exceed the remaining free memory of the system; if there is a lack of contiguous large memory blocks, then the data structure chosen must be able to be stored in dispersed memory spaces.

As shown in Figure 3-3, physical structure reflects the way data is stored in computer memory, and can be divided into contiguous space storage (arrays) and dispersed space storage (linked lists). The two physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency.

Figure 3-3   Contiguous space storage and dispersed space storage

It is worth noting that all data structures are implemented based on arrays, linked lists, or a combination of both. For example, stacks and queues can be implemented using either arrays or linked lists; while the implementation of hash tables may include both arrays and linked lists.

  • Can be implemented based on arrays: Stacks, queues, hash tables, trees, heaps, graphs, matrices, tensors (arrays with dimensions \\(\\geq 3\\)), etc.
  • Can be implemented based on linked lists: Stacks, queues, hash tables, trees, heaps, graphs, etc.

After initialization, linked lists can still adjust their length during program execution, so they are also called \"dynamic data structures\". After initialization, the length of arrays cannot be changed, so they are also called \"static data structures\". It is worth noting that arrays can achieve length changes by reallocating memory, thus possessing a certain degree of \"dynamism\".

Tip

If you find it difficult to understand physical structure, it is recommended to read the next chapter first, and then review this section.

","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/number_encoding/","level":1,"title":"3.3   Number Encoding *","text":"

Tip

In this book, chapters marked with an asterisk * are optional readings. If you are short on time or find them challenging, you may skip these initially and return to them after completing the essential chapters.

","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#331-sign-magnitude-1s-complement-and-2s-complement","level":2,"title":"3.3.1   Sign-Magnitude, 1's Complement, and 2's Complement","text":"

In the table from the previous section, we found that all integer types can represent one more negative number than positive numbers. For example, the byte range is \\([-128, 127]\\). This phenomenon is counterintuitive, and its underlying reason involves knowledge of sign-magnitude, 1's complement, and 2's complement.

First, it should be noted that numbers are stored in computers in the form of \"2's complement\". Before analyzing the reasons for this, let's first define these three concepts.

  • Sign-magnitude: We treat the highest bit of the binary representation of a number as the sign bit, where \\(0\\) represents a positive number and \\(1\\) represents a negative number, and the remaining bits represent the value of the number.
  • 1's complement: The 1's complement of a positive number is the same as its sign-magnitude. For a negative number, the 1's complement is obtained by inverting all bits except the sign bit of its sign-magnitude.
  • 2's complement: The 2's complement of a positive number is the same as its sign-magnitude. For a negative number, the 2's complement is obtained by adding \\(1\\) to its 1's complement.

Figure 3-4 shows the conversion methods among sign-magnitude, 1's complement, and 2's complement.

Figure 3-4   Conversions among sign-magnitude, 1's complement, and 2's complement

Sign-magnitude, although the most intuitive, has some limitations. On one hand, the sign-magnitude of negative numbers cannot be directly used in operations. For example, calculating \\(1 + (-2)\\) in sign-magnitude yields \\(-3\\), which is clearly incorrect.

\\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline & = 1000 \\; 0011 \\newline & \\rightarrow -3 \\end{aligned} \\]

To solve this problem, computers introduced 1's complement. If we first convert sign-magnitude to 1's complement and calculate \\(1 + (-2)\\) in 1's complement, then convert the result back to sign-magnitude, we can obtain the correct result of \\(-1\\).

\\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 \\; \\text{(Sign-magnitude)} + 1000 \\; 0010 \\; \\text{(Sign-magnitude)} \\newline & = 0000 \\; 0001 \\; \\text{(1's complement)} + 1111 \\; 1101 \\; \\text{(1's complement)} \\newline & = 1111 \\; 1110 \\; \\text{(1's complement)} \\newline & = 1000 \\; 0001 \\; \\text{(Sign-magnitude)} \\newline & \\rightarrow -1 \\end{aligned} \\]

On the other hand, the sign-magnitude of the number zero has two representations, \\(+0\\) and \\(-0\\). This means that the number zero corresponds to two different binary encodings, which may cause ambiguity. For example, in conditional judgments, if we don't distinguish between positive zero and negative zero, it may lead to incorrect judgment results. If we want to handle the ambiguity of positive and negative zero, we need to introduce additional judgment operations, which may reduce the computational efficiency of the computer.

\\[ \\begin{aligned} +0 & \\rightarrow 0000 \\; 0000 \\newline -0 & \\rightarrow 1000 \\; 0000 \\end{aligned} \\]

Like sign-magnitude, 1's complement also has the problem of positive and negative zero ambiguity. Therefore, computers further introduced 2's complement. Let's first observe the conversion process of negative zero from sign-magnitude to 1's complement to 2's complement:

\\[ \\begin{aligned} -0 \\rightarrow \\; & 1000 \\; 0000 \\; \\text{(Sign-magnitude)} \\newline = \\; & 1111 \\; 1111 \\; \\text{(1's complement)} \\newline = 1 \\; & 0000 \\; 0000 \\; \\text{(2's complement)} \\newline \\end{aligned} \\]

Adding \\(1\\) to the 1's complement of negative zero produces a carry, but since the byte type has a length of only 8 bits, the \\(1\\) that overflows to the 9th bit is discarded. That is to say, the 2's complement of negative zero is \\(0000 \\; 0000\\), which is the same as the 2's complement of positive zero. This means that in 2's complement representation, there is only one zero, and the positive and negative zero ambiguity is thus resolved.

One last question remains: the range of the byte type is \\([-128, 127]\\), and how is the extra negative number \\(-128\\) obtained? We notice that all integers in the interval \\([-127, +127]\\) have corresponding sign-magnitude, 1's complement, and 2's complement, and sign-magnitude and 2's complement can be converted to each other.

However, the 2's complement \\(1000 \\; 0000\\) is an exception, and it does not have a corresponding sign-magnitude. According to the conversion method, we get that the sign-magnitude of this 2's complement is \\(0000 \\; 0000\\). This is clearly contradictory because this sign-magnitude represents the number \\(0\\), and its 2's complement should be itself. The computer specifies that this special 2's complement \\(1000 \\; 0000\\) represents \\(-128\\). In fact, the result of calculating \\((-1) + (-127)\\) in 2's complement is \\(-128\\).

\\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\; 1111 \\; \\text{(Sign-magnitude)} + 1000 \\; 0001 \\; \\text{(Sign-magnitude)} \\newline & = 1000 \\; 0000 \\; \\text{(1's complement)} + 1111 \\; 1110 \\; \\text{(1's complement)} \\newline & = 1000 \\; 0001 \\; \\text{(2's complement)} + 1111 \\; 1111 \\; \\text{(2's complement)} \\newline & = 1000 \\; 0000 \\; \\text{(2's complement)} \\newline & \\rightarrow -128 \\end{aligned} \\]

You may have noticed that all the above calculations are addition operations. This hints at an important fact: the hardware circuits inside computers are mainly designed based on addition operations. This is because addition operations are simpler to implement in hardware compared to other operations (such as multiplication, division, and subtraction), easier to parallelize, and have faster operation speeds.

Please note that this does not mean that computers can only perform addition. By combining addition with some basic logical operations, computers can implement various other mathematical operations. For example, calculating the subtraction \\(a - b\\) can be converted to calculating the addition \\(a + (-b)\\); calculating multiplication and division can be converted to calculating multiple additions or subtractions.

Now we can summarize the reasons why computers use 2's complement: based on 2's complement representation, computers can use the same circuits and operations to handle the addition of positive and negative numbers, without the need to design special hardware circuits to handle subtraction, and without the need to specially handle the ambiguity problem of positive and negative zero. This greatly simplifies hardware design and improves operational efficiency.

The design of 2's complement is very ingenious. Due to space limitations, we will stop here. Interested readers are encouraged to explore further.

","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332-floating-point-number-encoding","level":2,"title":"3.3.2   Floating-Point Number Encoding","text":"

Careful readers may have noticed: int and float have the same length, both are 4 bytes, but why does float have a much larger range than int? This is very counterintuitive because it stands to reason that float needs to represent decimals, so the range should be smaller.

In fact, this is because floating-point number float uses a different representation method. Let's denote a 32-bit binary number as:

\\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

According to the IEEE 754 standard, a 32-bit float consists of the following three parts.

  • Sign bit \\(\\mathrm{S}\\): occupies 1 bit, corresponding to \\(b_{31}\\).
  • Exponent bit \\(\\mathrm{E}\\): occupies 8 bits, corresponding to \\(b_{30} b_{29} \\ldots b_{23}\\).
  • Fraction bit \\(\\mathrm{N}\\): occupies 23 bits, corresponding to \\(b_{22} b_{21} \\ldots b_0\\).

The calculation method for the value corresponding to the binary float is:

\\[ \\text {val} = (-1)^{b_{31}} \\times 2^{\\left(b_{30} b_{29} \\ldots b_{23}\\right)_2-127} \\times\\left(1 . b_{22} b_{21} \\ldots b_0\\right)_2 \\]

Converted to decimal, the calculation formula is:

\\[ \\text {val}=(-1)^{\\mathrm{S}} \\times 2^{\\mathrm{E} -127} \\times (1 + \\mathrm{N}) \\]

The range of each component is:

\\[ \\begin{aligned} \\mathrm{S} \\in & \\{ 0, 1\\}, \\quad \\mathrm{E} \\in \\{ 1, 2, \\dots, 254 \\} \\newline (1 + \\mathrm{N}) = & (1 + \\sum_{i=1}^{23} b_{23-i} 2^{-i}) \\subset [1, 2 - 2^{-23}] \\end{aligned} \\]

Figure 3-5   Calculation example of float under IEEE 754 standard

Observing Figure 3-5, given example data \\(\\mathrm{S} = 0\\), \\(\\mathrm{E} = 124\\), \\(\\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\\), we have:

\\[ \\text { val } = (-1)^0 \\times 2^{124 - 127} \\times (1 + 0.375) = 0.171875 \\]

Now we can answer the initial question: the representation of float includes an exponent bit, resulting in a range far greater than int. According to the above calculation, the maximum positive number that float can represent is \\(2^{254 - 127} \\times (2 - 2^{-23}) \\approx 3.4 \\times 10^{38}\\), and the minimum negative number can be obtained by switching the sign bit.

Although floating-point number float expands the range, its side effect is sacrificing precision. The integer type int uses all 32 bits to represent numbers, and the numbers are evenly distributed; however, due to the existence of the exponent bit, the larger the value of floating-point number float, the larger the difference between two adjacent numbers tends to be.

As shown in Table 3-2, exponent bits \\(\\mathrm{E} = 0\\) and \\(\\mathrm{E} = 255\\) have special meanings, used to represent zero, infinity, \\(\\mathrm{NaN}\\), etc.

Table 3-2   Meaning of exponent bits

Exponent Bit E Fraction Bit \\(\\mathrm{N} = 0\\) Fraction Bit \\(\\mathrm{N} \\ne 0\\) Calculation Formula \\(0\\) \\(\\pm 0\\) Subnormal Number \\((-1)^{\\mathrm{S}} \\times 2^{-126} \\times (0.\\mathrm{N})\\) \\(1, 2, \\dots, 254\\) Normal Number Normal Number \\((-1)^{\\mathrm{S}} \\times 2^{(\\mathrm{E} -127)} \\times (1.\\mathrm{N})\\) \\(255\\) \\(\\pm \\infty\\) \\(\\mathrm{NaN}\\)

It is worth noting that subnormal numbers significantly improve the precision of floating-point numbers. The smallest positive normal number is \\(2^{-126}\\), and the smallest positive subnormal number is \\(2^{-126} \\times 2^{-23}\\).

Double-precision double also uses a representation method similar to float, which will not be elaborated here.

","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/summary/","level":1,"title":"3.5   Summary","text":"","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_data_structure/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Data structures can be classified from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data elements, while physical structure describes how data is stored in computer memory.
  • Common logical structures include linear, tree, and network structures. We typically classify data structures as linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures.
  • When a program runs, data is stored in computer memory. Each memory space has a corresponding memory address, and the program accesses data through these memory addresses.
  • Physical structures are primarily divided into contiguous space storage (arrays) and dispersed space storage (linked lists). All data structures are implemented using arrays, linked lists, or a combination of both.
  • Basic data types in computers include integers byte, short, int, long, floating-point numbers float, double, characters char, and booleans bool. Their value ranges depend on the size of space they occupy and their representation method.
  • Sign-magnitude, 1's complement, and 2's complement are three methods for encoding numbers in computers, and they can be converted into each other. The most significant bit of sign-magnitude is the sign bit, and the remaining bits represent the value of the number.
  • Integers are stored in computers in 2's complement form. Under 2's complement representation, computers can treat the addition of positive and negative numbers uniformly, without needing to design special hardware circuits for subtraction, and there is no ambiguity of positive and negative zero.
  • The encoding of floating-point numbers consists of 1 sign bit, 8 exponent bits, and 23 fraction bits. Due to the exponent bits, the range of floating-point numbers is much larger than that of integers, at the cost of sacrificing precision.
  • ASCII is the earliest English character set, with a length of 1 byte, containing a total of 127 characters. GBK is a commonly used Chinese character set, containing over 20,000 Chinese characters. Unicode is committed to providing a complete character set standard, collecting characters from various languages around the world, thereby solving the garbled text problem caused by inconsistent character encoding methods.
  • UTF-8 is the most popular Unicode encoding method, with excellent universality. It is a variable-length encoding method with good scalability, effectively improving storage space efficiency. UTF-16 and UTF-32 are fixed-length encoding methods. When encoding Chinese characters, UTF-16 occupies less space than UTF-8. Programming languages such as Java and C# use UTF-16 encoding by default.
","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_data_structure/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Why do hash tables contain both linear and non-linear data structures?

The underlying structure of a hash table is an array. To resolve hash collisions, we may use \"chaining\" (discussed in the subsequent \"Hash Collision\" section): each bucket in the array points to a linked list, which may be converted to a tree (usually a red-black tree) when the list length exceeds a certain threshold.

From a storage perspective, the underlying structure of a hash table is an array, where each bucket slot may contain a value, a linked list, or a tree. Therefore, hash tables may contain both linear data structures (arrays, linked lists) and non-linear data structures (trees).

Q: Is the length of the char type 1 byte?

The length of the char type is determined by the encoding method used by the programming language. For example, Java, JavaScript, TypeScript, and C# all use UTF-16 encoding (to store Unicode code points), so the char type has a length of 2 bytes.

Q: Is there ambiguity in referring to array-based data structures as \"static data structures\"? Stacks can also perform \"dynamic\" operations such as push and pop.

Stacks can indeed implement dynamic data operations, but the data structure is still \"static\" (fixed length). Although array-based data structures can dynamically add or remove elements, their capacity is fixed. If the data volume exceeds the pre-allocated size, a new larger array needs to be created, and the contents of the old array must be copied to the new array.

Q: When constructing a stack (queue), its size is not specified. Why are they \"static data structures\"?

In high-level programming languages, we do not need to manually specify the initial capacity of a stack (queue); this work is automatically completed within the class. For example, the initial capacity of Java's ArrayList is typically 10. Additionally, the expansion operation is also automatically implemented. See the subsequent \"List\" section for details.

Q: The method of converting sign-magnitude to 2's complement is \"first negate then add 1\". So converting 2's complement to sign-magnitude should be the inverse operation \"first subtract 1 then negate\". However, 2's complement can also be converted to sign-magnitude through \"first negate then add 1\". Why is this?

This is because the mutual conversion between sign-magnitude and 2's complement is actually the process of computing the \"complement\". Let us first define the complement: assuming \\(a + b = c\\), then we say that \\(a\\) is the complement of \\(b\\) to \\(c\\), and conversely, \\(b\\) is the complement of \\(a\\) to \\(c\\).

Given an \\(n = 4\\) bit binary number \\(0010\\), if we treat this number as sign-magnitude (ignoring the sign bit), then its 2's complement can be obtained through \"first negate then add 1\":

\\[ 0010 \\rightarrow 1101 \\rightarrow 1110 \\]

We find that the sum of sign-magnitude and 2's complement is \\(0010 + 1110 = 10000\\), which means the 2's complement \\(1110\\) is the \"complement\" of sign-magnitude \\(0010\\) to \\(10000\\). This means the above \"first negate then add 1\" is actually the process of computing the complement to \\(10000\\).

So, what is the \"complement\" of 2's complement \\(1110\\) to \\(10000\\)? We can still use \"first negate then add 1\" to obtain it:

\\[ 1110 \\rightarrow 0001 \\rightarrow 0010 \\]

In other words, sign-magnitude and 2's complement are each other's \"complement\" to \\(10000\\), so \"sign-magnitude to 2's complement\" and \"2's complement to sign-magnitude\" can be implemented using the same operation (first negate then add 1).

Of course, we can also use the inverse operation to find the sign-magnitude of 2's complement \\(1110\\), that is, \"first subtract 1 then negate\":

\\[ 1110 \\rightarrow 1101 \\rightarrow 0010 \\]

In summary, both \"first negate then add 1\" and \"first subtract 1 then negate\" are computing the complement to \\(10000\\), and they are equivalent.

Essentially, the \"negate\" operation is actually finding the complement to \\(1111\\) (because sign-magnitude + 1's complement = 1111 always holds); and adding 1 to the 1's complement yields the 2's complement, which is the complement to \\(10000\\).

The above uses \\(n = 4\\) as an example, and it can be generalized to binary numbers of any number of bits.

","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"Chapter 12.   Divide and Conquer","text":"

Abstract

Difficult problems are decomposed layer by layer, with each decomposition making them simpler.

Divide and conquer reveals an important truth: start with simplicity, and nothing remains complex.

","path":["Chapter 12. Divide and Conquer","Chapter 12.   Divide and Conquer"],"tags":[]},{"location":"chapter_divide_and_conquer/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 12.1   Divide and Conquer Algorithms
  • 12.2   Divide and Conquer Search Strategy
  • 12.3   Building a Binary Tree Problem
  • 12.4   Hanoi Tower Problem
  • 12.5   Summary
","path":["Chapter 12. Divide and Conquer","Chapter 12.   Divide and Conquer"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/","level":1,"title":"12.2   Divide and Conquer Search Strategy","text":"

We have already learned that search algorithms are divided into two major categories.

  • Brute-force search: Implemented by traversing the data structure, with a time complexity of \\(O(n)\\).
  • Adaptive search: Utilizes unique data organization forms or prior information, with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\).

In fact, search algorithms with time complexity of \\(O(\\log n)\\) are typically implemented based on the divide and conquer strategy, such as binary search and trees.

  • Each step of binary search divides the problem (searching for a target element in an array) into a smaller problem (searching for the target element in half of the array), continuing until the array is empty or the target element is found.
  • Trees are representative of the divide and conquer idea. In data structures such as binary search trees, AVL trees, and heaps, the time complexity of various operations is \\(O(\\log n)\\).

The divide and conquer strategy of binary search is as follows.

  • The problem can be decomposed: Binary search recursively decomposes the original problem (searching in an array) into subproblems (searching in half of the array), achieved by comparing the middle element with the target element.
  • Subproblems are independent: In binary search, each round only processes one subproblem, which is not affected by other subproblems.
  • Solutions of subproblems do not need to be merged: Binary search aims to find a specific element, so there is no need to merge the solutions of subproblems. When a subproblem is solved, the original problem is also solved.

Divide and conquer can improve search efficiency because brute-force search can only eliminate one option per round, while divide and conquer search can eliminate half of the options per round.

","path":["Chapter 12. Divide and Conquer","12.2   Divide and Conquer Search Strategy"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/#1-implementing-binary-search-based-on-divide-and-conquer","level":3,"title":"1.   Implementing Binary Search Based on Divide and Conquer","text":"

In previous sections, binary search was implemented based on iteration. Now we implement it based on divide and conquer (recursion).

Question

Given a sorted array nums of length \\(n\\), where all elements are unique, find the element target.

From a divide and conquer perspective, we denote the subproblem corresponding to the search interval \\([i, j]\\) as \\(f(i, j)\\).

Starting from the original problem \\(f(0, n-1)\\), perform binary search through the following steps.

  1. Calculate the midpoint \\(m\\) of the search interval \\([i, j]\\), and use it to eliminate half of the search interval.
  2. Recursively solve the subproblem reduced by half in size, which could be \\(f(i, m-1)\\) or \\(f(m+1, j)\\).
  3. Repeat steps 1. and 2. until target is found or the interval is empty and return.

Figure 12-4 shows the divide and conquer process of binary search for element \\(6\\) in an array.

Figure 12-4   Divide and conquer process of binary search

In the implementation code, we declare a recursive function dfs() to solve the problem \\(f(i, j)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_recur.py
def dfs(nums: list[int], target: int, i: int, j: int) -> int:\n    \"\"\"Binary search: problem f(i, j)\"\"\"\n    # If the interval is empty, it means there is no target element, return -1\n    if i > j:\n        return -1\n    # Calculate the midpoint index m\n    m = (i + j) // 2\n    if nums[m] < target:\n        # Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j)\n    elif nums[m] > target:\n        # Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1)\n    else:\n        # Found the target element, return its index\n        return m\n\ndef binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Binary search\"\"\"\n    n = len(nums)\n    # Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n
binary_search_recur.cpp
/* Binary search: problem f(i, j) */\nint dfs(vector<int> &nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(vector<int> &nums, int target) {\n    int n = nums.size();\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.java
/* Binary search: problem f(i, j) */\nint dfs(int[] nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(int[] nums, int target) {\n    int n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.cs
/* Binary search: problem f(i, j) */\nint DFS(int[] nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return DFS(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return DFS(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint BinarySearch(int[] nums, int target) {\n    int n = nums.Length;\n    // Solve the problem f(0, n-1)\n    return DFS(nums, target, 0, n - 1);\n}\n
binary_search_recur.go
/* Binary search: problem f(i, j) */\nfunc dfs(nums []int, target, i, j int) int {\n    // If interval is empty, indicating no target element, return -1\n    if i > j {\n        return -1\n    }\n    // Calculate midpoint index\n    m := i + ((j - i) >> 1)\n    // Compare midpoint with target element\n    if nums[m] < target {\n        // If smaller, recurse on right half of array\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m+1, j)\n    } else if nums[m] > target {\n        // If larger, recurse on left half of array\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m-1)\n    } else {\n        // Found the target element, return its index\n        return m\n    }\n}\n\n/* Binary search */\nfunc binarySearch(nums []int, target int) int {\n    n := len(nums)\n    return dfs(nums, target, 0, n-1)\n}\n
binary_search_recur.swift
/* Binary search: problem f(i, j) */\nfunc dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {\n    // If the interval is empty, it means there is no target element, return -1\n    if i > j {\n        return -1\n    }\n    // Calculate the midpoint index m\n    let m = (i + j) / 2\n    if nums[m] < target {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums: nums, target: target, i: m + 1, j: j)\n    } else if nums[m] > target {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums: nums, target: target, i: i, j: m - 1)\n    } else {\n        // Found the target element, return its index\n        return m\n    }\n}\n\n/* Binary search */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Solve the problem f(0, n-1)\n    dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)\n}\n
binary_search_recur.js
/* Binary search: problem f(i, j) */\nfunction dfs(nums, target, i, j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfunction binarySearch(nums, target) {\n    const n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.ts
/* Binary search: problem f(i, j) */\nfunction dfs(nums: number[], target: number, i: number, j: number): number {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfunction binarySearch(nums: number[], target: number): number {\n    const n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.dart
/* Binary search: problem f(i, j) */\nint dfs(List<int> nums, int target, int i, int j) {\n  // If the interval is empty, it means there is no target element, return -1\n  if (i > j) {\n    return -1;\n  }\n  // Calculate the midpoint index m\n  int m = (i + j) ~/ 2;\n  if (nums[m] < target) {\n    // Recursion subproblem f(m+1, j)\n    return dfs(nums, target, m + 1, j);\n  } else if (nums[m] > target) {\n    // Recursion subproblem f(i, m-1)\n    return dfs(nums, target, i, m - 1);\n  } else {\n    // Found the target element, return its index\n    return m;\n  }\n}\n\n/* Binary search */\nint binarySearch(List<int> nums, int target) {\n  int n = nums.length;\n  // Solve the problem f(0, n-1)\n  return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.rs
/* Binary search: problem f(i, j) */\nfn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {\n    // If the interval is empty, it means there is no target element, return -1\n    if i > j {\n        return -1;\n    }\n    let m: i32 = i + (j - i) / 2;\n    if nums[m as usize] < target {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if nums[m as usize] > target {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    let n = nums.len() as i32;\n    // Solve the problem f(0, n-1)\n    dfs(nums, target, 0, n - 1)\n}\n
binary_search_recur.c
/* Binary search: problem f(i, j) */\nint dfs(int nums[], int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(int nums[], int target, int numsSize) {\n    int n = numsSize;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.kt
/* Binary search: problem f(i, j) */\nfun dfs(\n    nums: IntArray,\n    target: Int,\n    i: Int,\n    j: Int\n): Int {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1\n    }\n    // Calculate the midpoint index m\n    val m = (i + j) / 2\n    return if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        dfs(nums, target, m + 1, j)\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        dfs(nums, target, i, m - 1)\n    } else {\n        // Found the target element, return its index\n        m\n    }\n}\n\n/* Binary search */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    val n = nums.size\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n}\n
binary_search_recur.rb
### Binary search: problem f(i, j) ###\ndef dfs(nums, target, i, j)\n  # If the interval is empty, it means there is no target element, return -1\n  return -1 if i > j\n\n  # Calculate the midpoint index m\n  m = (i + j) / 2\n\n  if nums[m] < target\n    # Recursion subproblem f(m+1, j)\n    return dfs(nums, target, m + 1, j)\n  elsif nums[m] > target\n    # Recursion subproblem f(i, m-1)\n    return dfs(nums, target, i, m - 1)\n  else\n    # Found the target element, return its index\n    return m\n  end\nend\n\n### Binary search ###\ndef binary_search(nums, target)\n  n = nums.length\n  # Solve the problem f(0, n-1)\n  dfs(nums, target, 0, n - 1)\nend\n
","path":["Chapter 12. Divide and Conquer","12.2   Divide and Conquer Search Strategy"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/","level":1,"title":"12.3   Building a Binary Tree Problem","text":"

Question

Given the preorder traversal preorder and inorder traversal inorder of a binary tree, construct the binary tree and return the root node of the binary tree. Assume there are no duplicate node values in the binary tree (as shown in Figure 12-5).

Figure 12-5   Example data for building a binary tree

","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1-determining-if-it-is-a-divide-and-conquer-problem","level":3,"title":"1.   Determining If It Is a Divide and Conquer Problem","text":"

The original problem is defined as constructing a binary tree from preorder and inorder, which is a typical divide and conquer problem.

  • The problem can be decomposed: From a divide and conquer perspective, we can divide the original problem into two subproblems: constructing the left subtree and constructing the right subtree, plus one operation: initializing the root node. For each subtree (subproblem), we can still reuse the above division method, dividing it into smaller subtrees (subproblems) until the smallest subproblem (empty subtree) is reached.
  • Subproblems are independent: The left and right subtrees are independent of each other; there is no overlap between them. When constructing the left subtree, we only need to focus on the parts of the inorder and preorder traversals corresponding to the left subtree. The same applies to the right subtree.
  • Solutions of subproblems can be merged: Once we have the left and right subtrees (solutions of subproblems), we can link them to the root node to obtain the solution to the original problem.
","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2-how-to-divide-subtrees","level":3,"title":"2.   How to Divide Subtrees","text":"

Based on the above analysis, this problem can be solved using divide and conquer, but how do we divide the left and right subtrees through the preorder traversal preorder and inorder traversal inorder?

According to the definition, both preorder and inorder can be divided into three parts.

  • Preorder traversal: [ Root Node | Left Subtree | Right Subtree ], for example, the tree in Figure 12-5 corresponds to [ 3 | 9 | 2 1 7 ].
  • Inorder traversal: [ Left Subtree | Root Node | Right Subtree ], for example, the tree in Figure 12-5 corresponds to [ 9 | 3 | 1 2 7 ].

Using the data from the figure above as an example, we can obtain the division results through the steps shown in Figure 12-6.

  1. The first element 3 in the preorder traversal is the value of the root node.
  2. Find the index of root node 3 in inorder, and use this index to divide inorder into [ 9 | 3 | 1 2 7 ].
  3. Based on the division result of inorder, it is easy to determine that the left and right subtrees have 1 and 3 nodes respectively, allowing us to divide preorder into [ 3 | 9 | 2 1 7 ].

Figure 12-6   Dividing subtrees in preorder and inorder traversals

","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#3-describing-subtree-intervals-based-on-variables","level":3,"title":"3.   Describing Subtree Intervals Based on Variables","text":"

Based on the above division method, we have obtained the index intervals of the root node, left subtree, and right subtree in preorder and inorder. To describe these index intervals, we need to use several pointer variables.

  • Denote the index of the current tree's root node in preorder as \\(i\\).
  • Denote the index of the current tree's root node in inorder as \\(m\\).
  • Denote the index interval of the current tree in inorder as \\([l, r]\\).

As shown in Table 12-1, through these variables we can represent the index of the root node in preorder and the index intervals of the subtrees in inorder.

Table 12-1   Indices of root node and subtrees in preorder and inorder traversals

Root node index in preorder Subtree index interval in inorder Current tree \\(i\\) \\([l, r]\\) Left subtree \\(i + 1\\) \\([l, m-1]\\) Right subtree \\(i + 1 + (m - l)\\) \\([m+1, r]\\)

Please note that \\((m-l)\\) in the right subtree root node index means \"the number of nodes in the left subtree\". It is recommended to understand this in conjunction with Figure 12-7.

Figure 12-7   Index interval representation of root node and left and right subtrees

","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#4-code-implementation","level":3,"title":"4.   Code Implementation","text":"

To improve the efficiency of querying \\(m\\), we use a hash table hmap to store the mapping from elements in the inorder array to their indices:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby build_tree.py
def dfs(\n    preorder: list[int],\n    inorder_map: dict[int, int],\n    i: int,\n    l: int,\n    r: int,\n) -> TreeNode | None:\n    \"\"\"Build binary tree: divide and conquer\"\"\"\n    # Terminate when the subtree interval is empty\n    if r - l < 0:\n        return None\n    # Initialize the root node\n    root = TreeNode(preorder[i])\n    # Query m to divide the left and right subtrees\n    m = inorder_map[preorder[i]]\n    # Subproblem: build the left subtree\n    root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n    # Subproblem: build the right subtree\n    root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n    # Return the root node\n    return root\n\ndef build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:\n    \"\"\"Build binary tree\"\"\"\n    # Initialize hash map, storing the mapping from inorder elements to indices\n    inorder_map = {val: i for i, val in enumerate(inorder)}\n    root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)\n    return root\n
build_tree.cpp
/* Build binary tree: divide and conquer */\nTreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return NULL;\n    // Initialize the root node\n    TreeNode *root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    unordered_map<int, int> inorderMap;\n    for (int i = 0; i < inorder.size(); i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);\n    return root;\n}\n
build_tree.java
/* Build binary tree: divide and conquer */\nTreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return null;\n    // Initialize the root node\n    TreeNode root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode buildTree(int[] preorder, int[] inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    Map<Integer, Integer> inorderMap = new HashMap<>();\n    for (int i = 0; i < inorder.length; i++) {\n        inorderMap.put(inorder[i], i);\n    }\n    TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.cs
/* Build binary tree: divide and conquer */\nTreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return null;\n    // Initialize the root node\n    TreeNode root = new(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode? BuildTree(int[] preorder, int[] inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    Dictionary<int, int> inorderMap = [];\n    for (int i = 0; i < inorder.Length; i++) {\n        inorderMap.TryAdd(inorder[i], i);\n    }\n    TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);\n    return root;\n}\n
build_tree.go
/* Build binary tree: divide and conquer */\nfunc dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {\n    // Terminate when the subtree interval is empty\n    if r-l < 0 {\n        return nil\n    }\n    // Initialize the root node\n    root := NewTreeNode(preorder[i])\n    // Query m to divide the left and right subtrees\n    m := inorderMap[preorder[i]]\n    // Subproblem: build the left subtree\n    root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)\n    // Subproblem: build the right subtree\n    root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfunc buildTree(preorder, inorder []int) *TreeNode {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    inorderMap := make(map[int]int, len(inorder))\n    for i := 0; i < len(inorder); i++ {\n        inorderMap[inorder[i]] = i\n    }\n\n    root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)\n    return root\n}\n
build_tree.swift
/* Build binary tree: divide and conquer */\nfunc dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {\n    // Terminate when the subtree interval is empty\n    if r - l < 0 {\n        return nil\n    }\n    // Initialize the root node\n    let root = TreeNode(x: preorder[i])\n    // Query m to divide the left and right subtrees\n    let m = inorderMap[preorder[i]]!\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfunc buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }\n    return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)\n}\n
build_tree.js
/* Build binary tree: divide and conquer */\nfunction dfs(preorder, inorderMap, i, l, r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null;\n    // Initialize the root node\n    const root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    const m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nfunction buildTree(preorder, inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    let inorderMap = new Map();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.ts
/* Build binary tree: divide and conquer */\nfunction dfs(\n    preorder: number[],\n    inorderMap: Map<number, number>,\n    i: number,\n    l: number,\n    r: number\n): TreeNode | null {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null;\n    // Initialize the root node\n    const root: TreeNode = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    const m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nfunction buildTree(preorder: number[], inorder: number[]): TreeNode | null {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    let inorderMap = new Map<number, number>();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.dart
/* Build binary tree: divide and conquer */\nTreeNode? dfs(\n  List<int> preorder,\n  Map<int, int> inorderMap,\n  int i,\n  int l,\n  int r,\n) {\n  // Terminate when the subtree interval is empty\n  if (r - l < 0) {\n    return null;\n  }\n  // Initialize the root node\n  TreeNode? root = TreeNode(preorder[i]);\n  // Query m to divide the left and right subtrees\n  int m = inorderMap[preorder[i]]!;\n  // Subproblem: build the left subtree\n  root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n  // Subproblem: build the right subtree\n  root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n  // Return the root node\n  return root;\n}\n\n/* Build binary tree */\nTreeNode? buildTree(List<int> preorder, List<int> inorder) {\n  // Initialize hash map, storing the mapping from inorder elements to indices\n  Map<int, int> inorderMap = {};\n  for (int i = 0; i < inorder.length; i++) {\n    inorderMap[inorder[i]] = i;\n  }\n  TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n  return root;\n}\n
build_tree.rs
/* Build binary tree: divide and conquer */\nfn dfs(\n    preorder: &[i32],\n    inorder_map: &HashMap<i32, i32>,\n    i: i32,\n    l: i32,\n    r: i32,\n) -> Option<Rc<RefCell<TreeNode>>> {\n    // Terminate when the subtree interval is empty\n    if r - l < 0 {\n        return None;\n    }\n    // Initialize the root node\n    let root = TreeNode::new(preorder[i as usize]);\n    // Query m to divide the left and right subtrees\n    let m = inorder_map.get(&preorder[i as usize]).unwrap();\n    // Subproblem: build the left subtree\n    root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    Some(root)\n}\n\n/* Build binary tree */\nfn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    let mut inorder_map: HashMap<i32, i32> = HashMap::new();\n    for i in 0..inorder.len() {\n        inorder_map.insert(inorder[i], i as i32);\n    }\n    let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);\n    root\n}\n
build_tree.c
/* Build binary tree: divide and conquer */\nTreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return NULL;\n    // Initialize the root node\n    TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));\n    root->val = preorder[i];\n    root->left = NULL;\n    root->right = NULL;\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);\n    // Subproblem: build the right subtree\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);\n    for (int i = 0; i < inorderSize; i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);\n    free(inorderMap);\n    return root;\n}\n
build_tree.kt
/* Build binary tree: divide and conquer */\nfun dfs(\n    preorder: IntArray,\n    inorderMap: Map<Int?, Int?>,\n    i: Int,\n    l: Int,\n    r: Int\n): TreeNode? {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null\n    // Initialize the root node\n    val root = TreeNode(preorder[i])\n    // Query m to divide the left and right subtrees\n    val m = inorderMap[preorder[i]]!!\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    val inorderMap = HashMap<Int?, Int?>()\n    for (i in inorder.indices) {\n        inorderMap[inorder[i]] = i\n    }\n    val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)\n    return root\n}\n
build_tree.rb
### Build binary tree: divide and conquer ###\ndef dfs(preorder, inorder_map, i, l, r)\n  # Terminate when the subtree interval is empty\n  return if r - l < 0\n\n  # Initialize the root node\n  root = TreeNode.new(preorder[i])\n  # Query m to divide the left and right subtrees\n  m = inorder_map[preorder[i]]\n  # Subproblem: build the left subtree\n  root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n  # Subproblem: build the right subtree\n  root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n\n  # Return the root node\n  root\nend\n\n### Build binary tree ###\ndef build_tree(preorder, inorder)\n  # Initialize hash map, storing the mapping from inorder elements to indices\n  inorder_map = {}\n  inorder.each_with_index { |val, i| inorder_map[val] = i }\n  dfs(preorder, inorder_map, 0, 0, inorder.length - 1)\nend\n

Figure 12-8 shows the recursive process of building the binary tree. Each node is established during the downward \"recursion\" process, while each edge (reference) is established during the upward \"return\" process.

<1><2><3><4><5><6><7><8><9>

Figure 12-8   Recursive process of building a binary tree

The division results of the preorder traversal preorder and inorder traversal inorder within each recursive function are shown in Figure 12-9.

Figure 12-9   Division results in each recursive function

Let the number of nodes in the tree be \\(n\\). Initializing each node (executing one recursive function dfs()) takes \\(O(1)\\) time. Therefore, the overall time complexity is \\(O(n)\\).

The hash table stores the mapping from inorder elements to their indices, with a space complexity of \\(O(n)\\). In the worst case, when the binary tree degenerates into a linked list, the recursion depth reaches \\(n\\), using \\(O(n)\\) stack frame space. Therefore, the overall space complexity is \\(O(n)\\).

","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/","level":1,"title":"12.1   Divide and Conquer Algorithms","text":"

Divide and conquer is a very important and common algorithm strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: \"divide\" and \"conquer\".

  1. Divide (partition phase): Recursively divide the original problem into two or more subproblems until the smallest subproblem is reached.
  2. Conquer (merge phase): Starting from the smallest subproblems with known solutions, merge the solutions of subproblems from bottom to top to construct the solution to the original problem.

As shown in Figure 12-1, \"merge sort\" is one of the typical applications of the divide and conquer strategy.

  1. Divide: Recursively divide the original array (original problem) into two subarrays (subproblems) until the subarray has only one element (smallest subproblem).
  2. Conquer: Merge the sorted subarrays (solutions to subproblems) from bottom to top to obtain a sorted original array (solution to the original problem).

Figure 12-1   Divide and conquer strategy of merge sort

","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1211-how-to-determine-divide-and-conquer-problems","level":2,"title":"12.1.1   How to Determine Divide and Conquer Problems","text":"

Whether a problem is suitable for solving with divide and conquer can usually be determined based on the following criteria.

  1. The problem can be decomposed: The original problem can be divided into smaller, similar subproblems, and can be recursively divided in the same way.
  2. Subproblems are independent: There is no overlap between subproblems, they are independent of each other and can be solved independently.
  3. Solutions of subproblems can be merged: The solution to the original problem is obtained by merging the solutions of subproblems.

Clearly, merge sort satisfies these three criteria.

  1. The problem can be decomposed: Recursively divide the array (original problem) into two subarrays (subproblems).
  2. Subproblems are independent: Each subarray can be sorted independently (subproblems can be solved independently).
  3. Solutions of subproblems can be merged: Two sorted subarrays (solutions of subproblems) can be merged into one sorted array (solution of the original problem).
","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212-improving-efficiency-through-divide-and-conquer","level":2,"title":"12.1.2   Improving Efficiency Through Divide and Conquer","text":"

Divide and conquer can not only effectively solve algorithmic problems but often also improve algorithm efficiency. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy.

This raises the question: Why can divide and conquer improve algorithm efficiency, and what is the underlying logic? In other words, why is dividing a large problem into multiple subproblems, solving the subproblems, and merging their solutions more efficient than directly solving the original problem? This question can be discussed from two aspects: operation count and parallel computation.

","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1-operation-count-optimization","level":3,"title":"1.   Operation Count Optimization","text":"

Taking \"bubble sort\" as an example, processing an array of length \\(n\\) requires \\(O(n^2)\\) time. Suppose we divide the array into two subarrays from the midpoint as shown in Figure 12-2, the division requires \\(O(n)\\) time, sorting each subarray requires \\(O((n / 2)^2)\\) time, and merging the two subarrays requires \\(O(n)\\) time, resulting in an overall time complexity of:

\\[ O(n + (\\frac{n}{2})^2 \\times 2 + n) = O(\\frac{n^2}{2} + 2n) \\]

Figure 12-2   Bubble sort before and after array division

Next, we compute the following inequality, where the left and right sides represent the total number of operations before and after division, respectively:

\\[ \\begin{aligned} n^2 & > \\frac{n^2}{2} + 2n \\newline n^2 - \\frac{n^2}{2} - 2n & > 0 \\newline n(n - 4) & > 0 \\end{aligned} \\]

This means that when \\(n > 4\\), the number of operations after division is smaller, and sorting efficiency should be higher. Note that the time complexity after division is still quadratic \\(O(n^2)\\), but the constant term in the complexity has become smaller.

Going further, what if we continuously divide the subarrays from their midpoints into two subarrays until the subarrays have only one element? This approach is actually \"merge sort\", with a time complexity of \\(O(n \\log n)\\).

Thinking further, what if we set multiple division points and evenly divide the original array into \\(k\\) subarrays? This situation is very similar to \"bucket sort\", which is well-suited for sorting massive amounts of data, with a theoretical time complexity of \\(O(n + k)\\).

","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#2-parallel-computation-optimization","level":3,"title":"2.   Parallel Computation Optimization","text":"

We know that the subproblems generated by divide and conquer are independent of each other, so they can typically be solved in parallel. This means divide and conquer can not only reduce the time complexity of algorithms, but also benefits from parallel optimization by operating systems.

Parallel optimization is particularly effective in multi-core or multi-processor environments, as the system can simultaneously handle multiple subproblems, making fuller use of computing resources and significantly reducing overall runtime.

For example, in the \"bucket sort\" shown in Figure 12-3, we evenly distribute massive data into various buckets, and the sorting tasks for all buckets can be distributed to various computing units. After completion, the results are merged.

Figure 12-3   Parallel computation in bucket sort

","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1213-common-applications-of-divide-and-conquer","level":2,"title":"12.1.3   Common Applications of Divide and Conquer","text":"

On one hand, divide and conquer can be used to solve many classic algorithmic problems.

  • Finding the closest pair of points: This algorithm first divides the point set into two parts, then finds the closest pair of points in each part separately, and finally finds the closest pair of points that spans both parts.
  • Large integer multiplication: For example, the Karatsuba algorithm, which decomposes large integer multiplication into several smaller integer multiplications and additions.
  • Matrix multiplication: For example, the Strassen algorithm, which decomposes large matrix multiplication into multiple small matrix multiplications and additions.
  • Hanota problem: The hanota problem can be solved through recursion, which is a typical application of the divide and conquer strategy.
  • Solving inversion pairs: In a sequence, if a preceding number is greater than a following number, these two numbers form an inversion pair. Solving the inversion pair problem can utilize the divide and conquer approach with the help of merge sort.

On the other hand, divide and conquer is widely applied in the design of algorithms and data structures.

  • Binary search: Binary search divides a sorted array into two parts from the midpoint index, then decides which half to eliminate based on the comparison result between the target value and the middle element value, and performs the same binary operation on the remaining interval.
  • Merge sort: Already introduced at the beginning of this section, no further elaboration needed.
  • Quick sort: Quick sort selects a pivot value, then divides the array into two subarrays, one with elements smaller than the pivot and the other with elements larger than the pivot, then performs the same division operation on these two parts until the subarrays have only one element.
  • Bucket sort: The basic idea of bucket sort is to scatter data into multiple buckets, then sort the elements within each bucket, and finally extract the elements from each bucket in sequence to obtain a sorted array.
  • Trees: For example, binary search trees, AVL trees, red-black trees, B-trees, B+ trees, etc. Their search, insertion, and deletion operations can all be viewed as applications of the divide and conquer strategy.
  • Heaps: A heap is a special complete binary tree, and its various operations, such as insertion, deletion, and heapify, actually imply the divide and conquer idea.
  • Hash tables: Although hash tables do not directly apply divide and conquer, some hash collision resolution solutions indirectly apply the divide and conquer strategy. For example, long linked lists in chaining may be converted to red-black trees to improve query efficiency.

It can be seen that divide and conquer is a \"subtly pervasive\" algorithmic idea, embedded in various algorithms and data structures.

","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   Hanota Problem","text":"

In merge sort and building binary trees, we decompose the original problem into two subproblems, each half the size of the original problem. However, for the hanota problem, we adopt a different decomposition strategy.

Question

Given three pillars, denoted as A, B, and C. Initially, pillar A has \\(n\\) discs stacked on it, arranged from top to bottom in ascending order of size. Our task is to move these \\(n\\) discs to pillar C while maintaining their original order (as shown in Figure 12-10). The following rules must be followed when moving the discs.

  1. A disc can only be taken from the top of one pillar and placed on top of another pillar.
  2. Only one disc can be moved at a time.
  3. A smaller disc must always be on top of a larger disc.

Figure 12-10   Example of the hanota problem

We denote the hanota problem of size \\(i\\) as \\(f(i)\\). For example, \\(f(3)\\) represents moving \\(3\\) discs from A to C.

","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#1-considering-the-base-cases","level":3,"title":"1.   Considering the Base Cases","text":"

As shown in Figure 12-11, for problem \\(f(1)\\), when there is only one disc, we can move it directly from A to C.

<1><2>

Figure 12-11   Solution for a problem of size 1

As shown in Figure 12-12, for problem \\(f(2)\\), when there are two discs, since we must always keep the smaller disc on top of the larger disc, we need to use B to assist in the move.

  1. First, move the smaller disc from A to B.
  2. Then move the larger disc from A to C.
  3. Finally, move the smaller disc from B to C.
<1><2><3><4>

Figure 12-12   Solution for a problem of size 2

The process of solving problem \\(f(2)\\) can be summarized as: moving two discs from A to C with the help of B. Here, C is called the target pillar, and B is called the buffer pillar.

","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2-subproblem-decomposition","level":3,"title":"2.   Subproblem Decomposition","text":"

For problem \\(f(3)\\), when there are three discs, the situation becomes slightly more complex.

Since we already know the solutions to \\(f(1)\\) and \\(f(2)\\), we can think from a divide and conquer perspective, treating the top two discs on A as a whole, and execute the steps shown in Figure 12-13. This successfully moves the three discs from A to C.

  1. Let B be the target pillar and C be the buffer pillar, and move two discs from A to B.
  2. Move the remaining disc from A directly to C.
  3. Let C be the target pillar and A be the buffer pillar, and move two discs from B to C.
<1><2><3><4>

Figure 12-13   Solution for a problem of size 3

Essentially, we divide problem \\(f(3)\\) into two subproblems \\(f(2)\\) and one subproblem \\(f(1)\\). By solving these three subproblems in order, the original problem is solved. This shows that the subproblems are independent and their solutions can be merged.

From this, we can summarize the divide and conquer strategy for solving the hanota problem shown in Figure 12-14: divide the original problem \\(f(n)\\) into two subproblems \\(f(n-1)\\) and one subproblem \\(f(1)\\), and solve these three subproblems in the following order.

  1. Move \\(n-1\\) discs from A to B with the help of C.
  2. Move the remaining \\(1\\) disc directly from A to C.
  3. Move \\(n-1\\) discs from B to C with the help of A.

For these two subproblems \\(f(n-1)\\), we can recursively divide them in the same way until reaching the smallest subproblem \\(f(1)\\). The solution to \\(f(1)\\) is known and requires only one move operation.

Figure 12-14   Divide and conquer strategy for solving the hanota problem

","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

In the code, we declare a recursive function dfs(i, src, buf, tar), whose purpose is to move the top \\(i\\) discs from pillar src to target pillar tar with the help of buffer pillar buf:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py
def move(src: list[int], tar: list[int]):\n    \"\"\"Move a disk\"\"\"\n    # Take out a disk from the top of src\n    pan = src.pop()\n    # Place the disk on top of tar\n    tar.append(pan)\n\ndef dfs(i: int, src: list[int], buf: list[int], tar: list[int]):\n    \"\"\"Solve the Tower of Hanoi problem f(i)\"\"\"\n    # If there is only one disk left in src, move it directly to tar\n    if i == 1:\n        move(src, tar)\n        return\n    # Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf)\n    # Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    # Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar)\n\ndef solve_hanota(A: list[int], B: list[int], C: list[int]):\n    \"\"\"Solve the Tower of Hanoi problem\"\"\"\n    n = len(A)\n    # Move the top n disks from A to C using B\n    dfs(n, A, B, C)\n
hanota.cpp
/* Move a disk */\nvoid move(vector<int> &src, vector<int> &tar) {\n    // Take out a disk from the top of src\n    int pan = src.back();\n    src.pop_back();\n    // Place the disk on top of tar\n    tar.push_back(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {\n    int n = A.size();\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
hanota.java
/* Move a disk */\nvoid move(List<Integer> src, List<Integer> tar) {\n    // Take out a disk from the top of src\n    Integer pan = src.remove(src.size() - 1);\n    // Place the disk on top of tar\n    tar.add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {\n    int n = A.size();\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
hanota.cs
/* Move a disk */\nvoid Move(List<int> src, List<int> tar) {\n    // Take out a disk from the top of src\n    int pan = src[^1];\n    src.RemoveAt(src.Count - 1);\n    // Place the disk on top of tar\n    tar.Add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid DFS(int i, List<int> src, List<int> buf, List<int> tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        Move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    DFS(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    Move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    DFS(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid SolveHanota(List<int> A, List<int> B, List<int> C) {\n    int n = A.Count;\n    // Move the top n disks from A to C using B\n    DFS(n, A, B, C);\n}\n
hanota.go
/* Move a disk */\nfunc move(src, tar *list.List) {\n    // Take out a disk from the top of src\n    pan := src.Back()\n    // Place the disk on top of tar\n    tar.PushBack(pan.Value)\n    // Remove top disk from src\n    src.Remove(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunc dfsHanota(i int, src, buf, tar *list.List) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move(src, tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfsHanota(i-1, src, tar, buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // Move the top n disks from A to C using B\n    dfsHanota(n, A, B, C)\n}\n
hanota.swift
/* Move a disk */\nfunc move(src: inout [Int], tar: inout [Int]) {\n    // Take out a disk from the top of src\n    let pan = src.popLast()!\n    // Place the disk on top of tar\n    tar.append(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunc dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move(src: &src, tar: &tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src: &src, tar: &tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfunc solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {\n    let n = A.count\n    // The tail of the list is the top of the rod\n    // Move top n disks from src to C using B\n    dfs(i: n, src: &A, buf: &B, tar: &C)\n}\n
hanota.js
/* Move a disk */\nfunction move(src, tar) {\n    // Take out a disk from the top of src\n    const pan = src.pop();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunction dfs(i, src, buf, tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
hanota.ts
/* Move a disk */\nfunction move(src: number[], tar: number[]): void {\n    // Take out a disk from the top of src\n    const pan = src.pop();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunction dfs(i: number, src: number[], buf: number[], tar: number[]): void {\n    // If there is only one disk left in src, move it directly to tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfunction solveHanota(A: number[], B: number[], C: number[]): void {\n    const n = A.length;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
hanota.dart
/* Move a disk */\nvoid move(List<int> src, List<int> tar) {\n  // Take out a disk from the top of src\n  int pan = src.removeLast();\n  // Place the disk on top of tar\n  tar.add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, List<int> src, List<int> buf, List<int> tar) {\n  // If there is only one disk left in src, move it directly to tar\n  if (i == 1) {\n    move(src, tar);\n    return;\n  }\n  // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n  dfs(i - 1, src, tar, buf);\n  // Subproblem f(1): move the remaining disk from src to tar\n  move(src, tar);\n  // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n  dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(List<int> A, List<int> B, List<int> C) {\n  int n = A.length;\n  // Move the top n disks from A to C using B\n  dfs(n, A, B, C);\n}\n
hanota.rs
/* Move a disk */\nfn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // Take out a disk from the top of src\n    let pan = src.pop().unwrap();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move_pan(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move_pan(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {\n    let n = A.len() as i32;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
hanota.c
/* Move a disk */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n    // Take out a disk from the top of src\n    int pan = src[*srcSize - 1];\n    src[*srcSize - 1] = 0;\n    (*srcSize)--;\n    // Place the disk on top of tar\n    tar[*tarSize] = pan;\n    (*tarSize)++;\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, srcSize, tar, tarSize);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, srcSize, tar, tarSize);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {\n    // Move the top n disks from A to C using B\n    dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n
hanota.kt
/* Move a disk */\nfun move(src: MutableList<Int>, tar: MutableList<Int>) {\n    // Take out a disk from the top of src\n    val pan = src.removeAt(src.size - 1)\n    // Place the disk on top of tar\n    tar.add(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {\n    val n = A.size\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C)\n}\n
hanota.rb
### Move one disk ###\ndef move(src, tar)\n  # Take out a disk from the top of src\n  pan = src.pop\n  # Place the disk on top of tar\n  tar << pan\nend\n\n### Solve Tower of Hanoi f(i) ###\ndef dfs(i, src, buf, tar)\n  # If there is only one disk left in src, move it directly to tar\n  if i == 1\n    move(src, tar)\n    return\n  end\n\n  # Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n  dfs(i - 1, src, tar, buf)\n  # Subproblem f(1): move the remaining disk from src to tar\n  move(src, tar)\n  # Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n  dfs(i - 1, buf, src, tar)\nend\n\n### Solve Tower of Hanoi ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # Move the top n disks from A to C using B\n  dfs(n, _A, _B, _C)\nend\n

As shown in Figure 12-15, the hanota problem forms a recursion tree of height \\(n\\), where each node represents a subproblem corresponding to an invocation of the dfs() function, therefore the time complexity is \\(O(2^n)\\) and the space complexity is \\(O(n)\\).

Figure 12-15   Recursion tree of the hanota problem

Quote

The hanota problem originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and \\(64\\) golden discs of different sizes. The monks continuously moved the discs, believing that when the last disc was correctly placed, the world would come to an end.

However, even if the monks moved one disc per second, it would take approximately \\(2^{64} \\approx 1.84×10^{19}\\) seconds, which is about \\(5850\\) billion years, far exceeding current estimates of the age of the universe. Therefore, if this legend is true, we should not need to worry about the end of the world.

","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/","level":1,"title":"12.5   Summary","text":"","path":["Chapter 12. Divide and Conquer","12.5   Summary"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Divide and conquer is a common algorithm design strategy, consisting of two phases: divide (partition) and conquer (merge), typically implemented based on recursion.
  • The criteria for determining whether a problem is a divide and conquer problem include: whether the problem can be decomposed, whether subproblems are independent, and whether subproblems can be merged.
  • Merge sort is a typical application of the divide and conquer strategy. It recursively divides an array into two equal-length subarrays until only one element remains, then merges them layer by layer to complete the sorting.
  • Introducing the divide and conquer strategy can often improve algorithm efficiency. On one hand, the divide and conquer strategy reduces the number of operations; on the other hand, it facilitates parallel optimization of the system after division.
  • Divide and conquer can both solve many algorithmic problems and is widely applied in data structure and algorithm design, appearing everywhere.
  • Compared to brute-force search, adaptive search is more efficient. Search algorithms with time complexity of \\(O(\\log n)\\) are typically implemented based on the divide and conquer strategy.
  • Binary search is another typical application of divide and conquer. It does not include the step of merging solutions of subproblems. We can implement binary search through recursive divide and conquer.
  • In the problem of building a binary tree, building the tree (original problem) can be divided into building the left subtree and right subtree (subproblems), which can be achieved by dividing the index intervals of the preorder and inorder traversals.
  • In the hanota problem, a problem of size \\(n\\) can be divided into two subproblems of size \\(n-1\\) and one subproblem of size \\(1\\). After solving these three subproblems in order, the original problem is solved.
","path":["Chapter 12. Divide and Conquer","12.5   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"Chapter 14.   Dynamic Programming","text":"

Abstract

Streams converge into rivers, rivers converge into the sea.

Dynamic programming gathers solutions to small problems into answers to large problems, step by step guiding us to the shore of problem-solving.

","path":["Chapter 14. Dynamic Programming","Chapter 14.   Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 14.1   Introduction to Dynamic Programming
  • 14.2   Characteristics of Dynamic Programming Problems
  • 14.3   Dynamic Programming Problem-Solving Approach
  • 14.4   0-1 Knapsack Problem
  • 14.5   Unbounded Knapsack Problem
  • 14.6   Edit Distance Problem
  • 14.7   Summary
","path":["Chapter 14. Dynamic Programming","Chapter 14.   Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/","level":1,"title":"14.2   Characteristics of Dynamic Programming Problems","text":"

In the previous section, we learned how dynamic programming solves the original problem by decomposing it into subproblems. In fact, subproblem decomposition is a general algorithmic approach, with different emphases in divide and conquer, dynamic programming, and backtracking.

  • Divide and conquer algorithms recursively divide the original problem into multiple independent subproblems until the smallest subproblems are reached, and merge the solutions to the subproblems during backtracking to ultimately obtain the solution to the original problem.
  • Dynamic programming also recursively decomposes problems, but the main difference from divide and conquer algorithms is that subproblems in dynamic programming are interdependent, and many overlapping subproblems appear during the decomposition process.
  • Backtracking algorithms enumerate all possible solutions through trial and error, and avoid unnecessary search branches through pruning. The solution to the original problem consists of a series of decision steps, and we can regard the subsequence before each decision step as a subproblem.

In fact, dynamic programming is commonly used to solve optimization problems, which not only contain overlapping subproblems but also have two other major characteristics: optimal substructure and no aftereffects.

","path":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1421-optimal-substructure","level":2,"title":"14.2.1   Optimal Substructure","text":"

We make a slight modification to the stair climbing problem to make it more suitable for demonstrating the concept of optimal substructure.

Climbing stairs with minimum cost

Given a staircase, where you can climb \\(1\\) or \\(2\\) steps at a time, and each step has a non-negative integer representing the cost you need to pay at that step. Given a non-negative integer array \\(cost\\), where \\(cost[i]\\) represents the cost at the \\(i\\)-th step, and \\(cost[0]\\) is the ground (starting point). What is the minimum cost required to reach the top?

As shown in Figure 14-6, if the costs of the \\(1\\)st, \\(2\\)nd, and \\(3\\)rd steps are \\(1\\), \\(10\\), and \\(1\\) respectively, then climbing from the ground to the \\(3\\)rd step requires a minimum cost of \\(2\\).

Figure 14-6   Minimum cost to climb to the 3rd step

Let \\(dp[i]\\) be the accumulated cost of climbing to the \\(i\\)-th step. Since the \\(i\\)-th step can only come from the \\(i-1\\)-th or \\(i-2\\)-th step, \\(dp[i]\\) can only equal \\(dp[i-1] + cost[i]\\) or \\(dp[i-2] + cost[i]\\). To minimize the cost, we should choose the smaller of the two:

\\[ dp[i] = \\min(dp[i-1], dp[i-2]) + cost[i] \\]

This leads us to the meaning of optimal substructure: the optimal solution to the original problem is constructed from the optimal solutions to the subproblems.

This problem clearly has optimal substructure: we select the better one from the optimal solutions to the two subproblems \\(dp[i-1]\\) and \\(dp[i-2]\\), and use it to construct the optimal solution to the original problem \\(dp[i]\\).

So, does the stair climbing problem from the previous section have optimal substructure? Its goal is to find the number of ways, which seems to be a counting problem, but if we change the question: \"Find the maximum number of ways\". We surprisingly discover that although the problem before and after modification are equivalent, the optimal substructure has emerged: the maximum number of ways for the \\(n\\)-th step equals the sum of the maximum number of ways for the \\(n-1\\)-th and \\(n-2\\)-th steps. Therefore, the interpretation of optimal substructure is quite flexible and will have different meanings in different problems.

According to the state transition equation and the initial states \\(dp[1] = cost[1]\\) and \\(dp[2] = cost[2]\\), we can obtain the dynamic programming code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
def min_cost_climbing_stairs_dp(cost: list[int]) -> int:\n    \"\"\"Minimum cost climbing stairs: Dynamic programming\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [0] * (n + 1)\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1], dp[2] = cost[1], cost[2]\n    # State transition: gradually solve larger subproblems from smaller ones\n    for i in range(3, n + 1):\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    return dp[n]\n
min_cost_climbing_stairs_dp.cpp
/* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    vector<int> dp(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.java
/* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.cs
/* Minimum cost climbing stairs: Dynamic programming */\nint MinCostClimbingStairsDP(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.go
/* Minimum cost climbing stairs: Dynamic programming */\nfunc minCostClimbingStairsDP(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    dp := make([]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        dp[i] = min(dp[i-1], dp[i-2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.swift
/* Minimum cost climbing stairs: Dynamic programming */\nfunc minCostClimbingStairsDP(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: 0, count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3 ... n {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.js
/* Minimum cost climbing stairs: Dynamic programming */\nfunction minCostClimbingStairsDP(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.ts
/* Minimum cost climbing stairs: Dynamic programming */\nfunction minCostClimbingStairsDP(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.dart
/* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  // Initialize dp table, used to store solutions to subproblems\n  List<int> dp = List.filled(n + 1, 0);\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1] = cost[1];\n  dp[2] = cost[2];\n  // State transition: gradually solve larger subproblems from smaller ones\n  for (int i = 3; i <= n; i++) {\n    dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n  }\n  return dp[n];\n}\n
min_cost_climbing_stairs_dp.rs
/* Minimum cost climbing stairs: Dynamic programming */\nfn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![-1; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3..=n {\n        dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    dp[n]\n}\n
min_cost_climbing_stairs_dp.c
/* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int *dp = calloc(n + 1, sizeof(int));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    int res = dp[n];\n    // Free memory\n    free(dp);\n    return res;\n}\n
min_cost_climbing_stairs_dp.kt
/* Minimum cost climbing stairs: Dynamic programming */\nfun minCostClimbingStairsDP(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = IntArray(n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (i in 3..n) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.rb
### Minimum cost climbing stairs: DP ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = cost[1], cost[2]\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n

Figure 14-7 shows the dynamic programming process for the above code.

Figure 14-7   Dynamic programming process for climbing stairs with minimum cost

This problem can also be space-optimized, compressing from one dimension to zero, reducing the space complexity from \\(O(n)\\) to \\(O(1)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:\n    \"\"\"Minimum cost climbing stairs: Space-optimized dynamic programming\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    a, b = cost[1], cost[2]\n    for i in range(3, n + 1):\n        a, b = b, min(a, b) + cost[i]\n    return b\n
min_cost_climbing_stairs_dp.cpp
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nint minCostClimbingStairsDPComp(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.java
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nint minCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.cs
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nint MinCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.Min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.go
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfunc minCostClimbingStairsDPComp(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // Initial state: preset the solution to the smallest subproblem\n    a, b := cost[1], cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        tmp := b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.swift
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfunc minCostClimbingStairsDPComp(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    var (a, b) = (cost[1], cost[2])\n    for i in 3 ... n {\n        (a, b) = (b, min(a, b) + cost[i])\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.js
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfunction minCostClimbingStairsDPComp(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.ts
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfunction minCostClimbingStairsDPComp(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.dart
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nint minCostClimbingStairsDPComp(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  int a = cost[1], b = cost[2];\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = min(a, tmp) + cost[i];\n    a = tmp;\n  }\n  return b;\n}\n
min_cost_climbing_stairs_dp.rs
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    };\n    let (mut a, mut b) = (cost[1], cost[2]);\n    for i in 3..=n {\n        let tmp = b;\n        b = cmp::min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    b\n}\n
min_cost_climbing_stairs_dp.c
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nint minCostClimbingStairsDPComp(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = myMin(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.kt
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfun minCostClimbingStairsDPComp(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    var a = cost[1]\n    var b = cost[2]\n    for (i in 3..n) {\n        val tmp = b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.rb
### Minimum cost climbing stairs: DP ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = cost[1], cost[2]\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n\n# Minimum cost climbing stairs: Space-optimized dynamic programming\ndef min_cost_climbing_stairs_dp_comp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  a, b = cost[1], cost[2]\n  (3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] }\n  b\nend\n
","path":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1422-no-aftereffects","level":2,"title":"14.2.2   No Aftereffects","text":"

No aftereffects is one of the important characteristics that enable dynamic programming to solve problems effectively. Its definition is: given a certain state, its future development is only related to the current state and has nothing to do with all past states.

Taking the stair climbing problem as an example, given state \\(i\\), it will develop into states \\(i+1\\) and \\(i+2\\), corresponding to jumping \\(1\\) step and jumping \\(2\\) steps, respectively. When making these two choices, we do not need to consider the states before state \\(i\\), as they have no effect on the future of state \\(i\\).

However, if we add a constraint to the stair climbing problem, the situation changes.

Climbing stairs with constraint

Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time, but you cannot jump \\(1\\) step in two consecutive rounds. How many ways are there to climb to the top?

As shown in Figure 14-8, there are only \\(2\\) feasible ways to climb to the \\(3\\)rd step. The way of jumping \\(1\\) step three consecutive times does not satisfy the constraint and is therefore discarded.

Figure 14-8   Number of ways to climb to the 3rd step with constraint

In this problem, if the previous round was a jump of \\(1\\) step, then the next round must jump \\(2\\) steps. This means that the next choice cannot be determined solely by the current state (current stair step number), but also depends on the previous state (the stair step number from the previous round).

It is not difficult to see that this problem no longer satisfies no aftereffects, and the state transition equation \\(dp[i] = dp[i-1] + dp[i-2]\\) also fails, because \\(dp[i-1]\\) represents jumping \\(1\\) step in this round, but it includes many solutions where \"the previous round was a jump of \\(1\\) step\", which cannot be directly counted in \\(dp[i]\\) to satisfy the constraint.

For this reason, we need to expand the state definition: state \\([i, j]\\) represents being on the \\(i\\)-th step with the previous round having jumped \\(j\\) steps, where \\(j \\in \\{1, 2\\}\\). This state definition effectively distinguishes whether the previous round was a jump of \\(1\\) step or \\(2\\) steps, allowing us to determine where the current state came from.

  • When the previous round jumped \\(1\\) step, the round before that could only choose to jump \\(2\\) steps, i.e., \\(dp[i, 1]\\) can only be transferred from \\(dp[i-1, 2]\\).
  • When the previous round jumped \\(2\\) steps, the round before that could choose to jump \\(1\\) step or \\(2\\) steps, i.e., \\(dp[i, 2]\\) can be transferred from \\(dp[i-2, 1]\\) or \\(dp[i-2, 2]\\).

As shown in Figure 14-9, under this definition, \\(dp[i, j]\\) represents the number of ways for state \\([i, j]\\). The state transition equation is then:

\\[ \\begin{cases} dp[i, 1] = dp[i-1, 2] \\\\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \\end{cases} \\]

Figure 14-9   Recurrence relation considering constraints

Finally, return \\(dp[n, 1] + dp[n, 2]\\), where the sum of the two represents the total number of ways to climb to the \\(n\\)-th step:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_constraint_dp.py
def climbing_stairs_constraint_dp(n: int) -> int:\n    \"\"\"Climbing stairs with constraint: Dynamic programming\"\"\"\n    if n == 1 or n == 2:\n        return 1\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [[0] * 3 for _ in range(n + 1)]\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1][1], dp[1][2] = 1, 0\n    dp[2][1], dp[2][2] = 0, 1\n    # State transition: gradually solve larger subproblems from smaller ones\n    for i in range(3, n + 1):\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    return dp[n][1] + dp[n][2]\n
climbing_stairs_constraint_dp.cpp
/* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    vector<vector<int>> dp(n + 1, vector<int>(3, 0));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.java
/* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    int[][] dp = new int[n + 1][3];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.cs
/* Climbing stairs with constraint: Dynamic programming */\nint ClimbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    int[,] dp = new int[n + 1, 3];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1, 1] = 1;\n    dp[1, 2] = 0;\n    dp[2, 1] = 0;\n    dp[2, 2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i, 1] = dp[i - 1, 2];\n        dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];\n    }\n    return dp[n, 1] + dp[n, 2];\n}\n
climbing_stairs_constraint_dp.go
/* Climbing stairs with constraint: Dynamic programming */\nfunc climbingStairsConstraintDP(n int) int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    dp := make([][3]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        dp[i][1] = dp[i-1][2]\n        dp[i][2] = dp[i-2][1] + dp[i-2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.swift
/* Climbing stairs with constraint: Dynamic programming */\nfunc climbingStairsConstraintDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3 ... n {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.js
/* Climbing stairs with constraint: Dynamic programming */\nfunction climbingStairsConstraintDP(n) {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = Array.from(new Array(n + 1), () => new Array(3));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.ts
/* Climbing stairs with constraint: Dynamic programming */\nfunction climbingStairsConstraintDP(n: number): number {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = Array.from({ length: n + 1 }, () => new Array(3));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.dart
/* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n  if (n == 1 || n == 2) {\n    return 1;\n  }\n  // Initialize dp table, used to store solutions to subproblems\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1][1] = 1;\n  dp[1][2] = 0;\n  dp[2][1] = 0;\n  dp[2][2] = 1;\n  // State transition: gradually solve larger subproblems from smaller ones\n  for (int i = 3; i <= n; i++) {\n    dp[i][1] = dp[i - 1][2];\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n  }\n  return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.rs
/* Climbing stairs with constraint: Dynamic programming */\nfn climbing_stairs_constraint_dp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return 1;\n    };\n    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![vec![-1; 3]; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3..=n {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.c
/* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(3, sizeof(int));\n    }\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    int res = dp[n][1] + dp[n][2];\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
climbing_stairs_constraint_dp.kt
/* Climbing stairs with constraint: Dynamic programming */\nfun climbingStairsConstraintDP(n: Int): Int {\n    if (n == 1 || n == 2) {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = Array(n + 1) { IntArray(3) }\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (i in 3..n) {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.rb
### Climbing stairs with constraint: DP ###\ndef climbing_stairs_constraint_dp(n)\n  return 1 if n == 1 || n == 2\n\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1) { Array.new(3, 0) }\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1][1], dp[1][2] = 1, 0\n  dp[2][1], dp[2][2] = 0, 1\n  # State transition: gradually solve larger subproblems from smaller ones\n  for i in 3...(n + 1)\n    dp[i][1] = dp[i - 1][2]\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n  end\n\n  dp[n][1] + dp[n][2]\nend\n

In the above case, since we only need to consider one more preceding state, we can still make the problem satisfy no aftereffects by expanding the state definition. However, some problems have very severe \"aftereffects\".

Climbing stairs with obstacle generation

Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time. It is stipulated that when climbing to the \\(i\\)-th step, the system will automatically place an obstacle on the \\(2i\\)-th step, and thereafter no round is allowed to jump to the \\(2i\\)-th step. For example, if the first two rounds jump to the \\(2\\)nd and \\(3\\)rd steps, then afterwards you cannot jump to the \\(4\\)th and \\(6\\)th steps. How many ways are there to climb to the top?

In this problem, the next jump depends on all past states, because each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming is often difficult to solve.

In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy no aftereffects. For such problems, we usually choose to use other methods, such as heuristic search, genetic algorithms, reinforcement learning, etc., to obtain usable local optimal solutions within a limited time.

","path":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   Dynamic Programming Problem-Solving Approach","text":"

The previous two sections introduced the main characteristics of dynamic programming problems. Next, let us explore two more practical issues together.

  1. How to determine whether a problem is a dynamic programming problem?
  2. What is the complete process for solving a dynamic programming problem, and where should we start?
","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431-problem-determination","level":2,"title":"14.3.1   Problem Determination","text":"

Generally speaking, if a problem contains overlapping subproblems, optimal substructure, and satisfies no aftereffects, then it is usually suitable for solving with dynamic programming. However, it is difficult to directly extract these characteristics from the problem description. Therefore, we usually relax the conditions and first observe whether the problem is suitable for solving with backtracking (exhaustive search).

Problems suitable for solving with backtracking usually satisfy the \"decision tree model\", which means the problem can be described using a tree structure, where each node represents a decision and each path represents a sequence of decisions.

In other words, if a problem contains an explicit concept of decisions, and the solution is generated through a series of decisions, then it satisfies the decision tree model and can usually be solved using backtracking.

On this basis, dynamic programming problems also have some \"bonus points\" for determination.

  • The problem contains descriptions such as maximum (minimum) or most (least), indicating optimization.
  • The problem's state can be represented using a list, multi-dimensional matrix, or tree, and a state has a recurrence relation with its surrounding states.

Correspondingly, there are also some \"penalty points\".

  • The goal of the problem is to find all possible solutions, rather than finding the optimal solution.
  • The problem description has obvious permutation and combination characteristics, requiring the return of specific multiple solutions.

If a problem satisfies the decision tree model and has relatively obvious \"bonus points\", we can assume it is a dynamic programming problem and verify it during the solving process.

","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432-problem-solving-steps","level":2,"title":"14.3.2   Problem-Solving Steps","text":"

The problem-solving process for dynamic programming varies depending on the nature and difficulty of the problem, but generally follows these steps: describe decisions, define states, establish the \\(dp\\) table, derive state transition equations, determine boundary conditions, etc.

To illustrate the problem-solving steps more vividly, we use a classic problem \"minimum path sum\" as an example.

Question

Given an \\(n \\times m\\) two-dimensional grid grid, where each cell in the grid contains a non-negative integer representing the cost of that cell. A robot starts from the top-left cell and can only move down or right at each step until reaching the bottom-right cell. Return the minimum path sum from the top-left to the bottom-right.

Figure 14-10 shows an example where the minimum path sum for the given grid is \\(13\\).

Figure 14-10   Minimum path sum example data

Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

The decision in each round of this problem is to move one step down or right from the current cell. Let the row and column indices of the current cell be \\([i, j]\\). After moving down or right, the indices become \\([i+1, j]\\) or \\([i, j+1]\\). Therefore, the state should include two variables, the row index and column index, denoted as \\([i, j]\\).

State \\([i, j]\\) corresponds to the subproblem: the minimum path sum from the starting point \\([0, 0]\\) to \\([i, j]\\), denoted as \\(dp[i, j]\\).

From this, we obtain the two-dimensional \\(dp\\) matrix shown in Figure 14-11, whose size is the same as the input grid \\(grid\\).

Figure 14-11   State definition and dp table

Note

The dynamic programming and backtracking processes can be described as a sequence of decisions, and the state consists of all decision variables. It should contain all variables describing the progress of problem-solving, and should contain sufficient information to derive the next state.

Each state corresponds to a subproblem, and we define a \\(dp\\) table to store the solutions to all subproblems. Each independent variable of the state is a dimension of the \\(dp\\) table. Essentially, the \\(dp\\) table is a mapping between states and solutions to subproblems.

Step 2: Identify the optimal substructure, and then derive the state transition equation

For state \\([i, j]\\), it can only be transferred from the cell above \\([i-1, j]\\) or the cell to the left \\([i, j-1]\\). Therefore, the optimal substructure is: the minimum path sum to reach \\([i, j]\\) is determined by the smaller of the minimum path sums of \\([i, j-1]\\) and \\([i-1, j]\\).

Based on the above analysis, the state transition equation shown in Figure 14-12 can be derived:

\\[ dp[i, j] = \\min(dp[i-1, j], dp[i, j-1]) + grid[i, j] \\]

Figure 14-12   Optimal substructure and state transition equation

Note

Based on the defined \\(dp\\) table, think about the relationship between the original problem and subproblems, and find the method to construct the optimal solution to the original problem from the optimal solutions to the subproblems, which is the optimal substructure.

Once we identify the optimal substructure, we can use it to construct the state transition equation.

Step 3: Determine boundary conditions and state transition order

In this problem, states in the first row can only come from the state to their left, and states in the first column can only come from the state above them. Therefore, the first row \\(i = 0\\) and first column \\(j = 0\\) are boundary conditions.

As shown in Figure 14-13, since each cell is transferred from the cell to its left and the cell above it, we use loops to traverse the matrix, with the outer loop traversing rows and the inner loop traversing columns.

Figure 14-13   Boundary conditions and state transition order

Note

Boundary conditions in dynamic programming are used to initialize the \\(dp\\) table, and in search are used for pruning.

The core of state transition order is to ensure that when computing the solution to the current problem, all the smaller subproblems it depends on have already been computed correctly.

Based on the above analysis, we can directly write the dynamic programming code. However, subproblem decomposition is a top-down approach, so implementing in the order \"brute force search \\(\\rightarrow\\) memoization \\(\\rightarrow\\) dynamic programming\" is more aligned with thinking habits.

","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1-method-1-brute-force-search","level":3,"title":"1.   Method 1: Brute Force Search","text":"

Starting from state \\([i, j]\\), continuously decompose into smaller states \\([i-1, j]\\) and \\([i, j-1]\\). The recursive function includes the following elements.

  • Recursive parameters: state \\([i, j]\\).
  • Return value: minimum path sum from \\([0, 0]\\) to \\([i, j]\\), which is \\(dp[i, j]\\).
  • Termination condition: when \\(i = 0\\) and \\(j = 0\\), return cost \\(grid[0, 0]\\).
  • Pruning: when \\(i < 0\\) or \\(j < 0\\), the index is out of bounds, return cost \\(+\\infty\\), representing infeasibility.

The implementation code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:\n    \"\"\"Minimum path sum: Brute-force search\"\"\"\n    # If it's the top-left cell, terminate the search\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # If row or column index is out of bounds, return +∞ cost\n    if i < 0 or j < 0:\n        return inf\n    # Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    up = min_path_sum_dfs(grid, i - 1, j)\n    left = min_path_sum_dfs(grid, i, j - 1)\n    # Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n
min_path_sum.cpp
/* Minimum path sum: Brute-force search */\nint minPathSumDFS(vector<vector<int>> &grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n}\n
min_path_sum.java
/* Minimum path sum: Brute-force search */\nint minPathSumDFS(int[][] grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.cs
/* Minimum path sum: Brute-force search */\nint MinPathSumDFS(int[][] grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = MinPathSumDFS(grid, i - 1, j);\n    int left = MinPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.Min(left, up) + grid[i][j];\n}\n
min_path_sum.go
/* Minimum path sum: Brute-force search */\nfunc minPathSumDFS(grid [][]int, i, j int) int {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    up := minPathSumDFS(grid, i-1, j)\n    left := minPathSumDFS(grid, i, j-1)\n    // Return the minimum path cost from top-left to (i, j)\n    return int(math.Min(float64(left), float64(up))) + grid[i][j]\n}\n
min_path_sum.swift
/* Minimum path sum: Brute-force search */\nfunc minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int {\n    // If it's the top-left cell, terminate the search\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    let up = minPathSumDFS(grid: grid, i: i - 1, j: j)\n    let left = minPathSumDFS(grid: grid, i: i, j: j - 1)\n    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n}\n
min_path_sum.js
/* Minimum path sum: Brute-force search */\nfunction minPathSumDFS(grid, i, j) {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.ts
/* Minimum path sum: Brute-force search */\nfunction minPathSumDFS(\n    grid: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.dart
/* Minimum path sum: Brute-force search */\nint minPathSumDFS(List<List<int>> grid, int i, int j) {\n  // If it's the top-left cell, terminate the search\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // If row or column index is out of bounds, return +∞ cost\n  if (i < 0 || j < 0) {\n    // In Dart, int type is fixed-range integer, no value representing \"infinity\"\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n  int up = minPathSumDFS(grid, i - 1, j);\n  int left = minPathSumDFS(grid, i, j - 1);\n  // Return the minimum path cost from top-left to (i, j)\n  return min(left, up) + grid[i][j];\n}\n
min_path_sum.rs
/* Minimum path sum: Brute-force search */\nfn min_path_sum_dfs(grid: &Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    let up = min_path_sum_dfs(grid, i - 1, j);\n    let left = min_path_sum_dfs(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    std::cmp::min(left, up) + grid[i as usize][j as usize]\n}\n
min_path_sum.c
/* Minimum path sum: Brute-force search */\nint minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n}\n
min_path_sum.kt
/* Minimum path sum: Brute-force search */\nfun minPathSumDFS(grid: Array<IntArray>, i: Int, j: Int): Int {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    val up = minPathSumDFS(grid, i - 1, j)\n    val left = minPathSumDFS(grid, i, j - 1)\n    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n}\n
min_path_sum.rb
### Minimum path sum: brute force search ###\ndef min_path_sum_dfs(grid, i, j)\n  # If it's the top-left cell, terminate the search\n  return grid[i][j] if i == 0 && j == 0\n  # If row or column index is out of bounds, return +∞ cost\n  return Float::INFINITY if i < 0 || j < 0\n  # Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n  up = min_path_sum_dfs(grid, i - 1, j)\n  left = min_path_sum_dfs(grid, i, j - 1)\n  # Return the minimum path cost from top-left to (i, j)\n  [left, up].min + grid[i][j]\nend\n

Figure 14-14 shows the recursion tree rooted at \\(dp[2, 1]\\), which includes some overlapping subproblems whose number will increase sharply as the size of grid grid grows.

Essentially, the reason for overlapping subproblems is: there are multiple paths from the top-left corner to reach a certain cell.

Figure 14-14   Brute force search recursion tree

Each state has two choices, down and right, so the total number of steps from the top-left corner to the bottom-right corner is \\(m + n - 2\\), giving a worst-case time complexity of \\(O(2^{m + n})\\), where \\(n\\) and \\(m\\) are the number of rows and columns of the grid, respectively. Note that this calculation does not account for situations near the grid boundaries, where only one choice remains when reaching the grid boundary, so the actual number of paths will be somewhat less.

","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-method-2-memoization","level":3,"title":"2.   Method 2: Memoization","text":"

We introduce a memo list mem of the same size as grid grid to record the solutions to subproblems and prune overlapping subproblems:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dfs_mem(\n    grid: list[list[int]], mem: list[list[int]], i: int, j: int\n) -> int:\n    \"\"\"Minimum path sum: Memoization search\"\"\"\n    # If it's the top-left cell, terminate the search\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # If row or column index is out of bounds, return +∞ cost\n    if i < 0 or j < 0:\n        return inf\n    # If there's a record, return it directly\n    if mem[i][j] != -1:\n        return mem[i][j]\n    # Minimum path cost for left and upper cells\n    up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n    left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n    # Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n
min_path_sum.cpp
/* Minimum path sum: Memoization search */\nint minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
min_path_sum.java
/* Minimum path sum: Memoization search */\nint minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.cs
/* Minimum path sum: Memoization search */\nint MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = MinPathSumDFSMem(grid, mem, i - 1, j);\n    int left = MinPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.Min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.go
/* Minimum path sum: Memoization search */\nfunc minPathSumDFSMem(grid, mem [][]int, i, j int) int {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // If there's a record, return it directly\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\n    up := minPathSumDFSMem(grid, mem, i-1, j)\n    left := minPathSumDFSMem(grid, mem, i, j-1)\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = int(math.Min(float64(left), float64(up))) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.swift
/* Minimum path sum: Memoization search */\nfunc minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int {\n    // If it's the top-left cell, terminate the search\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // If there's a record, return it directly\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\n    let up = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j)\n    let left = minPathSumDFSMem(grid: grid, mem: &mem, i: i, j: j - 1)\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.js
/* Minimum path sum: Memoization search */\nfunction minPathSumDFSMem(grid, mem, i, j) {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] !== -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.ts
/* Minimum path sum: Memoization search */\nfunction minPathSumDFSMem(\n    grid: Array<Array<number>>,\n    mem: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.dart
/* Minimum path sum: Memoization search */\nint minPathSumDFSMem(List<List<int>> grid, List<List<int>> mem, int i, int j) {\n  // If it's the top-left cell, terminate the search\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // If row or column index is out of bounds, return +∞ cost\n  if (i < 0 || j < 0) {\n    // In Dart, int type is fixed-range integer, no value representing \"infinity\"\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // If there's a record, return it directly\n  if (mem[i][j] != -1) {\n    return mem[i][j];\n  }\n  // Minimum path cost for left and upper cells\n  int up = minPathSumDFSMem(grid, mem, i - 1, j);\n  int left = minPathSumDFSMem(grid, mem, i, j - 1);\n  // Record and return the minimum path cost from top-left to (i, j)\n  mem[i][j] = min(left, up) + grid[i][j];\n  return mem[i][j];\n}\n
min_path_sum.rs
/* Minimum path sum: Memoization search */\nfn min_path_sum_dfs_mem(grid: &Vec<Vec<i32>>, mem: &mut Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // If there's a record, return it directly\n    if mem[i as usize][j as usize] != -1 {\n        return mem[i as usize][j as usize];\n    }\n    // Minimum path cost for left and upper cells\n    let up = min_path_sum_dfs_mem(grid, mem, i - 1, j);\n    let left = min_path_sum_dfs_mem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize];\n    mem[i as usize][j as usize]\n}\n
min_path_sum.c
/* Minimum path sum: Memoization search */\nint minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
min_path_sum.kt
/* Minimum path sum: Memoization search */\nfun minPathSumDFSMem(\n    grid: Array<IntArray>,\n    mem: Array<IntArray>,\n    i: Int,\n    j: Int\n): Int {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\n    val up = minPathSumDFSMem(grid, mem, i - 1, j)\n    val left = minPathSumDFSMem(grid, mem, i, j - 1)\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.rb
### Minimum path sum: memoization search ###\ndef min_path_sum_dfs_mem(grid, mem, i, j)\n  # If it's the top-left cell, terminate the search\n  return grid[0][0] if i == 0 && j == 0\n  # If row or column index is out of bounds, return +∞ cost\n  return Float::INFINITY if i < 0 || j < 0\n  # If there's a record, return it directly\n  return mem[i][j] if mem[i][j] != -1\n  # Minimum path cost for left and upper cells\n  up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n  left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n  # Record and return the minimum path cost from top-left to (i, j)\n  mem[i][j] = [left, up].min + grid[i][j]\nend\n

As shown in Figure 14-15, after introducing memoization, all subproblem solutions only need to be computed once, so the time complexity depends on the total number of states, which is the grid size \\(O(nm)\\).

Figure 14-15   Memoization recursion tree

","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#3-method-3-dynamic-programming","level":3,"title":"3.   Method 3: Dynamic Programming","text":"

Implement the dynamic programming solution based on iteration, as shown in the code below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dp(grid: list[list[int]]) -> int:\n    \"\"\"Minimum path sum: Dynamic programming\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Initialize dp table\n    dp = [[0] * m for _ in range(n)]\n    dp[0][0] = grid[0][0]\n    # State transition: first row\n    for j in range(1, m):\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    # State transition: first column\n    for i in range(1, n):\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    # State transition: rest of the rows and columns\n    for i in range(1, n):\n        for j in range(1, m):\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n    return dp[n - 1][m - 1]\n
min_path_sum.cpp
/* Minimum path sum: Dynamic programming */\nint minPathSumDP(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Initialize dp table\n    vector<vector<int>> dp(n, vector<int>(m));\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.java
/* Minimum path sum: Dynamic programming */\nint minPathSumDP(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Initialize dp table\n    int[][] dp = new int[n][m];\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.cs
/* Minimum path sum: Dynamic programming */\nint MinPathSumDP(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Initialize dp table\n    int[,] dp = new int[n, m];\n    dp[0, 0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0, j] = dp[0, j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i, 0] = dp[i - 1, 0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i, j] = Math.Min(dp[i, j - 1], dp[i - 1, j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1, m - 1];\n}\n
min_path_sum.go
/* Minimum path sum: Dynamic programming */\nfunc minPathSumDP(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Initialize dp table\n    dp := make([][]int, n)\n    for i := 0; i < n; i++ {\n        dp[i] = make([]int, m)\n    }\n    dp[0][0] = grid[0][0]\n    // State transition: first row\n    for j := 1; j < m; j++ {\n        dp[0][j] = dp[0][j-1] + grid[0][j]\n    }\n    // State transition: first column\n    for i := 1; i < n; i++ {\n        dp[i][0] = dp[i-1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i < n; i++ {\n        for j := 1; j < m; j++ {\n            dp[i][j] = int(math.Min(float64(dp[i][j-1]), float64(dp[i-1][j]))) + grid[i][j]\n        }\n    }\n    return dp[n-1][m-1]\n}\n
min_path_sum.swift
/* Minimum path sum: Dynamic programming */\nfunc minPathSumDP(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: m), count: n)\n    dp[0][0] = grid[0][0]\n    // State transition: first row\n    for j in 1 ..< m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // State transition: first column\n    for i in 1 ..< n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\n    for i in 1 ..< n {\n        for j in 1 ..< m {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
min_path_sum.js
/* Minimum path sum: Dynamic programming */\nfunction minPathSumDP(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i < n; i++) {\n        for (let j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.ts
/* Minimum path sum: Dynamic programming */\nfunction minPathSumDP(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i < n; i++) {\n        for (let j: number = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.dart
/* Minimum path sum: Dynamic programming */\nint minPathSumDP(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n, (i) => List.filled(m, 0));\n  dp[0][0] = grid[0][0];\n  // State transition: first row\n  for (int j = 1; j < m; j++) {\n    dp[0][j] = dp[0][j - 1] + grid[0][j];\n  }\n  // State transition: first column\n  for (int i = 1; i < n; i++) {\n    dp[i][0] = dp[i - 1][0] + grid[i][0];\n  }\n  // State transition: rest of the rows and columns\n  for (int i = 1; i < n; i++) {\n    for (int j = 1; j < m; j++) {\n      dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n    }\n  }\n  return dp[n - 1][m - 1];\n}\n
min_path_sum.rs
/* Minimum path sum: Dynamic programming */\nfn min_path_sum_dp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Initialize dp table\n    let mut dp = vec![vec![0; m]; n];\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for j in 1..m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for i in 1..n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for i in 1..n {\n        for j in 1..m {\n            dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    dp[n - 1][m - 1]\n}\n
min_path_sum.c
/* Minimum path sum: Dynamic programming */\nint minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Initialize dp table\n    int **dp = malloc(n * sizeof(int *));\n    for (int i = 0; i < n; i++) {\n        dp[i] = calloc(m, sizeof(int));\n    }\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    int res = dp[n - 1][m - 1];\n    // Free memory\n    for (int i = 0; i < n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
min_path_sum.kt
/* Minimum path sum: Dynamic programming */\nfun minPathSumDP(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Initialize dp table\n    val dp = Array(n) { IntArray(m) }\n    dp[0][0] = grid[0][0]\n    // State transition: first row\n    for (j in 1..<m) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // State transition: first column\n    for (i in 1..<n) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\n    for (i in 1..<n) {\n        for (j in 1..<m) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
min_path_sum.rb
### Minimum path sum: dynamic programming ###\ndef min_path_sum_dp(grid)\n  n, m = grid.length, grid.first.length\n  # Initialize dp table\n  dp = Array.new(n) { Array.new(m, 0) }\n  dp[0][0] = grid[0][0]\n  # State transition: first row\n  (1...m).each { |j| dp[0][j] = dp[0][j - 1] + grid[0][j] }\n  # State transition: first column\n  (1...n).each { |i| dp[i][0] = dp[i - 1][0] + grid[i][0] }\n  # State transition: rest of the rows and columns\n  for i in 1...n\n    for j in 1...m\n      dp[i][j] = [dp[i][j - 1], dp[i - 1][j]].min + grid[i][j]\n    end\n  end\n  dp[n -1][m -1]\nend\n

Figure 14-16 shows the state transition process for minimum path sum, which traverses the entire grid, thus the time complexity is \\(O(nm)\\).

The array dp has size \\(n \\times m\\), thus the space complexity is \\(O(nm)\\).

<1><2><3><4><5><6><7><8><9><10><11><12>

Figure 14-16   Dynamic programming process for minimum path sum

","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#4-space-optimization","level":3,"title":"4.   Space Optimization","text":"

Since each cell is only related to the cell to its left and the cell above it, we can use a single-row array to implement the \\(dp\\) table.

Note that since the array dp can only represent the state of one row, we cannot initialize the first column state in advance, but rather update it when traversing each row:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dp_comp(grid: list[list[int]]) -> int:\n    \"\"\"Minimum path sum: Space-optimized dynamic programming\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Initialize dp table\n    dp = [0] * m\n    # State transition: first row\n    dp[0] = grid[0][0]\n    for j in range(1, m):\n        dp[j] = dp[j - 1] + grid[0][j]\n    # State transition: rest of the rows\n    for i in range(1, n):\n        # State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        # State transition: rest of the columns\n        for j in range(1, m):\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n    return dp[m - 1]\n
min_path_sum.cpp
/* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Initialize dp table\n    vector<int> dp(m);\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (int j = 1; j < m; j++) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.java
/* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Initialize dp table\n    int[] dp = new int[m];\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.cs
/* Minimum path sum: Space-optimized dynamic programming */\nint MinPathSumDPComp(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Initialize dp table\n    int[] dp = new int[m];\n    dp[0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.Min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.go
/* Minimum path sum: Space-optimized dynamic programming */\nfunc minPathSumDPComp(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Initialize dp table\n    dp := make([]int, m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for j := 1; j < m; j++ {\n        dp[j] = dp[j-1] + grid[0][j]\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i < n; i++ {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\n        for j := 1; j < m; j++ {\n            dp[j] = int(math.Min(float64(dp[j-1]), float64(dp[j]))) + grid[i][j]\n        }\n    }\n    return dp[m-1]\n}\n
min_path_sum.swift
/* Minimum path sum: Space-optimized dynamic programming */\nfunc minPathSumDPComp(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for j in 1 ..< m {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // State transition: rest of the rows\n    for i in 1 ..< n {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\n        for j in 1 ..< m {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
min_path_sum.js
/* Minimum path sum: Space-optimized dynamic programming */\nfunction minPathSumDPComp(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = new Array(m);\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.ts
/* Minimum path sum: Space-optimized dynamic programming */\nfunction minPathSumDPComp(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = new Array(m);\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.dart
/* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Initialize dp table\n  List<int> dp = List.filled(m, 0);\n  dp[0] = grid[0][0];\n  for (int j = 1; j < m; j++) {\n    dp[j] = dp[j - 1] + grid[0][j];\n  }\n  // State transition: rest of the rows\n  for (int i = 1; i < n; i++) {\n    // State transition: first column\n    dp[0] = dp[0] + grid[i][0];\n    // State transition: rest of the columns\n    for (int j = 1; j < m; j++) {\n      dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n    }\n  }\n  return dp[m - 1];\n}\n
min_path_sum.rs
/* Minimum path sum: Space-optimized dynamic programming */\nfn min_path_sum_dp_comp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Initialize dp table\n    let mut dp = vec![0; m];\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for j in 1..m {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for i in 1..n {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for j in 1..m {\n            dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    dp[m - 1]\n}\n
min_path_sum.c
/* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Initialize dp table\n    int *dp = calloc(m, sizeof(int));\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (int j = 1; j < m; j++) {\n            dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    int res = dp[m - 1];\n    // Free memory\n    free(dp);\n    return res;\n}\n
min_path_sum.kt
/* Minimum path sum: Space-optimized dynamic programming */\nfun minPathSumDPComp(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Initialize dp table\n    val dp = IntArray(m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for (j in 1..<m) {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // State transition: rest of the rows\n    for (i in 1..<n) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\n        for (j in 1..<m) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
min_path_sum.rb
### Minimum path sum: space-optimized DP ###\ndef min_path_sum_dp_comp(grid)\n  n, m = grid.length, grid.first.length\n  # Initialize dp table\n  dp = Array.new(m, 0)\n  # State transition: first row\n  dp[0] = grid[0][0]\n  (1...m).each { |j| dp[j] = dp[j - 1] + grid[0][j] }\n  # State transition: rest of the rows\n  for i in 1...n\n    # State transition: first column\n    dp[0] = dp[0] + grid[i][0]\n    # State transition: rest of the columns\n    (1...m).each { |j| dp[j] = [dp[j - 1], dp[j]].min + grid[i][j] }\n  end\n  dp[m - 1]\nend\n
","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/","level":1,"title":"14.6   Edit Distance Problem","text":"

Edit distance, also known as Levenshtein distance, refers to the minimum number of edits required to transform one string into another, commonly used in information retrieval and natural language processing to measure the similarity between two sequences.

Question

Given two strings \\(s\\) and \\(t\\), return the minimum number of edits required to transform \\(s\\) into \\(t\\).

You can perform three types of edit operations on a string: insert a character, delete a character, or replace a character with any other character.

As shown in Figure 14-27, transforming kitten into sitting requires 3 edits, including 2 replacements and 1 insertion; transforming hello into algo requires 3 steps, including 2 replacements and 1 deletion.

Figure 14-27   Example data for edit distance

The edit distance problem can be naturally explained using the decision tree model. Strings correspond to tree nodes, and a round of decision (one edit operation) corresponds to an edge of the tree.

As shown in Figure 14-28, without restricting operations, each node can branch into many edges, with each edge corresponding to one operation, meaning there are many possible paths to transform hello into algo.

From the perspective of the decision tree, the goal of this problem is to find the shortest path between node hello and node algo.

Figure 14-28   Representing edit distance problem based on decision tree model

","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#1-dynamic-programming-approach","level":3,"title":"1.   Dynamic Programming Approach","text":"

Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

Each round of decision involves performing one edit operation on string \\(s\\).

We want the problem scale to gradually decrease during the editing process, which allows us to construct subproblems. Let the lengths of strings \\(s\\) and \\(t\\) be \\(n\\) and \\(m\\) respectively. We first consider the tail characters of the two strings, \\(s[n-1]\\) and \\(t[m-1]\\).

  • If \\(s[n-1]\\) and \\(t[m-1]\\) are the same, we can skip them and directly consider \\(s[n-2]\\) and \\(t[m-2]\\).
  • If \\(s[n-1]\\) and \\(t[m-1]\\) are different, we need to perform one edit on \\(s\\) (insert, delete, or replace) to make the tail characters of the two strings the same, allowing us to skip them and consider a smaller-scale problem.

In other words, each round of decision (edit operation) we make on string \\(s\\) will change the remaining characters to be matched in \\(s\\) and \\(t\\). Therefore, the state is the \\(i\\)-th and \\(j\\)-th characters currently being considered in \\(s\\) and \\(t\\), denoted as \\([i, j]\\).

State \\([i, j]\\) corresponds to the subproblem: the minimum number of edits required to change the first \\(i\\) characters of \\(s\\) into the first \\(j\\) characters of \\(t\\).

From this, we obtain a two-dimensional \\(dp\\) table of size \\((i+1) \\times (j+1)\\).

Step 2: Identify the optimal substructure, and then derive the state transition equation

Consider subproblem \\(dp[i, j]\\), where the tail characters of the corresponding two strings are \\(s[i-1]\\) and \\(t[j-1]\\), which can be divided into the three cases shown in Figure 14-29 based on different edit operations.

  1. Insert \\(t[j-1]\\) after \\(s[i-1]\\), then the remaining subproblem is \\(dp[i, j-1]\\).
  2. Delete \\(s[i-1]\\), then the remaining subproblem is \\(dp[i-1, j]\\).
  3. Replace \\(s[i-1]\\) with \\(t[j-1]\\), then the remaining subproblem is \\(dp[i-1, j-1]\\).

Figure 14-29   State transition for edit distance

Based on the above analysis, the optimal substructure can be obtained: the minimum number of edits for \\(dp[i, j]\\) equals the minimum among the minimum edit steps of \\(dp[i, j-1]\\), \\(dp[i-1, j]\\), and \\(dp[i-1, j-1]\\), plus the edit step \\(1\\) for this time. The corresponding state transition equation is:

\\[ dp[i, j] = \\min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \\]

Please note that when \\(s[i-1]\\) and \\(t[j-1]\\) are the same, no edit is required for the current character, in which case the state transition equation is:

\\[ dp[i, j] = dp[i-1, j-1] \\]

Step 3: Determine boundary conditions and state transition order

When both strings are empty, the number of edit steps is \\(0\\), i.e., \\(dp[0, 0] = 0\\). When \\(s\\) is empty but \\(t\\) is not, the minimum number of edit steps equals the length of \\(t\\), i.e., the first row \\(dp[0, j] = j\\). When \\(s\\) is not empty but \\(t\\) is empty, the minimum number of edit steps equals the length of \\(s\\), i.e., the first column \\(dp[i, 0] = i\\).

Observing the state transition equation, the solution \\(dp[i, j]\\) depends on solutions to the left, above, and upper-left, so the entire \\(dp\\) table can be traversed in order through two nested loops.

","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
def edit_distance_dp(s: str, t: str) -> int:\n    \"\"\"Edit distance: Dynamic programming\"\"\"\n    n, m = len(s), len(t)\n    dp = [[0] * (m + 1) for _ in range(n + 1)]\n    # State transition: first row and first column\n    for i in range(1, n + 1):\n        dp[i][0] = i\n    for j in range(1, m + 1):\n        dp[0][j] = j\n    # State transition: rest of the rows and columns\n    for i in range(1, n + 1):\n        for j in range(1, m + 1):\n            if s[i - 1] == t[j - 1]:\n                # If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            else:\n                # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1\n    return dp[n][m]\n
edit_distance.cpp
/* Edit distance: Dynamic programming */\nint editDistanceDP(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));\n    // State transition: first row and first column\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.java
/* Edit distance: Dynamic programming */\nint editDistanceDP(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[][] dp = new int[n + 1][m + 1];\n    // State transition: first row and first column\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.cs
/* Edit distance: Dynamic programming */\nint EditDistanceDP(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[,] dp = new int[n + 1, m + 1];\n    // State transition: first row and first column\n    for (int i = 1; i <= n; i++) {\n        dp[i, 0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0, j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[i, j] = dp[i - 1, j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n, m];\n}\n
edit_distance.go
/* Edit distance: Dynamic programming */\nfunc editDistanceDP(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, m+1)\n    }\n    // State transition: first row and first column\n    for i := 1; i <= n; i++ {\n        dp[i][0] = i\n    }\n    for j := 1; j <= m; j++ {\n        dp[0][j] = j\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= m; j++ {\n            if s[i-1] == t[j-1] {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i-1][j-1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.swift
/* Edit distance: Dynamic programming */\nfunc editDistanceDP(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)\n    // State transition: first row and first column\n    for i in 1 ... n {\n        dp[i][0] = i\n    }\n    for j in 1 ... m {\n        dp[0][j] = j\n    }\n    // State transition: rest of the rows and columns\n    for i in 1 ... n {\n        for j in 1 ... m {\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.js
/* Edit distance: Dynamic programming */\nfunction editDistanceDP(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));\n    // State transition: first row and first column\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.ts
/* Edit distance: Dynamic programming */\nfunction editDistanceDP(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: m + 1 }, () => 0)\n    );\n    // State transition: first row and first column\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.dart
/* Edit distance: Dynamic programming */\nint editDistanceDP(String s, String t) {\n  int n = s.length, m = t.length;\n  List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));\n  // State transition: first row and first column\n  for (int i = 1; i <= n; i++) {\n    dp[i][0] = i;\n  }\n  for (int j = 1; j <= m; j++) {\n    dp[0][j] = j;\n  }\n  // State transition: rest of the rows and columns\n  for (int i = 1; i <= n; i++) {\n    for (int j = 1; j <= m; j++) {\n      if (s[i - 1] == t[j - 1]) {\n        // If two characters are equal, skip both characters\n        dp[i][j] = dp[i - 1][j - 1];\n      } else {\n        // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n      }\n    }\n  }\n  return dp[n][m];\n}\n
edit_distance.rs
/* Edit distance: Dynamic programming */\nfn edit_distance_dp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![vec![0; m + 1]; n + 1];\n    // State transition: first row and first column\n    for i in 1..=n {\n        dp[i][0] = i as i32;\n    }\n    for j in 1..m {\n        dp[0][j] = j as i32;\n    }\n    // State transition: rest of the rows and columns\n    for i in 1..=n {\n        for j in 1..=m {\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] =\n                    std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    dp[n][m]\n}\n
edit_distance.c
/* Edit distance: Dynamic programming */\nint editDistanceDP(char *s, char *t, int n, int m) {\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(m + 1, sizeof(int));\n    }\n    // State transition: first row and first column\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    int res = dp[n][m];\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
edit_distance.kt
/* Edit distance: Dynamic programming */\nfun editDistanceDP(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = Array(n + 1) { IntArray(m + 1) }\n    // State transition: first row and first column\n    for (i in 1..n) {\n        dp[i][0] = i\n    }\n    for (j in 1..m) {\n        dp[0][j] = j\n    }\n    // State transition: rest of the rows and columns\n    for (i in 1..n) {\n        for (j in 1..m) {\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.rb
### Edit distance: dynamic programming ###\ndef edit_distance_dp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(n + 1) { Array.new(m + 1, 0) }\n  # State transition: first row and first column\n  (1...(n + 1)).each { |i| dp[i][0] = i }\n  (1...(m + 1)).each { |j| dp[0][j] = j }\n  # State transition: rest of the rows and columns\n  for i in 1...(n + 1)\n    for j in 1...(m +1)\n      if s[i - 1] == t[j - 1]\n        # If two characters are equal, skip both characters\n        dp[i][j] = dp[i - 1][j - 1]\n      else\n        # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1\n      end\n    end\n  end\n  dp[n][m]\nend\n

As shown in Figure 14-30, the state transition process for the edit distance problem is very similar to the knapsack problem and can both be viewed as the process of filling a two-dimensional grid.

<1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

Figure 14-30   Dynamic programming process for edit distance

","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#3-space-optimization","level":3,"title":"3.   Space Optimization","text":"

Since \\(dp[i, j]\\) is transferred from the solutions above \\(dp[i-1, j]\\), to the left \\(dp[i, j-1]\\), and to the upper-left \\(dp[i-1, j-1]\\), forward traversal will lose the upper-left solution \\(dp[i-1, j-1]\\), and reverse traversal cannot build \\(dp[i, j-1]\\) in advance, so neither traversal order is feasible.

For this reason, we can use a variable leftup to temporarily store the upper-left solution \\(dp[i-1, j-1]\\), so we only need to consider the solutions to the left and above. This situation is the same as the unbounded knapsack problem, allowing for forward traversal. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
def edit_distance_dp_comp(s: str, t: str) -> int:\n    \"\"\"Edit distance: Space-optimized dynamic programming\"\"\"\n    n, m = len(s), len(t)\n    dp = [0] * (m + 1)\n    # State transition: first row\n    for j in range(1, m + 1):\n        dp[j] = j\n    # State transition: rest of the rows\n    for i in range(1, n + 1):\n        # State transition: first column\n        leftup = dp[0]  # Temporarily store dp[i-1, j-1]\n        dp[0] += 1\n        # State transition: rest of the columns\n        for j in range(1, m + 1):\n            temp = dp[j]\n            if s[i - 1] == t[j - 1]:\n                # If two characters are equal, skip both characters\n                dp[j] = leftup\n            else:\n                # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(dp[j - 1], dp[j], leftup) + 1\n            leftup = temp  # Update for next round's dp[i-1, j-1]\n    return dp[m]\n
edit_distance.cpp
/* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<int> dp(m + 1, 0);\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.java
/* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[] dp = new int[m + 1];\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.cs
/* Edit distance: Space-optimized dynamic programming */\nint EditDistanceDPComp(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[] dp = new int[m + 1];\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.go
/* Edit distance: Space-optimized dynamic programming */\nfunc editDistanceDPComp(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([]int, m+1)\n    // State transition: first row\n    for j := 1; j <= m; j++ {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for i := 1; i <= n; i++ {\n        // State transition: first column\n        leftUp := dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for j := 1; j <= m; j++ {\n            temp := dp[j]\n            if s[i-1] == t[j-1] {\n                // If two characters are equal, skip both characters\n                dp[j] = leftUp\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1\n            }\n            leftUp = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
edit_distance.swift
/* Edit distance: Space-optimized dynamic programming */\nfunc editDistanceDPComp(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: 0, count: m + 1)\n    // State transition: first row\n    for j in 1 ... m {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for i in 1 ... n {\n        // State transition: first column\n        var leftup = dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for j in 1 ... m {\n            let temp = dp[j]\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
edit_distance.js
/* Edit distance: Space-optimized dynamic programming */\nfunction editDistanceDPComp(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // State transition: first row\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i <= n; i++) {\n        // State transition: first column\n        let leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.ts
/* Edit distance: Space-optimized dynamic programming */\nfunction editDistanceDPComp(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // State transition: first row\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i <= n; i++) {\n        // State transition: first column\n        let leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.dart
/* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(String s, String t) {\n  int n = s.length, m = t.length;\n  List<int> dp = List.filled(m + 1, 0);\n  // State transition: first row\n  for (int j = 1; j <= m; j++) {\n    dp[j] = j;\n  }\n  // State transition: rest of the rows\n  for (int i = 1; i <= n; i++) {\n    // State transition: first column\n    int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n    dp[0] = i;\n    // State transition: rest of the columns\n    for (int j = 1; j <= m; j++) {\n      int temp = dp[j];\n      if (s[i - 1] == t[j - 1]) {\n        // If two characters are equal, skip both characters\n        dp[j] = leftup;\n      } else {\n        // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n      }\n      leftup = temp; // Update for next round's dp[i-1, j-1]\n    }\n  }\n  return dp[m];\n}\n
edit_distance.rs
/* Edit distance: Space-optimized dynamic programming */\nfn edit_distance_dp_comp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![0; m + 1];\n    // State transition: first row\n    for j in 1..m {\n        dp[j] = j as i32;\n    }\n    // State transition: rest of the rows\n    for i in 1..=n {\n        // State transition: first column\n        let mut leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i as i32;\n        // State transition: rest of the columns\n        for j in 1..=m {\n            let temp = dp[j];\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    dp[m]\n}\n
edit_distance.c
/* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(char *s, char *t, int n, int m) {\n    int *dp = calloc(m + 1, sizeof(int));\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    int res = dp[m];\n    // Free memory\n    free(dp);\n    return res;\n}\n
edit_distance.kt
/* Edit distance: Space-optimized dynamic programming */\nfun editDistanceDPComp(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = IntArray(m + 1)\n    // State transition: first row\n    for (j in 1..m) {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for (i in 1..n) {\n        // State transition: first column\n        var leftup = dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for (j in 1..m) {\n            val temp = dp[j]\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
edit_distance.rb
### Edit distance: space-optimized DP ###\ndef edit_distance_dp_comp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(m + 1, 0)\n  # State transition: first row\n  (1...(m + 1)).each { |j| dp[j] = j }\n  # State transition: rest of the rows\n  for i in 1...(n + 1)\n    # State transition: first column\n    leftup = dp.first # Temporarily store dp[i-1, j-1]\n    dp[0] += 1\n    # State transition: rest of the columns\n    for j in 1...(m + 1)\n      temp = dp[j]\n      if s[i - 1] == t[j - 1]\n        # If two characters are equal, skip both characters\n        dp[j] = leftup\n      else\n        # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[j] = [dp[j - 1], dp[j], leftup].min + 1\n      end\n      leftup = temp # Update for next round's dp[i-1, j-1]\n    end\n  end\n  dp[m]\nend\n
","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/","level":1,"title":"14.1   Introduction to Dynamic Programming","text":"

Dynamic programming is an important algorithmic paradigm that decomposes a problem into a series of smaller subproblems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving time efficiency.

In this section, we start with a classic example, first presenting its brute force backtracking solution, observing the overlapping subproblems within it, and then gradually deriving a more efficient dynamic programming solution.

Climbing stairs

Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time, how many different ways are there to reach the top?

As shown in Figure 14-1, for a \\(3\\)-step staircase, there are \\(3\\) different ways to reach the top.

Figure 14-1   Number of ways to reach the 3rd step

The goal of this problem is to find the number of ways, we can consider using backtracking to enumerate all possibilities. Specifically, imagine climbing stairs as a multi-round selection process: starting from the ground, choosing to go up \\(1\\) or \\(2\\) steps in each round, incrementing the count by \\(1\\) whenever the top of the stairs is reached, and pruning when exceeding the top. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_backtrack.py
def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:\n    \"\"\"Backtracking\"\"\"\n    # When climbing to the n-th stair, add 1 to the solution count\n    if state == n:\n        res[0] += 1\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n:\n            continue\n        # Attempt: make a choice, update state\n        backtrack(choices, state + choice, n, res)\n        # Backtrack\n\ndef climbing_stairs_backtrack(n: int) -> int:\n    \"\"\"Climbing stairs: Backtracking\"\"\"\n    choices = [1, 2]  # Can choose to climb up 1 or 2 stairs\n    state = 0  # Start climbing from the 0-th stair\n    res = [0]  # Use res[0] to record the solution count\n    backtrack(choices, state, n, res)\n    return res[0]\n
climbing_stairs_backtrack.cpp
/* Backtracking */\nvoid backtrack(vector<int> &choices, int state, int n, vector<int> &res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    for (auto &choice : choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    vector<int> choices = {1, 2}; // Can choose to climb up 1 or 2 stairs\n    int state = 0;                // Start climbing from the 0-th stair\n    vector<int> res = {0};        // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res[0];\n}\n
climbing_stairs_backtrack.java
/* Backtracking */\nvoid backtrack(List<Integer> choices, int state, int n, List<Integer> res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (Integer choice : choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    List<Integer> choices = Arrays.asList(1, 2); // Can choose to climb up 1 or 2 stairs\n    int state = 0; // Start climbing from the 0-th stair\n    List<Integer> res = new ArrayList<>();\n    res.add(0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.cs
/* Backtracking */\nvoid Backtrack(List<int> choices, int state, int n, List<int> res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    foreach (int choice in choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        Backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint ClimbingStairsBacktrack(int n) {\n    List<int> choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    int state = 0; // Start climbing from the 0-th stair\n    List<int> res = [0]; // Use res[0] to record the solution count\n    Backtrack(choices, state, n, res);\n    return res[0];\n}\n
climbing_stairs_backtrack.go
/* Backtracking */\nfunc backtrack(choices []int, state, n int, res []int) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] = res[0] + 1\n    }\n    // Traverse all choices\n    for _, choice := range choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state+choice > n {\n            continue\n        }\n        // Attempt: make choice, update state\n        backtrack(choices, state+choice, n, res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunc climbingStairsBacktrack(n int) int {\n    // Can choose to climb up 1 or 2 stairs\n    choices := []int{1, 2}\n    // Start climbing from the 0-th stair\n    state := 0\n    res := make([]int, 1)\n    // Use res[0] to record the solution count\n    res[0] = 0\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
climbing_stairs_backtrack.swift
/* Backtracking */\nfunc backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] += 1\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n {\n            continue\n        }\n        // Attempt: make choice, update state\n        backtrack(choices: choices, state: state + choice, n: n, res: &res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunc climbingStairsBacktrack(n: Int) -> Int {\n    let choices = [1, 2] // Can choose to climb up 1 or 2 stairs\n    let state = 0 // Start climbing from the 0-th stair\n    var res: [Int] = []\n    res.append(0) // Use res[0] to record the solution count\n    backtrack(choices: choices, state: state, n: n, res: &res)\n    return res[0]\n}\n
climbing_stairs_backtrack.js
/* Backtracking */\nfunction backtrack(choices, state, n, res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunction climbingStairsBacktrack(n) {\n    const choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    const state = 0; // Start climbing from the 0-th stair\n    const res = new Map();\n    res.set(0, 0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.ts
/* Backtracking */\nfunction backtrack(\n    choices: number[],\n    state: number,\n    n: number,\n    res: Map<0, any>\n): void {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunction climbingStairsBacktrack(n: number): number {\n    const choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    const state = 0; // Start climbing from the 0-th stair\n    const res = new Map();\n    res.set(0, 0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.dart
/* Backtracking */\nvoid backtrack(List<int> choices, int state, int n, List<int> res) {\n  // When climbing to the n-th stair, add 1 to the solution count\n  if (state == n) {\n    res[0]++;\n  }\n  // Traverse all choices\n  for (int choice in choices) {\n    // Pruning: not allowed to go beyond the n-th stair\n    if (state + choice > n) continue;\n    // Attempt: make choice, update state\n    backtrack(choices, state + choice, n, res);\n    // Backtrack\n  }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n  List<int> choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n  int state = 0; // Start climbing from the 0-th stair\n  List<int> res = [];\n  res.add(0); // Use res[0] to record the solution count\n  backtrack(choices, state, n, res);\n  return res[0];\n}\n
climbing_stairs_backtrack.rs
/* Backtracking */\nfn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] = res[0] + 1;\n    }\n    // Traverse all choices\n    for &choice in choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n {\n            continue;\n        }\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfn climbing_stairs_backtrack(n: usize) -> i32 {\n    let choices = vec![1, 2]; // Can choose to climb up 1 or 2 stairs\n    let state = 0; // Start climbing from the 0-th stair\n    let mut res = Vec::new();\n    res.push(0); // Use res[0] to record the solution count\n    backtrack(&choices, state, n as i32, &mut res);\n    res[0]\n}\n
climbing_stairs_backtrack.c
/* Backtracking */\nvoid backtrack(int *choices, int state, int n, int *res, int len) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    for (int i = 0; i < len; i++) {\n        int choice = choices[i];\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res, len);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    int choices[2] = {1, 2}; // Can choose to climb up 1 or 2 stairs\n    int state = 0;           // Start climbing from the 0-th stair\n    int *res = (int *)malloc(sizeof(int));\n    *res = 0; // Use res[0] to record the solution count\n    int len = sizeof(choices) / sizeof(int);\n    backtrack(choices, state, n, res, len);\n    int result = *res;\n    free(res);\n    return result;\n}\n
climbing_stairs_backtrack.kt
/* Backtracking */\nfun backtrack(\n    choices: MutableList<Int>,\n    state: Int,\n    n: Int,\n    res: MutableList<Int>\n) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0] = res[0] + 1\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfun climbingStairsBacktrack(n: Int): Int {\n    val choices = mutableListOf(1, 2) // Can choose to climb up 1 or 2 stairs\n    val state = 0 // Start climbing from the 0-th stair\n    val res = mutableListOf<Int>()\n    res.add(0) // Use res[0] to record the solution count\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
climbing_stairs_backtrack.rb
### Backtracking ###\ndef backtrack(choices, state, n, res)\n  # When climbing to the n-th stair, add 1 to the solution count\n  res[0] += 1 if state == n\n  # Traverse all choices\n  for choice in choices\n    # Pruning: not allowed to go beyond the n-th stair\n    next if state + choice > n\n\n    # Attempt: make choice, update state\n    backtrack(choices, state + choice, n, res)\n  end\n  # Backtrack\nend\n\n### Climbing stairs: backtracking ###\ndef climbing_stairs_backtrack(n)\n  choices = [1, 2] # Can choose to climb up 1 or 2 stairs\n  state = 0 # Start climbing from the 0-th stair\n  res = [0] # Use res[0] to record the solution count\n  backtrack(choices, state, n, res)\n  res.first\nend\n
","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1411-method-1-brute-force-search","level":2,"title":"14.1.1   Method 1: Brute Force Search","text":"

Backtracking algorithms typically do not explicitly decompose problems, but rather treat solving the problem as a series of decision steps, searching for all possible solutions through trial and pruning.

We can try to analyze this problem from the perspective of problem decomposition. Let the number of ways to climb to the \\(i\\)-th step be \\(dp[i]\\), then \\(dp[i]\\) is the original problem, and its subproblems include:

\\[ dp[i-1], dp[i-2], \\dots, dp[2], dp[1] \\]

Since we can only go up \\(1\\) or \\(2\\) steps in each round, when we stand on the \\(i\\)-th step, we could only have been on the \\(i-1\\)-th or \\(i-2\\)-th step in the previous round. In other words, we can only reach the \\(i\\)-th step from the \\(i-1\\)-th or \\(i-2\\)-th step.

This leads to an important conclusion: the number of ways to climb to the \\(i-1\\)-th step plus the number of ways to climb to the \\(i-2\\)-th step equals the number of ways to climb to the \\(i\\)-th step. The formula is as follows:

\\[ dp[i] = dp[i-1] + dp[i-2] \\]

This means that in the stair climbing problem, there exists a recurrence relation among the subproblems, the solution to the original problem can be constructed from the solutions to the subproblems. Figure 14-2 illustrates this recurrence relation.

Figure 14-2   Recurrence relation for the number of ways

We can obtain a brute force search solution based on the recurrence formula. Starting from \\(dp[n]\\), recursively decompose a larger problem into the sum of two smaller problems, until reaching the smallest subproblems \\(dp[1]\\) and \\(dp[2]\\) and returning. Among them, the solutions to the smallest subproblems are known, namely \\(dp[1] = 1\\) and \\(dp[2] = 2\\), representing \\(1\\) and \\(2\\) ways to climb to the \\(1\\)st and \\(2\\)nd steps, respectively.

Observe the following code, which, like standard backtracking code, belongs to depth-first search but is more concise:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs.py
def dfs(i: int) -> int:\n    \"\"\"Search\"\"\"\n    # Known dp[1] and dp[2], return them\n    if i == 1 or i == 2:\n        return i\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1) + dfs(i - 2)\n    return count\n\ndef climbing_stairs_dfs(n: int) -> int:\n    \"\"\"Climbing stairs: Search\"\"\"\n    return dfs(n)\n
climbing_stairs_dfs.cpp
/* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.java
/* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.cs
/* Search */\nint DFS(int i) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1) + DFS(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nint ClimbingStairsDFS(int n) {\n    return DFS(n);\n}\n
climbing_stairs_dfs.go
/* Search */\nfunc dfs(i int) int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfs(i-1) + dfs(i-2)\n    return count\n}\n\n/* Climbing stairs: Search */\nfunc climbingStairsDFS(n int) int {\n    return dfs(n)\n}\n
climbing_stairs_dfs.swift
/* Search */\nfunc dfs(i: Int) -> Int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1) + dfs(i: i - 2)\n    return count\n}\n\n/* Climbing stairs: Search */\nfunc climbingStairsDFS(n: Int) -> Int {\n    dfs(i: n)\n}\n
climbing_stairs_dfs.js
/* Search */\nfunction dfs(i) {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nfunction climbingStairsDFS(n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.ts
/* Search */\nfunction dfs(i: number): number {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nfunction climbingStairsDFS(n: number): number {\n    return dfs(n);\n}\n
climbing_stairs_dfs.dart
/* Search */\nint dfs(int i) {\n  // Known dp[1] and dp[2], return them\n  if (i == 1 || i == 2) return i;\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1) + dfs(i - 2);\n  return count;\n}\n\n/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n  return dfs(n);\n}\n
climbing_stairs_dfs.rs
/* Search */\nfn dfs(i: usize) -> i32 {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1) + dfs(i - 2);\n    count\n}\n\n/* Climbing stairs: Search */\nfn climbing_stairs_dfs(n: usize) -> i32 {\n    dfs(n)\n}\n
climbing_stairs_dfs.c
/* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.kt
/* Search */\nfun dfs(i: Int): Int {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2) return i\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1) + dfs(i - 2)\n    return count\n}\n\n/* Climbing stairs: Search */\nfun climbingStairsDFS(n: Int): Int {\n    return dfs(n)\n}\n
climbing_stairs_dfs.rb
### Search ###\ndef dfs(i)\n  # Known dp[1] and dp[2], return them\n  return i if i == 1 || i == 2\n  # dp[i] = dp[i-1] + dp[i-2]\n  dfs(i - 1) + dfs(i - 2)\nend\n\n### Climbing stairs: search ###\ndef climbing_stairs_dfs(n)\n  dfs(n)\nend\n

Figure 14-3 shows the recursion tree formed by brute force search. For the problem \\(dp[n]\\), the depth of its recursion tree is \\(n\\), with a time complexity of \\(O(2^n)\\). Exponential order represents explosive growth; if we input a relatively large \\(n\\), we will fall into a long wait.

Figure 14-3   Recursion tree for climbing stairs

Observing the above figure, the exponential time complexity is caused by \"overlapping subproblems\". For example, \\(dp[9]\\) is decomposed into \\(dp[8]\\) and \\(dp[7]\\), and \\(dp[8]\\) is decomposed into \\(dp[7]\\) and \\(dp[6]\\), both of which contain the subproblem \\(dp[7]\\).

And so on, subproblems contain smaller overlapping subproblems, ad infinitum. The vast majority of computational resources are wasted on these overlapping subproblems.

","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1412-method-2-memoization","level":2,"title":"14.1.2   Method 2: Memoization","text":"

To improve algorithm efficiency, we want all overlapping subproblems to be computed only once. For this purpose, we declare an array mem to record the solution to each subproblem and prune overlapping subproblems during the search process.

  1. When computing \\(dp[i]\\) for the first time, we record it in mem[i] for later use.
  2. When we need to compute \\(dp[i]\\) again, we can directly retrieve the result from mem[i], thereby avoiding redundant computation of that subproblem.

The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs_mem.py
def dfs(i: int, mem: list[int]) -> int:\n    \"\"\"Memoization search\"\"\"\n    # Known dp[1] and dp[2], return them\n    if i == 1 or i == 2:\n        return i\n    # If record dp[i] exists, return it directly\n    if mem[i] != -1:\n        return mem[i]\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    # Record dp[i]\n    mem[i] = count\n    return count\n\ndef climbing_stairs_dfs_mem(n: int) -> int:\n    \"\"\"Climbing stairs: Memoization search\"\"\"\n    # mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    mem = [-1] * (n + 1)\n    return dfs(n, mem)\n
climbing_stairs_dfs_mem.cpp
/* Memoization search */\nint dfs(int i, vector<int> &mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    vector<int> mem(n + 1, -1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.java
/* Memoization search */\nint dfs(int i, int[] mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    int[] mem = new int[n + 1];\n    Arrays.fill(mem, -1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.cs
/* Memoization search */\nint DFS(int i, int[] mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1, mem) + DFS(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint ClimbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    int[] mem = new int[n + 1];\n    Array.Fill(mem, -1);\n    return DFS(n, mem);\n}\n
climbing_stairs_dfs_mem.go
/* Memoization search */\nfunc dfsMem(i int, mem []int) int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // If record dp[i] exists, return it directly\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfsMem(i-1, mem) + dfsMem(i-2, mem)\n    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfunc climbingStairsDFSMem(n int) int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    mem := make([]int, n+1)\n    for i := range mem {\n        mem[i] = -1\n    }\n    return dfsMem(n, mem)\n}\n
climbing_stairs_dfs_mem.swift
/* Memoization search */\nfunc dfs(i: Int, mem: inout [Int]) -> Int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // If record dp[i] exists, return it directly\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1, mem: &mem) + dfs(i: i - 2, mem: &mem)\n    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfunc climbingStairsDFSMem(n: Int) -> Int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    var mem = Array(repeating: -1, count: n + 1)\n    return dfs(i: n, mem: &mem)\n}\n
climbing_stairs_dfs_mem.js
/* Memoization search */\nfunction dfs(i, mem) {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nfunction climbingStairsDFSMem(n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.ts
/* Memoization search */\nfunction dfs(i: number, mem: number[]): number {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nfunction climbingStairsDFSMem(n: number): number {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.dart
/* Memoization search */\nint dfs(int i, List<int> mem) {\n  // Known dp[1] and dp[2], return them\n  if (i == 1 || i == 2) return i;\n  // If record dp[i] exists, return it directly\n  if (mem[i] != -1) return mem[i];\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n  // Record dp[i]\n  mem[i] = count;\n  return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n  // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n  List<int> mem = List.filled(n + 1, -1);\n  return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.rs
/* Memoization search */\nfn dfs(i: usize, mem: &mut [i32]) -> i32 {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // If record dp[i] exists, return it directly\n    if mem[i] != -1 {\n        return mem[i];\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    count\n}\n\n/* Climbing stairs: Memoization search */\nfn climbing_stairs_dfs_mem(n: usize) -> i32 {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    let mut mem = vec![-1; n + 1];\n    dfs(n, &mut mem)\n}\n
climbing_stairs_dfs_mem.c
/* Memoization search */\nint dfs(int i, int *mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    int *mem = (int *)malloc((n + 1) * sizeof(int));\n    for (int i = 0; i <= n; i++) {\n        mem[i] = -1;\n    }\n    int result = dfs(n, mem);\n    free(mem);\n    return result;\n}\n
climbing_stairs_dfs_mem.kt
/* Memoization search */\nfun dfs(i: Int, mem: IntArray): Int {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2) return i\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1) return mem[i]\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfun climbingStairsDFSMem(n: Int): Int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    val mem = IntArray(n + 1)\n    mem.fill(-1)\n    return dfs(n, mem)\n}\n
climbing_stairs_dfs_mem.rb
### Memoization search ###\ndef dfs(i, mem)\n  # Known dp[1] and dp[2], return them\n  return i if i == 1 || i == 2\n  # If record dp[i] exists, return it directly\n  return mem[i] if mem[i] != -1\n\n  # dp[i] = dp[i-1] + dp[i-2]\n  count = dfs(i - 1, mem) + dfs(i - 2, mem)\n  # Record dp[i]\n  mem[i] = count\nend\n\n### Climbing stairs: memoization search ###\ndef climbing_stairs_dfs_mem(n)\n  # mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n  mem = Array.new(n + 1, -1)\n  dfs(n, mem)\nend\n

Observe Figure 14-4, after memoization, all overlapping subproblems only need to be computed once, optimizing the time complexity to \\(O(n)\\), which is a tremendous leap.

Figure 14-4   Recursion tree with memoization

","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1413-method-3-dynamic-programming","level":2,"title":"14.1.3   Method 3: Dynamic Programming","text":"

Memoization is a \"top-down\" method: we start from the original problem (root node), recursively decompose larger subproblems into smaller ones, until reaching the smallest known subproblems (leaf nodes). Afterward, by backtracking, we collect the solutions to the subproblems layer by layer to construct the solution to the original problem.

In contrast, dynamic programming is a \"bottom-up\" method: starting from the solutions to the smallest subproblems, iteratively constructing solutions to larger subproblems until obtaining the solution to the original problem.

Since dynamic programming does not include a backtracking process, it only requires loop iteration for implementation and does not need recursion. In the following code, we initialize an array dp to store the solutions to subproblems, which serves the same recording function as the array mem in memoization:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
def climbing_stairs_dp(n: int) -> int:\n    \"\"\"Climbing stairs: Dynamic programming\"\"\"\n    if n == 1 or n == 2:\n        return n\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [0] * (n + 1)\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1], dp[2] = 1, 2\n    # State transition: gradually solve larger subproblems from smaller ones\n    for i in range(3, n + 1):\n        dp[i] = dp[i - 1] + dp[i - 2]\n    return dp[n]\n
climbing_stairs_dp.cpp
/* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    vector<int> dp(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.java
/* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.cs
/* Climbing stairs: Dynamic programming */\nint ClimbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.go
/* Climbing stairs: Dynamic programming */\nfunc climbingStairsDP(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    dp := make([]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        dp[i] = dp[i-1] + dp[i-2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.swift
/* Climbing stairs: Dynamic programming */\nfunc climbingStairsDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: 0, count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3 ... n {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.js
/* Climbing stairs: Dynamic programming */\nfunction climbingStairsDP(n) {\n    if (n === 1 || n === 2) return n;\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1).fill(-1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.ts
/* Climbing stairs: Dynamic programming */\nfunction climbingStairsDP(n: number): number {\n    if (n === 1 || n === 2) return n;\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1).fill(-1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.dart
/* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n  if (n == 1 || n == 2) return n;\n  // Initialize dp table, used to store solutions to subproblems\n  List<int> dp = List.filled(n + 1, 0);\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1] = 1;\n  dp[2] = 2;\n  // State transition: gradually solve larger subproblems from smaller ones\n  for (int i = 3; i <= n; i++) {\n    dp[i] = dp[i - 1] + dp[i - 2];\n  }\n  return dp[n];\n}\n
climbing_stairs_dp.rs
/* Climbing stairs: Dynamic programming */\nfn climbing_stairs_dp(n: usize) -> i32 {\n    // Known dp[1] and dp[2], return them\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![-1; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3..=n {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    dp[n]\n}\n
climbing_stairs_dp.c
/* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int *dp = (int *)malloc((n + 1) * sizeof(int));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    int result = dp[n];\n    free(dp);\n    return result;\n}\n
climbing_stairs_dp.kt
/* Climbing stairs: Dynamic programming */\nfun climbingStairsDP(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = IntArray(n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (i in 3..n) {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.rb
### Climbing stairs: dynamic programming ###\ndef climbing_stairs_dp(n)\n  return n  if n == 1 || n == 2\n\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = 1, 2\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] }\n\n  dp[n]\nend\n

Figure 14-5 simulates the execution process of the above code.

Figure 14-5   Dynamic programming process for climbing stairs

Like backtracking algorithms, dynamic programming also uses the \"state\" concept to represent specific stages of problem solving, with each state corresponding to a subproblem and its corresponding local optimal solution. For example, the state in the stair climbing problem is defined as the current stair step number \\(i\\).

Based on the above content, we can summarize the commonly used terminology in dynamic programming.

  • The array dp is called the dp table, where \\(dp[i]\\) represents the solution to the subproblem corresponding to state \\(i\\).
  • The states corresponding to the smallest subproblems (the \\(1\\)st and \\(2\\)nd steps) are called initial states.
  • The recurrence formula \\(dp[i] = dp[i-1] + dp[i-2]\\) is called the state transition equation.
","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1414-space-optimization","level":2,"title":"14.1.4   Space Optimization","text":"

Observant readers may have noticed that since \\(dp[i]\\) is only related to \\(dp[i-1]\\) and \\(dp[i-2]\\), we do not need to use an array dp to store the solutions to all subproblems, but can simply use two variables to roll forward. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
def climbing_stairs_dp_comp(n: int) -> int:\n    \"\"\"Climbing stairs: Space-optimized dynamic programming\"\"\"\n    if n == 1 or n == 2:\n        return n\n    a, b = 1, 2\n    for _ in range(3, n + 1):\n        a, b = b, a + b\n    return b\n
climbing_stairs_dp.cpp
/* Climbing stairs: Space-optimized dynamic programming */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.java
/* Climbing stairs: Space-optimized dynamic programming */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.cs
/* Climbing stairs: Space-optimized dynamic programming */\nint ClimbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.go
/* Climbing stairs: Space-optimized dynamic programming */\nfunc climbingStairsDPComp(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    a, b := 1, 2\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        a, b = b, a+b\n    }\n    return b\n}\n
climbing_stairs_dp.swift
/* Climbing stairs: Space-optimized dynamic programming */\nfunc climbingStairsDPComp(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    var a = 1\n    var b = 2\n    for _ in 3 ... n {\n        (a, b) = (b, a + b)\n    }\n    return b\n}\n
climbing_stairs_dp.js
/* Climbing stairs: Space-optimized dynamic programming */\nfunction climbingStairsDPComp(n) {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.ts
/* Climbing stairs: Space-optimized dynamic programming */\nfunction climbingStairsDPComp(n: number): number {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.dart
/* Climbing stairs: Space-optimized dynamic programming */\nint climbingStairsDPComp(int n) {\n  if (n == 1 || n == 2) return n;\n  int a = 1, b = 2;\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = a + b;\n    a = tmp;\n  }\n  return b;\n}\n
climbing_stairs_dp.rs
/* Climbing stairs: Space-optimized dynamic programming */\nfn climbing_stairs_dp_comp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    let (mut a, mut b) = (1, 2);\n    for _ in 3..=n {\n        let tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    b\n}\n
climbing_stairs_dp.c
/* Climbing stairs: Space-optimized dynamic programming */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.kt
/* Climbing stairs: Space-optimized dynamic programming */\nfun climbingStairsDPComp(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    var a = 1\n    var b = 2\n    for (i in 3..n) {\n        val temp = b\n        b += a\n        a = temp\n    }\n    return b\n}\n
climbing_stairs_dp.rb
### Climbing stairs: space-optimized DP ###\ndef climbing_stairs_dp_comp(n)\n  return n if n == 1 || n == 2\n\n  a, b = 1, 2\n  (3...(n + 1)).each { a, b = b, a + b }\n\n  b\nend\n

Observing the above code, since the space occupied by the array dp is saved, the space complexity is reduced from \\(O(n)\\) to \\(O(1)\\).

In dynamic programming problems, the current state often depends only on a limited number of preceding states, allowing us to retain only the necessary states and save memory space through \"dimension reduction\". This space optimization technique is called \"rolling variable\" or \"rolling array\".

","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/","level":1,"title":"14.4   0-1 Knapsack Problem","text":"

The knapsack problem is an excellent introductory problem for dynamic programming and is one of the most common problem forms in dynamic programming. It has many variants, such as the 0-1 knapsack problem, the unbounded knapsack problem, and the multiple knapsack problem.

In this section, we will first solve the most common 0-1 knapsack problem.

Question

Given \\(n\\) items, where the weight of the \\(i\\)-th item is \\(wgt[i-1]\\) and its value is \\(val[i-1]\\), and a knapsack with capacity \\(cap\\). Each item can only be selected once. What is the maximum value that can be placed in the knapsack within the capacity limit?

Observe Figure 14-17. Since item number \\(i\\) starts counting from \\(1\\) and array indices start from \\(0\\), item \\(i\\) corresponds to weight \\(wgt[i-1]\\) and value \\(val[i-1]\\).

Figure 14-17   Example data for 0-1 knapsack

We can view the 0-1 knapsack problem as a process consisting of \\(n\\) rounds of decisions, where for each item there are two decisions: not putting it in and putting it in, thus the problem satisfies the decision tree model.

The goal of this problem is to find \"the maximum value that can be placed in the knapsack within the capacity limit\", so it is more likely to be a dynamic programming problem.

Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

For each item, if not placed in the knapsack, the knapsack capacity remains unchanged; if placed in, the knapsack capacity decreases. From this, we can derive the state definition: current item number \\(i\\) and knapsack capacity \\(c\\), denoted as \\([i, c]\\).

State \\([i, c]\\) corresponds to the subproblem: the maximum value among the first \\(i\\) items in a knapsack of capacity \\(c\\), denoted as \\(dp[i, c]\\).

What we need to find is \\(dp[n, cap]\\), so we need a two-dimensional \\(dp\\) table of size \\((n+1) \\times (cap+1)\\).

Step 2: Identify the optimal substructure, and then derive the state transition equation

After making the decision for item \\(i\\), what remains is the subproblem of the first \\(i-1\\) items, which can be divided into the following two cases.

  • Not putting item \\(i\\): The knapsack capacity remains unchanged, and the state changes to \\([i-1, c]\\).
  • Putting item \\(i\\): The knapsack capacity decreases by \\(wgt[i-1]\\), the value increases by \\(val[i-1]\\), and the state changes to \\([i-1, c-wgt[i-1]]\\).

The above analysis reveals the optimal substructure of this problem: the maximum value \\(dp[i, c]\\) equals the larger value between not putting item \\(i\\) and putting item \\(i\\). From this, the state transition equation can be derived:

\\[ dp[i, c] = \\max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \\]

Note that if the weight of the current item \\(wgt[i - 1]\\) exceeds the remaining knapsack capacity \\(c\\), then the only option is not to put it in the knapsack.

Step 3: Determine boundary conditions and state transition order

When there are no items or the knapsack capacity is \\(0\\), the maximum value is \\(0\\), i.e., the first column \\(dp[i, 0]\\) and the first row \\(dp[0, c]\\) are both equal to \\(0\\).

The current state \\([i, c]\\) is transferred from the state above \\([i-1, c]\\) and the state in the upper-left \\([i-1, c-wgt[i-1]]\\), so the entire \\(dp\\) table is traversed in order through two nested loops.

Based on the above analysis, we will next implement the brute force search, memoization, and dynamic programming solutions in order.

","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#1-method-1-brute-force-search","level":3,"title":"1.   Method 1: Brute Force Search","text":"

The search code includes the following elements.

  • Recursive parameters: state \\([i, c]\\).
  • Return value: solution to the subproblem \\(dp[i, c]\\).
  • Termination condition: when the item number is out of bounds \\(i = 0\\) or the remaining knapsack capacity is \\(0\\), terminate recursion and return value \\(0\\).
  • Pruning: if the weight of the current item exceeds the remaining knapsack capacity, only the option of not putting it in is available.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:\n    \"\"\"0-1 knapsack: Brute-force search\"\"\"\n    # If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 or c == 0:\n        return 0\n    # If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c:\n        return knapsack_dfs(wgt, val, i - 1, c)\n    # Calculate the maximum value of not putting in and putting in item i\n    no = knapsack_dfs(wgt, val, i - 1, c)\n    yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # Return the larger value of the two options\n    return max(no, yes)\n
knapsack.cpp
/* 0-1 knapsack: Brute-force search */\nint knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return max(no, yes);\n}\n
knapsack.java
/* 0-1 knapsack: Brute-force search */\nint knapsackDFS(int[] wgt, int[] val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
knapsack.cs
/* 0-1 knapsack: Brute-force search */\nint KnapsackDFS(int[] weight, int[] val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (weight[i - 1] > c) {\n        return KnapsackDFS(weight, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = KnapsackDFS(weight, val, i - 1, c);\n    int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return Math.Max(no, yes);\n}\n
knapsack.go
/* 0-1 knapsack: Brute-force search */\nfunc knapsackDFS(wgt, val []int, i, c int) int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i-1] > c {\n        return knapsackDFS(wgt, val, i-1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    no := knapsackDFS(wgt, val, i-1, c)\n    yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1]\n    // Return the larger value of the two options\n    return int(math.Max(float64(no), float64(yes)))\n}\n
knapsack.swift
/* 0-1 knapsack: Brute-force search */\nfunc knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c {\n        return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    let no = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    let yes = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // Return the larger value of the two options\n    return max(no, yes)\n}\n
knapsack.js
/* 0-1 knapsack: Brute-force search */\nfunction knapsackDFS(wgt, val, i, c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
knapsack.ts
/* 0-1 knapsack: Brute-force search */\nfunction knapsackDFS(\n    wgt: Array<number>,\n    val: Array<number>,\n    i: number,\n    c: number\n): number {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
knapsack.dart
/* 0-1 knapsack: Brute-force search */\nint knapsackDFS(List<int> wgt, List<int> val, int i, int c) {\n  // If all items have been selected or knapsack has no remaining capacity, return value 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // If exceeds knapsack capacity, can only choose not to put it in\n  if (wgt[i - 1] > c) {\n    return knapsackDFS(wgt, val, i - 1, c);\n  }\n  // Calculate the maximum value of not putting in and putting in item i\n  int no = knapsackDFS(wgt, val, i - 1, c);\n  int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // Return the larger value of the two options\n  return max(no, yes);\n}\n
knapsack.rs
/* 0-1 knapsack: Brute-force search */\nfn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    let no = knapsack_dfs(wgt, val, i - 1, c);\n    let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // Return the larger value of the two options\n    std::cmp::max(no, yes)\n}\n
knapsack.c
/* 0-1 knapsack: Brute-force search */\nint knapsackDFS(int wgt[], int val[], int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return myMax(no, yes);\n}\n
knapsack.kt
/* 0-1 knapsack: Brute-force search */\nfun knapsackDFS(\n    wgt: IntArray,\n    _val: IntArray,\n    i: Int,\n    c: Int\n): Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, _val, i - 1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    val no = knapsackDFS(wgt, _val, i - 1, c)\n    val yes = knapsackDFS(wgt, _val, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // Return the larger value of the two options\n    return max(no, yes)\n}\n
knapsack.rb
### 0-1 knapsack: brute force search ###\ndef knapsack_dfs(wgt, val, i, c)\n  # If all items have been selected or knapsack has no remaining capacity, return value 0\n  return 0 if i == 0 || c == 0\n  # If exceeds knapsack capacity, can only choose not to put it in\n  return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c\n  # Calculate the maximum value of not putting in and putting in item i\n  no = knapsack_dfs(wgt, val, i - 1, c)\n  yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # Return the larger value of the two options\n  [no, yes].max\nend\n

As shown in Figure 14-18, since each item generates two search branches of not selecting and selecting, the time complexity is \\(O(2^n)\\).

Observing the recursion tree, it is easy to see overlapping subproblems, such as \\(dp[1, 10]\\). When there are many items, large knapsack capacity, and especially many items with the same weight, the number of overlapping subproblems will increase significantly.

Figure 14-18   Brute force search recursion tree for 0-1 knapsack problem

","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#2-method-2-memoization","level":3,"title":"2.   Method 2: Memoization","text":"

To ensure that overlapping subproblems are only computed once, we use a memo list mem to record the solutions to subproblems, where mem[i][c] corresponds to \\(dp[i, c]\\).

After introducing memoization, the time complexity depends on the number of subproblems, which is \\(O(n \\times cap)\\). The implementation code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dfs_mem(\n    wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int\n) -> int:\n    \"\"\"0-1 knapsack: Memoization search\"\"\"\n    # If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 or c == 0:\n        return 0\n    # If there's a record, return it directly\n    if mem[i][c] != -1:\n        return mem[i][c]\n    # If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c:\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    # Calculate the maximum value of not putting in and putting in item i\n    no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n
knapsack.cpp
/* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(vector<int> &wgt, vector<int> &val, vector<vector<int>> &mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes);\n    return mem[i][c];\n}\n
knapsack.java
/* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.cs
/* 0-1 knapsack: Memoization search */\nint KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (weight[i - 1] > c) {\n        return KnapsackDFSMem(weight, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = KnapsackDFSMem(weight, val, mem, i - 1, c);\n    int yes = KnapsackDFSMem(weight, val, mem, i - 1, c - weight[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = Math.Max(no, yes);\n    return mem[i][c];\n}\n
knapsack.go
/* 0-1 knapsack: Memoization search */\nfunc knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i-1] > c {\n        return knapsackDFSMem(wgt, val, mem, i-1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    no := knapsackDFSMem(wgt, val, mem, i-1, c)\n    yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1]\n    // Return the larger value of the two options\n    mem[i][c] = int(math.Max(float64(no), float64(yes)))\n    return mem[i][c]\n}\n
knapsack.swift
/* 0-1 knapsack: Memoization search */\nfunc knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c {\n        return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    let no = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    let yes = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
knapsack.js
/* 0-1 knapsack: Memoization search */\nfunction knapsackDFSMem(wgt, val, mem, i, c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.ts
/* 0-1 knapsack: Memoization search */\nfunction knapsackDFSMem(\n    wgt: Array<number>,\n    val: Array<number>,\n    mem: Array<Array<number>>,\n    i: number,\n    c: number\n): number {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.dart
/* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(\n  List<int> wgt,\n  List<int> val,\n  List<List<int>> mem,\n  int i,\n  int c,\n) {\n  // If all items have been selected or knapsack has no remaining capacity, return value 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // If there's a record, return it directly\n  if (mem[i][c] != -1) {\n    return mem[i][c];\n  }\n  // If exceeds knapsack capacity, can only choose not to put it in\n  if (wgt[i - 1] > c) {\n    return knapsackDFSMem(wgt, val, mem, i - 1, c);\n  }\n  // Calculate the maximum value of not putting in and putting in item i\n  int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n  int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // Record and return the larger value of the two options\n  mem[i][c] = max(no, yes);\n  return mem[i][c];\n}\n
knapsack.rs
/* 0-1 knapsack: Memoization search */\nfn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec<Vec<i32>>, i: usize, c: usize) -> i32 {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = std::cmp::max(no, yes);\n    mem[i][c]\n}\n
knapsack.c
/* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = myMax(no, yes);\n    return mem[i][c];\n}\n
knapsack.kt
/* 0-1 knapsack: Memoization search */\nfun knapsackDFSMem(\n    wgt: IntArray,\n    _val: IntArray,\n    mem: Array<IntArray>,\n    i: Int,\n    c: Int\n): Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    val no = knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    val yes = knapsackDFSMem(wgt, _val, mem, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
knapsack.rb
### 0-1 knapsack: memoization search ###\ndef knapsack_dfs_mem(wgt, val, mem, i, c)\n  # If all items have been selected or knapsack has no remaining capacity, return value 0\n  return 0 if i == 0 || c == 0\n  # If there's a record, return it directly\n  return mem[i][c] if mem[i][c] != -1\n  # If exceeds knapsack capacity, can only choose not to put it in\n  return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c\n  # Calculate the maximum value of not putting in and putting in item i\n  no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n  yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # Record and return the larger value of the two options\n  mem[i][c] = [no, yes].max\nend\n

Figure 14-19 shows the search branches pruned in memoization.

Figure 14-19   Memoization recursion tree for 0-1 knapsack problem

","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#3-method-3-dynamic-programming","level":3,"title":"3.   Method 3: Dynamic Programming","text":"

Dynamic programming is essentially the process of filling the \\(dp\\) table during state transitions. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 knapsack: Dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # State transition\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
knapsack.cpp
/* 0-1 knapsack: Dynamic programming */\nint knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.java
/* 0-1 knapsack: Dynamic programming */\nint knapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.cs
/* 0-1 knapsack: Dynamic programming */\nint KnapsackDP(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (weight[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i, c] = Math.Max(dp[i - 1, c - weight[i - 1]] + val[i - 1], dp[i - 1, c]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
knapsack.go
/* 0-1 knapsack: Dynamic programming */\nfunc knapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i-1][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.swift
/* 0-1 knapsack: Dynamic programming */\nfunc knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.js
/* 0-1 knapsack: Dynamic programming */\nfunction knapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(n + 1)\n        .fill(0)\n        .map(() => Array(cap + 1).fill(0));\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.ts
/* 0-1 knapsack: Dynamic programming */\nfunction knapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.dart
/* 0-1 knapsack: Dynamic programming */\nint knapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // The larger value between not selecting and selecting item i\n        dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
knapsack.rs
/* 0-1 knapsack: Dynamic programming */\nfn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = std::cmp::max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1],\n                );\n            }\n        }\n    }\n    dp[n][cap]\n}\n
knapsack.c
/* 0-1 knapsack: Dynamic programming */\nint knapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
knapsack.kt
/* 0-1 knapsack: Dynamic programming */\nfun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.rb
### 0-1 knapsack: dynamic programming ###\ndef knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # State transition\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c]\n      else\n        # The larger value between not selecting and selecting item i\n        dp[i][c] = [dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n

As shown in Figure 14-20, both time complexity and space complexity are determined by the size of the array dp, which is \\(O(n \\times cap)\\).

<1><2><3><4><5><6><7><8><9><10><11><12><13><14>

Figure 14-20   Dynamic programming process for 0-1 knapsack problem

","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#4-space-optimization","level":3,"title":"4.   Space Optimization","text":"

Since each state is only related to the state in the row above it, we can use two arrays rolling forward to reduce the space complexity from \\(O(n^2)\\) to \\(O(n)\\).

Further thinking, can we achieve space optimization using just one array? Observing, we can see that each state is transferred from the cell directly above or the cell in the upper-left. If there is only one array, when we start traversing row \\(i\\), that array still stores the state of row \\(i-1\\).

  • If using forward traversal, then when traversing to \\(dp[i, j]\\), the values in the upper-left \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\) may have already been overwritten, thus preventing correct state transition.
  • If using reverse traversal, there will be no overwriting issue, and state transition can proceed correctly.

Figure 14-21 shows the transition process from row \\(i = 1\\) to row \\(i = 2\\) using a single array. Please consider the difference between forward and reverse traversal.

<1><2><3><4><5><6>

Figure 14-21   Space-optimized dynamic programming process for 0-1 knapsack

In the code implementation, we simply need to delete the first dimension \\(i\\) of the array dp and change the inner loop to reverse traversal:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 knapsack: Space-optimized dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [0] * (cap + 1)\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in reverse order\n        for c in range(cap, 0, -1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
knapsack.cpp
/* 0-1 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<int> dp(cap + 1, 0);\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.java
/* 0-1 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.cs
/* 0-1 knapsack: Space-optimized dynamic programming */\nint KnapsackDPComp(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c > 0; c--) {\n            if (weight[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.go
/* 0-1 knapsack: Space-optimized dynamic programming */\nfunc knapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([]int, cap+1)\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in reverse order\n        for c := cap; c >= 1; c-- {\n            if wgt[i-1] <= c {\n                // The larger value between not selecting and selecting item i\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.swift
/* 0-1 knapsack: Space-optimized dynamic programming */\nfunc knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: cap + 1)\n    // State transition\n    for i in 1 ... n {\n        // Traverse in reverse order\n        for c in (1 ... cap).reversed() {\n            if wgt[i - 1] <= c {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.js
/* 0-1 knapsack: Space-optimized dynamic programming */\nfunction knapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(cap + 1).fill(0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.ts
/* 0-1 knapsack: Space-optimized dynamic programming */\nfunction knapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(cap + 1).fill(0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.dart
/* 0-1 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<int> dp = List.filled(cap + 1, 0);\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    // Traverse in reverse order\n    for (int c = cap; c >= 1; c--) {\n      if (wgt[i - 1] <= c) {\n        // The larger value between not selecting and selecting item i\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
knapsack.rs
/* 0-1 knapsack: Space-optimized dynamic programming */\nfn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![0; cap + 1];\n    // State transition\n    for i in 1..=n {\n        // Traverse in reverse order\n        for c in (1..=cap).rev() {\n            if wgt[i - 1] <= c as i32 {\n                // The larger value between not selecting and selecting item i\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
knapsack.c
/* 0-1 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int *dp = calloc(cap + 1, sizeof(int));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Free memory\n    free(dp);\n    return res;\n}\n
knapsack.kt
/* 0-1 knapsack: Space-optimized dynamic programming */\nfun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = IntArray(cap + 1)\n    // State transition\n    for (i in 1..n) {\n        // Traverse in reverse order\n        for (c in cap downTo 1) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.rb
### 0-1 knapsack: space-optimized DP ###\ndef knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(cap + 1, 0)\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in reverse order\n    for c in cap.downto(1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c]\n      else\n        # The larger value between not selecting and selecting item i\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/summary/","level":1,"title":"14.7   Summary","text":"","path":["Chapter 14. Dynamic Programming","14.7   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Dynamic programming decomposes problems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving computational efficiency.
  • Without considering time constraints, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree contains a large number of overlapping subproblems, resulting in extremely low efficiency. By introducing a memo list, we can store the solutions to all computed subproblems, ensuring that overlapping subproblems are only computed once.
  • Memoization is a top-down recursive solution, while the corresponding dynamic programming is a bottom-up iterative solution, similar to \"filling in a table\". Since the current state only depends on certain local states, we can eliminate one dimension of the \\(dp\\) table to reduce space complexity.
  • Subproblem decomposition is a general algorithmic approach, with different properties in divide and conquer, dynamic programming, and backtracking.
  • Dynamic programming problems have three major characteristics: overlapping subproblems, optimal substructure, and no aftereffects.
  • If the optimal solution to the original problem can be constructed from the optimal solutions to the subproblems, then it has optimal substructure.
  • No aftereffects means that for a given state, its future development is only related to that state and has nothing to do with all past states. Many combinatorial optimization problems do not have no aftereffects and cannot be quickly solved using dynamic programming.

Knapsack problem

  • The knapsack problem is one of the most typical dynamic programming problems, with variants such as the 0-1 knapsack, unbounded knapsack, and multiple knapsack.
  • The state definition for the 0-1 knapsack is the maximum value among the first \\(i\\) items in a knapsack of capacity \\(c\\). Based on the two decisions of not putting an item in the knapsack and putting it in, the optimal substructure can be identified and the state transition equation constructed. In space optimization, since each state depends on the state directly above and to the upper-left, the list needs to be traversed in reverse order to avoid overwriting the upper-left state.
  • The unbounded knapsack problem has no limit on the selection quantity of each type of item, so the state transition for choosing to put in an item differs from the 0-1 knapsack problem. Since the state depends on the state directly above and directly to the left, space optimization should use forward traversal.
  • The coin change problem is a variant of the unbounded knapsack problem. It changes from seeking the \"maximum\" value to seeking the \"minimum\" number of coins, so \\(\\max()\\) in the state transition equation should be changed to \\(\\min()\\). It changes from seeking \"not exceeding\" the knapsack capacity to seeking \"exactly\" making up the target amount, so \\(amt + 1\\) is used to represent the invalid solution of \"unable to make up the target amount\".
  • Coin change problem II changes from seeking the \"minimum number of coins\" to seeking the \"number of coin combinations\", so the state transition equation correspondingly changes from \\(\\min()\\) to a summation operator.

Edit distance problem

  • Edit distance (Levenshtein distance) is used to measure the similarity between two strings, defined as the minimum number of edit steps from one string to another, with edit operations including insert, delete, and replace.
  • The state definition for the edit distance problem is the minimum number of edit steps required to change the first \\(i\\) characters of \\(s\\) into the first \\(j\\) characters of \\(t\\). When \\(s[i] \\ne t[j]\\), there are three decisions: insert, delete, replace, each with corresponding remaining subproblems. From this, the optimal substructure can be identified and the state transition equation constructed. When \\(s[i] = t[j]\\), no edit is required for the current character.
  • In edit distance, the state depends on the state directly above, directly to the left, and to the upper-left, so after space optimization, neither forward nor reverse traversal can correctly perform state transitions. For this reason, we use a variable to temporarily store the upper-left state, thus transforming to a situation equivalent to the unbounded knapsack problem, allowing for forward traversal after space optimization.
","path":["Chapter 14. Dynamic Programming","14.7   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   Unbounded Knapsack Problem","text":"

In this section, we first solve another common knapsack problem: the unbounded knapsack, and then explore a special case of it: the coin change problem.

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1451-unbounded-knapsack-problem","level":2,"title":"14.5.1   Unbounded Knapsack Problem","text":"

Question

Given \\(n\\) items, where the weight of the \\(i\\)-th item is \\(wgt[i-1]\\) and its value is \\(val[i-1]\\), and a knapsack with capacity \\(cap\\). Each item can be selected multiple times. What is the maximum value that can be placed in the knapsack within the capacity limit? An example is shown in Figure 14-22.

Figure 14-22   Example data for unbounded knapsack problem

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach","level":3,"title":"1.   Dynamic Programming Approach","text":"

The unbounded knapsack problem is very similar to the 0-1 knapsack problem, differing only in that there is no limit on the number of times an item can be selected.

  • In the 0-1 knapsack problem, there is only one of each type of item, so after placing item \\(i\\) in the knapsack, we can only choose from the first \\(i-1\\) items.
  • In the unbounded knapsack problem, the quantity of each type of item is unlimited, so after placing item \\(i\\) in the knapsack, we can still choose from the first \\(i\\) items.

Under the rules of the unbounded knapsack problem, the changes in state \\([i, c]\\) are divided into two cases.

  • Not putting item \\(i\\): Same as the 0-1 knapsack problem, transfer to \\([i-1, c]\\).
  • Putting item \\(i\\): Different from the 0-1 knapsack problem, transfer to \\([i, c-wgt[i-1]]\\).

Thus, the state transition equation becomes:

\\[ dp[i, c] = \\max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \\]","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

Comparing the code for the two problems, there is one change in state transition from \\(i-1\\) to \\(i\\), with everything else identical:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Unbounded knapsack: Dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # State transition\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
unbounded_knapsack.cpp
/* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.java
/* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.cs
/* Unbounded knapsack: Dynamic programming */\nint UnboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i, c] = Math.Max(dp[i - 1, c], dp[i, c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
unbounded_knapsack.go
/* Unbounded knapsack: Dynamic programming */\nfunc unboundedKnapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.swift
/* Unbounded knapsack: Dynamic programming */\nfunc unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.js
/* Unbounded knapsack: Dynamic programming */\nfunction unboundedKnapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.ts
/* Unbounded knapsack: Dynamic programming */\nfunction unboundedKnapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.dart
/* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // The larger value between not selecting and selecting item i\n        dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
unbounded_knapsack.rs
/* Unbounded knapsack: Dynamic programming */\nfn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.c
/* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = myMax(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
unbounded_knapsack.kt
/* Unbounded knapsack: Dynamic programming */\nfun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.rb
### Unbounded knapsack: dynamic programming ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # State transition\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c]\n      else\n        # The larger value between not selecting and selecting item i\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization","level":3,"title":"3.   Space Optimization","text":"

Since the current state is transferred from states on the left and above, after space optimization, each row in the \\(dp\\) table should be traversed in forward order.

This traversal order is exactly opposite to the 0-1 knapsack. Please refer to Figure 14-23 to understand the difference between the two.

<1><2><3><4><5><6>

Figure 14-23   Space-optimized dynamic programming process for unbounded knapsack problem

The code implementation is relatively simple, just delete the first dimension of the array dp:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Unbounded knapsack: Space-optimized dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [0] * (cap + 1)\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
unbounded_knapsack.cpp
/* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<int> dp(cap + 1, 0);\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.java
/* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.cs
/* Unbounded knapsack: Space-optimized dynamic programming */\nint UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.go
/* Unbounded knapsack: Space-optimized dynamic programming */\nfunc unboundedKnapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([]int, cap+1)\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.swift
/* Unbounded knapsack: Space-optimized dynamic programming */\nfunc unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: cap + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.js
/* Unbounded knapsack: Space-optimized dynamic programming */\nfunction unboundedKnapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.ts
/* Unbounded knapsack: Space-optimized dynamic programming */\nfunction unboundedKnapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.dart
/* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<int> dp = List.filled(cap + 1, 0);\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c];\n      } else {\n        // The larger value between not selecting and selecting item i\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
unbounded_knapsack.rs
/* Unbounded knapsack: Space-optimized dynamic programming */\nfn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![0; cap + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
unbounded_knapsack.c
/* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int *dp = calloc(cap + 1, sizeof(int));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Free memory\n    free(dp);\n    return res;\n}\n
unbounded_knapsack.kt
/* Unbounded knapsack: Space-optimized dynamic programming */\nfun unboundedKnapsackDPComp(\n    wgt: IntArray,\n    _val: IntArray,\n    cap: Int\n): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = IntArray(cap + 1)\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.rb
### Unbounded knapsack: space-optimized DP ###\ndef unbounded_knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(cap + 1, 0)\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for c in 1...(cap + 1)\n      if wgt[i -1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c]\n      else\n        # The larger value between not selecting and selecting item i\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1452-coin-change-problem","level":2,"title":"14.5.2   Coin Change Problem","text":"

The knapsack problem represents a large class of dynamic programming problems and has many variants, such as the coin change problem.

Question

Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type of coin is \\(coins[i - 1]\\), and the target amount is \\(amt\\). Each type of coin can be selected multiple times. What is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return \\(-1\\). An example is shown in Figure 14-24.

Figure 14-24   Example data for coin change problem

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach_1","level":3,"title":"1.   Dynamic Programming Approach","text":"

The coin change problem can be viewed as a special case of the unbounded knapsack problem, with the following connections and differences.

  • The two problems can be converted to each other: \"item\" corresponds to \"coin\", \"item weight\" corresponds to \"coin denomination\", and \"knapsack capacity\" corresponds to \"target amount\".
  • The optimization goals are opposite: the unbounded knapsack problem aims to maximize item value, while the coin change problem aims to minimize the number of coins.
  • The unbounded knapsack problem seeks solutions \"not exceeding\" the knapsack capacity, while the coin change problem seeks solutions that \"exactly\" make up the target amount.

Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

State \\([i, a]\\) corresponds to the subproblem: the minimum number of coins among the first \\(i\\) types of coins that can make up amount \\(a\\), denoted as \\(dp[i, a]\\).

The two-dimensional \\(dp\\) table has size \\((n+1) \\times (amt+1)\\).

Step 2: Identify the optimal substructure, and then derive the state transition equation

This problem differs from the unbounded knapsack problem in the following two aspects regarding the state transition equation.

  • This problem seeks the minimum value, so the operator \\(\\max()\\) needs to be changed to \\(\\min()\\).
  • The optimization target is the number of coins rather than item value, so when a coin is selected, simply execute \\(+1\\).
\\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

Step 3: Determine boundary conditions and state transition order

When the target amount is \\(0\\), the minimum number of coins needed to make it up is \\(0\\), so all \\(dp[i, 0]\\) in the first column equal \\(0\\).

When there are no coins, it is impossible to make up any amount \\(> 0\\), which is an invalid solution. To enable the \\(\\min()\\) function in the state transition equation to identify and filter out invalid solutions, we consider using \\(+ \\infty\\) to represent them, i.e., set all \\(dp[0, a]\\) in the first row to \\(+ \\infty\\).

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation_1","level":3,"title":"2.   Code Implementation","text":"

Most programming languages do not provide a \\(+ \\infty\\) variable, and can only use the maximum value of integer type int as a substitute. However, this can lead to large number overflow: the \\(+ 1\\) operation in the state transition equation may cause overflow.

For this reason, we use the number \\(amt + 1\\) to represent invalid solutions, because the maximum number of coins needed to make up \\(amt\\) is at most \\(amt\\). Before returning, check whether \\(dp[n, amt]\\) equals \\(amt + 1\\); if so, return \\(-1\\), indicating that the target amount cannot be made up. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
def coin_change_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Dynamic programming\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Initialize dp table\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # State transition: first row and first column\n    for a in range(1, amt + 1):\n        dp[0][a] = MAX\n    # State transition: rest of the rows and columns\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # The smaller value between not selecting and selecting coin i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n    return dp[n][amt] if dp[n][amt] != MAX else -1\n
coin_change.cpp
/* Coin change: Dynamic programming */\nint coinChangeDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.java
/* Coin change: Dynamic programming */\nint coinChangeDP(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][amt + 1];\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.cs
/* Coin change: Dynamic programming */\nint CoinChangeDP(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, amt + 1];\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0, a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i, a] = Math.Min(dp[i - 1, a], dp[i, a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n, amt] != MAX ? dp[n, amt] : -1;\n}\n
coin_change.go
/* Coin change: Dynamic programming */\nfunc coinChangeDP(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // State transition: first row and first column\n    for a := 1; a <= amt; a++ {\n        dp[0][a] = max\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = int(math.Min(float64(dp[i-1][a]), float64(dp[i][a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt]\n    }\n    return -1\n}\n
coin_change.swift
/* Coin change: Dynamic programming */\nfunc coinChangeDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // State transition: first row and first column\n    for a in 1 ... amt {\n        dp[0][a] = MAX\n    }\n    // State transition: rest of the rows and columns\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1\n}\n
coin_change.js
/* Coin change: Dynamic programming */\nfunction coinChangeDP(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // State transition: first row and first column\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
coin_change.ts
/* Coin change: Dynamic programming */\nfunction coinChangeDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // State transition: first row and first column\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
coin_change.dart
/* Coin change: Dynamic programming */\nint coinChangeDP(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // State transition: first row and first column\n  for (int a = 1; a <= amt; a++) {\n    dp[0][a] = MAX;\n  }\n  // State transition: rest of the rows and columns\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // The smaller value between not selecting and selecting coin i\n        dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.rs
/* Coin change: Dynamic programming */\nfn coin_change_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Initialize dp table\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // State transition: first row and first column\n    for a in 1..=amt {\n        dp[0][a] = max;\n    }\n    // State transition: rest of the rows and columns\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt] as i32;\n    } else {\n        -1\n    }\n}\n
coin_change.c
/* Coin change: Dynamic programming */\nint coinChangeDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[n][amt] != MAX ? dp[n][amt] : -1;\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
coin_change.kt
/* Coin change: Dynamic programming */\nfun coinChangeDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // State transition: first row and first column\n    for (a in 1..amt) {\n        dp[0][a] = MAX\n    }\n    // State transition: rest of the rows and columns\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[n][amt] != MAX) dp[n][amt] else -1\n}\n
coin_change.rb
### Coin change: dynamic programming ###\ndef coin_change_dp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # State transition: first row and first column\n  (1...(amt + 1)).each { |a| dp[0][a] = _MAX }\n  # State transition: rest of the rows and columns\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a]\n      else\n        # The smaller value between not selecting and selecting coin i\n        dp[i][a] = [dp[i - 1][a], dp[i][a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[n][amt] != _MAX ? dp[n][amt] : -1\nend\n

Figure 14-25 shows the dynamic programming process for coin change, which is very similar to the unbounded knapsack problem.

<1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

Figure 14-25   Dynamic programming process for coin change problem

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization_1","level":3,"title":"3.   Space Optimization","text":"

The space optimization for the coin change problem is handled in the same way as the unbounded knapsack problem:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
def coin_change_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Space-optimized dynamic programming\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Initialize dp table\n    dp = [MAX] * (amt + 1)\n    dp[0] = 0\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            else:\n                # The smaller value between not selecting and selecting coin i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n    return dp[amt] if dp[amt] != MAX else -1\n
coin_change.cpp
/* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Initialize dp table\n    vector<int> dp(amt + 1, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.java
/* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    Arrays.fill(dp, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.cs
/* Coin change: Space-optimized dynamic programming */\nint CoinChangeDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    Array.Fill(dp, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = Math.Min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.go
/* Coin change: Dynamic programming */\nfunc coinChangeDPComp(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Initialize dp table\n    dp := make([]int, amt+1)\n    for i := 1; i <= amt; i++ {\n        dp[i] = max\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in forward order\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = int(math.Min(float64(dp[a]), float64(dp[a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt]\n    }\n    return -1\n}\n
coin_change.swift
/* Coin change: Space-optimized dynamic programming */\nfunc coinChangeDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Initialize dp table\n    var dp = Array(repeating: MAX, count: amt + 1)\n    dp[0] = 0\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1\n}\n
coin_change.js
/* Coin change: Space-optimized dynamic programming */\nfunction coinChangeDPComp(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
coin_change.ts
/* Coin change: Space-optimized dynamic programming */\nfunction coinChangeDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
coin_change.dart
/* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Initialize dp table\n  List<int> dp = List.filled(amt + 1, MAX);\n  dp[0] = 0;\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[a] = dp[a];\n      } else {\n        // The smaller value between not selecting and selecting coin i\n        dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.rs
/* Coin change: Space-optimized dynamic programming */\nfn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Initialize dp table\n    let mut dp = vec![0; amt + 1];\n    dp.fill(max);\n    dp[0] = 0;\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt] as i32;\n    } else {\n        -1\n    }\n}\n
coin_change.c
/* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int *dp = malloc((amt + 1) * sizeof(int));\n    for (int j = 1; j <= amt; j++) {\n        dp[j] = MAX;\n    } \n    dp[0] = 0;\n\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[amt] != MAX ? dp[amt] : -1;\n    // Free memory\n    free(dp);\n    return res;\n}\n
coin_change.kt
/* Coin change: Space-optimized dynamic programming */\nfun coinChangeDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Initialize dp table\n    val dp = IntArray(amt + 1)\n    dp.fill(MAX)\n    dp[0] = 0\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[amt] != MAX) dp[amt] else -1\n}\n
coin_change.rb
### Coin change: space-optimized DP ###\ndef coin_change_dp_comp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Initialize dp table\n  dp = Array.new(amt + 1, _MAX)\n  dp[0] = 0\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[a] = dp[a]\n      else\n        # The smaller value between not selecting and selecting coin i\n        dp[a] = [dp[a], dp[a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[amt] != _MAX ? dp[amt] : -1\nend\n
","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1453-coin-change-problem-ii","level":2,"title":"14.5.3   Coin Change Problem Ii","text":"

Question

Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type of coin is \\(coins[i - 1]\\), and the target amount is \\(amt\\). Each type of coin can be selected multiple times. What is the number of coin combinations that can make up the target amount? An example is shown in Figure 14-26.

Figure 14-26   Example data for coin change problem II

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach_2","level":3,"title":"1.   Dynamic Programming Approach","text":"

Compared to the previous problem, this problem's goal is to find the number of combinations, so the subproblem becomes: the number of combinations among the first \\(i\\) types of coins that can make up amount \\(a\\). The \\(dp\\) table remains a two-dimensional matrix of size \\((n+1) \\times (amt + 1)\\).

The number of combinations for the current state equals the sum of the combinations from not selecting the current coin and selecting the current coin. The state transition equation is:

\\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\]

When the target amount is \\(0\\), no coins need to be selected to make up the target amount, so all \\(dp[i, 0]\\) in the first column should be initialized to \\(1\\). When there are no coins, it is impossible to make up any amount \\(>0\\), so all \\(dp[0, a]\\) in the first row equal \\(0\\).

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation_2","level":3,"title":"2.   Code Implementation","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
def coin_change_ii_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change II: Dynamic programming\"\"\"\n    n = len(coins)\n    # Initialize dp table\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # Initialize first column\n    for i in range(n + 1):\n        dp[i][0] = 1\n    # State transition\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n    return dp[n][amt]\n
coin_change_ii.cpp
/* Coin change II: Dynamic programming */\nint coinChangeIIDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.java
/* Coin change II: Dynamic programming */\nint coinChangeIIDP(int[] coins, int amt) {\n    int n = coins.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][amt + 1];\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.cs
/* Coin change II: Dynamic programming */\nint CoinChangeIIDP(int[] coins, int amt) {\n    int n = coins.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, amt + 1];\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i, 0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i, a] = dp[i - 1, a] + dp[i, a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n, amt];\n}\n
coin_change_ii.go
/* Coin change II: Dynamic programming */\nfunc coinChangeIIDP(coins []int, amt int) int {\n    n := len(coins)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // Initialize first column\n    for i := 0; i <= n; i++ {\n        dp[i][0] = 1\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.swift
/* Coin change II: Dynamic programming */\nfunc coinChangeIIDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // Initialize first column\n    for i in 0 ... n {\n        dp[i][0] = 1\n    }\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.js
/* Coin change II: Dynamic programming */\nfunction coinChangeIIDP(coins, amt) {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Initialize first column\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.ts
/* Coin change II: Dynamic programming */\nfunction coinChangeIIDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Initialize first column\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.dart
/* Coin change II: Dynamic programming */\nint coinChangeIIDP(List<int> coins, int amt) {\n  int n = coins.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // Initialize first column\n  for (int i = 0; i <= n; i++) {\n    dp[i][0] = 1;\n  }\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // Sum of the two options: not selecting and selecting coin i\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[n][amt];\n}\n
coin_change_ii.rs
/* Coin change II: Dynamic programming */\nfn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // Initialize first column\n    for i in 0..=n {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[n][amt]\n}\n
coin_change_ii.c
/* Coin change II: Dynamic programming */\nint coinChangeIIDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Initialize dp table\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[n][amt];\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
coin_change_ii.kt
/* Coin change II: Dynamic programming */\nfun coinChangeIIDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // Initialize first column\n    for (i in 0..n) {\n        dp[i][0] = 1\n    }\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.rb
### Coin change II: dynamic programming ###\ndef coin_change_ii_dp(coins, amt)\n  n = coins.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # Initialize first column\n  (0...(n + 1)).each { |i| dp[i][0] = 1 }\n  # State transition\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a]\n      else\n        # Sum of the two options: not selecting and selecting coin i\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n      end\n    end\n  end\n  dp[n][amt]\nend\n
","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization_2","level":3,"title":"3.   Space Optimization","text":"

The space optimization is handled in the same way, just delete the coin dimension:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change II: Space-optimized dynamic programming\"\"\"\n    n = len(coins)\n    # Initialize dp table\n    dp = [0] * (amt + 1)\n    dp[0] = 1\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            else:\n                # Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n    return dp[amt]\n
coin_change_ii.cpp
/* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Initialize dp table\n    vector<int> dp(amt + 1, 0);\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.java
/* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.cs
/* Coin change II: Space-optimized dynamic programming */\nint CoinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.go
/* Coin change II: Space-optimized dynamic programming */\nfunc coinChangeIIDPComp(coins []int, amt int) int {\n    n := len(coins)\n    // Initialize dp table\n    dp := make([]int, amt+1)\n    dp[0] = 1\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in forward order\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a-coins[i-1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.swift
/* Coin change II: Space-optimized dynamic programming */\nfunc coinChangeIIDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: amt + 1)\n    dp[0] = 1\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.js
/* Coin change II: Space-optimized dynamic programming */\nfunction coinChangeIIDPComp(coins, amt) {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.ts
/* Coin change II: Space-optimized dynamic programming */\nfunction coinChangeIIDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.dart
/* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  // Initialize dp table\n  List<int> dp = List.filled(amt + 1, 0);\n  dp[0] = 1;\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[a] = dp[a];\n      } else {\n        // Sum of the two options: not selecting and selecting coin i\n        dp[a] = dp[a] + dp[a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[amt];\n}\n
coin_change_ii.rs
/* Coin change II: Space-optimized dynamic programming */\nfn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Initialize dp table\n    let mut dp = vec![0; amt + 1];\n    dp[0] = 1;\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[amt]\n}\n
coin_change_ii.c
/* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Initialize dp table\n    int *dp = calloc(amt + 1, sizeof(int));\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[amt];\n    // Free memory\n    free(dp);\n    return res;\n}\n
coin_change_ii.kt
/* Coin change II: Space-optimized dynamic programming */\nfun coinChangeIIDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Initialize dp table\n    val dp = IntArray(amt + 1)\n    dp[0] = 1\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.rb
### Coin change II: space-optimized DP ###\ndef coin_change_ii_dp_comp(coins, amt)\n  n = coins.length\n  # Initialize dp table\n  dp = Array.new(amt + 1, 0)\n  dp[0] = 1\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[a] = dp[a]\n      else\n        # Sum of the two options: not selecting and selecting coin i\n        dp[a] = dp[a] + dp[a - coins[i - 1]]\n      end\n    end\n  end\n  dp[amt]\nend\n
","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_graph/","level":1,"title":"Chapter 9.   Graph","text":"

Abstract

In the journey of life, we are like nodes, connected by countless invisible edges.

Each encounter and parting leaves a unique mark on this vast network graph.

","path":["Chapter 9. Graph","Chapter 9.   Graph"],"tags":[]},{"location":"chapter_graph/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 9.1   Graph
  • 9.2   Basic Operations on Graphs
  • 9.3   Graph Traversal
  • 9.4   Summary
","path":["Chapter 9. Graph","Chapter 9.   Graph"],"tags":[]},{"location":"chapter_graph/graph/","level":1,"title":"9.1   Graph","text":"

A graph is a nonlinear data structure consisting of vertices and edges. We can abstractly represent a graph \\(G\\) as a set of vertices \\(V\\) and a set of edges \\(E\\). The following example shows a graph containing 5 vertices and 7 edges.

\\[ \\begin{aligned} V & = \\{ 1, 2, 3, 4, 5 \\} \\newline E & = \\{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \\} \\newline G & = \\{ V, E \\} \\newline \\end{aligned} \\]

If we view vertices as nodes and edges as references (pointers) connecting the nodes, we can see graphs as a data structure extended from linked lists. As shown in Figure 9-1, compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex.

Figure 9-1   Relationships among linked lists, trees, and graphs

","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#911-common-types-and-terminology-of-graphs","level":2,"title":"9.1.1   Common Types and Terminology of Graphs","text":"

Graphs can be divided into undirected graphs and directed graphs based on whether edges have direction, as shown in Figure 9-2.

  • In undirected graphs, edges represent a \"bidirectional\" connection between two vertices, such as the \"friend relationship\" on WeChat or QQ.
  • In directed graphs, edges have directionality, meaning edges \\(A \\rightarrow B\\) and \\(A \\leftarrow B\\) are independent of each other, such as the \"follow\" and \"be followed\" relationships on Weibo or TikTok.

Figure 9-2   Directed and undirected graphs

Graphs can be divided into connected graphs and disconnected graphs based on whether all vertices are connected, as shown in Figure 9-3.

  • For connected graphs, starting from any vertex, all other vertices can be reached.
  • For disconnected graphs, starting from a certain vertex, at least one vertex cannot be reached.

Figure 9-3   Connected and disconnected graphs

We can also add a \"weight\" variable to edges, resulting in weighted graphs as shown in Figure 9-4. For example, in mobile games like \"Honor of Kings\", the system calculates the \"intimacy\" between players based on their shared game time, and such intimacy networks can be represented using weighted graphs.

Figure 9-4   Weighted and unweighted graphs

Graph data structures include the following commonly used terms.

  • Adjacency: When two vertices are connected by an edge, these two vertices are said to be \"adjacent\". In Figure 9-4, the adjacent vertices of vertex 1 are vertices 2, 3, and 5.
  • Path: The sequence of edges from vertex A to vertex B is called a \"path\" from A to B. In Figure 9-4, the edge sequence 1-5-2-4 is a path from vertex 1 to vertex 4.
  • Degree: The number of edges a vertex has. For directed graphs, in-degree indicates how many edges point to the vertex, and out-degree indicates how many edges point out from the vertex.
","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#912-representation-of-graphs","level":2,"title":"9.1.2   Representation of Graphs","text":"

Common representations of graphs include \"adjacency matrices\" and \"adjacency lists\". The following uses undirected graphs as examples.

","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#1-adjacency-matrix","level":3,"title":"1.   Adjacency Matrix","text":"

Given a graph with \\(n\\) vertices, an adjacency matrix uses an \\(n \\times n\\) matrix to represent the graph, where each row (column) represents a vertex, and matrix elements represent edges, using \\(1\\) or \\(0\\) to indicate whether an edge exists between two vertices.

As shown in Figure 9-5, let the adjacency matrix be \\(M\\) and the vertex list be \\(V\\). Then matrix element \\(M[i, j] = 1\\) indicates that an edge exists between vertex \\(V[i]\\) and vertex \\(V[j]\\), whereas \\(M[i, j] = 0\\) indicates no edge between the two vertices.

Figure 9-5   Adjacency matrix representation of a graph

Adjacency matrices have the following properties.

  • In simple graphs, vertices cannot connect to themselves, so the elements on the main diagonal of the adjacency matrix are meaningless.
  • For undirected graphs, edges in both directions are equivalent, so the adjacency matrix is symmetric about the main diagonal.
  • Replacing the elements of the adjacency matrix from \\(1\\) and \\(0\\) to weights allows representation of weighted graphs.

When using adjacency matrices to represent graphs, we can directly access matrix elements to obtain edges, resulting in highly efficient addition, deletion, lookup, and modification operations, all with a time complexity of \\(O(1)\\). However, the space complexity of the matrix is \\(O(n^2)\\), which consumes significant memory.

","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#2-adjacency-list","level":3,"title":"2.   Adjacency List","text":"

An adjacency list uses \\(n\\) linked lists to represent a graph, with linked list nodes representing vertices. The \\(i\\)-th linked list corresponds to vertex \\(i\\) and stores all adjacent vertices of that vertex (vertices connected to that vertex). Figure 9-6 shows an example of a graph stored using an adjacency list.

Figure 9-6   Adjacency list representation of a graph

Adjacency lists only store edges that actually exist, and the total number of edges is typically much less than \\(n^2\\), making them more space-efficient. However, finding edges in an adjacency list requires traversing the linked list, so its time efficiency is inferior to that of adjacency matrices.

Observing Figure 9-6, the structure of adjacency lists is very similar to \"chaining\" in hash tables, so we can adopt similar methods to optimize efficiency. For example, when linked lists are long, they can be converted to AVL trees or red-black trees, thereby optimizing time efficiency from \\(O(n)\\) to \\(O(\\log n)\\); linked lists can also be converted to hash tables, thereby reducing time complexity to \\(O(1)\\).

","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#913-common-applications-of-graphs","level":2,"title":"9.1.3   Common Applications of Graphs","text":"

As shown in Table 9-1, many real-world systems can be modeled using graphs, and corresponding problems can be reduced to graph computation problems.

Table 9-1   Common graphs in real life

Vertices Edges Graph Computation Problem Social network Users Friend relationships Potential friend recommendation Subway lines Stations Connectivity between stations Shortest route recommendation Solar system Celestial bodies Gravitational forces between celestial bodies Planetary orbit calculation","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   Basic Operations on Graphs","text":"

Basic operations on graphs can be divided into operations on \"edges\" and operations on \"vertices\". Under the two representation methods of \"adjacency matrix\" and \"adjacency list\", the implementation methods differ.

","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#921-implementation-based-on-adjacency-matrix","level":2,"title":"9.2.1   Implementation Based on Adjacency Matrix","text":"

Given an undirected graph with \\(n\\) vertices, the various operations are implemented as shown in Figure 9-7.

  • Adding or removing an edge: Directly modify the specified edge in the adjacency matrix, using \\(O(1)\\) time. Since it is an undirected graph, both directions of the edge need to be updated simultaneously.
  • Adding a vertex: Add a row and a column at the end of the adjacency matrix and fill them all with \\(0\\)s, using \\(O(n)\\) time.
  • Removing a vertex: Delete a row and a column in the adjacency matrix. The worst case occurs when removing the first row and column, requiring \\((n-1)^2\\) elements to be \"moved up and to the left\", thus using \\(O(n^2)\\) time.
  • Initialization: Pass in \\(n\\) vertices, initialize a vertex list vertices of length \\(n\\), using \\(O(n)\\) time; initialize an adjacency matrix adjMat of size \\(n \\times n\\), using \\(O(n^2)\\) time.
Initialize adjacency matrixAdd an edgeRemove an edgeAdd a vertexRemove a vertex

Figure 9-7   Initialization, adding and removing edges, adding and removing vertices in adjacency matrix

The following is the implementation code for graphs represented using an adjacency matrix:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_matrix.py
class GraphAdjMat:\n    \"\"\"Undirected graph class based on adjacency matrix\"\"\"\n\n    def __init__(self, vertices: list[int], edges: list[list[int]]):\n        \"\"\"Constructor\"\"\"\n        # Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n        self.vertices: list[int] = []\n        # Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n        self.adj_mat: list[list[int]] = []\n        # Add vertices\n        for val in vertices:\n            self.add_vertex(val)\n        # Add edges\n        # Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for e in edges:\n            self.add_edge(e[0], e[1])\n\n    def size(self) -> int:\n        \"\"\"Get the number of vertices\"\"\"\n        return len(self.vertices)\n\n    def add_vertex(self, val: int):\n        \"\"\"Add vertex\"\"\"\n        n = self.size()\n        # Add the value of the new vertex to the vertex list\n        self.vertices.append(val)\n        # Add a row to the adjacency matrix\n        new_row = [0] * n\n        self.adj_mat.append(new_row)\n        # Add a column to the adjacency matrix\n        for row in self.adj_mat:\n            row.append(0)\n\n    def remove_vertex(self, index: int):\n        \"\"\"Remove vertex\"\"\"\n        if index >= self.size():\n            raise IndexError()\n        # Remove the vertex at index from the vertex list\n        self.vertices.pop(index)\n        # Remove the row at index from the adjacency matrix\n        self.adj_mat.pop(index)\n        # Remove the column at index from the adjacency matrix\n        for row in self.adj_mat:\n            row.pop(index)\n\n    def add_edge(self, i: int, j: int):\n        \"\"\"Add edge\"\"\"\n        # Parameters i, j correspond to the vertices element indices\n        # Handle index out of bounds and equality\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        # In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        self.adj_mat[i][j] = 1\n        self.adj_mat[j][i] = 1\n\n    def remove_edge(self, i: int, j: int):\n        \"\"\"Remove edge\"\"\"\n        # Parameters i, j correspond to the vertices element indices\n        # Handle index out of bounds and equality\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        self.adj_mat[i][j] = 0\n        self.adj_mat[j][i] = 0\n\n    def print(self):\n        \"\"\"Print adjacency matrix\"\"\"\n        print(\"Vertex list =\", self.vertices)\n        print(\"Adjacency matrix =\")\n        print_matrix(self.adj_mat)\n
graph_adjacency_matrix.cpp
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vector<int> vertices;       // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    vector<vector<int>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n  public:\n    /* Constructor */\n    GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {\n        // Add vertex\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const vector<int> &edge : edges) {\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int size() const {\n        return vertices.size();\n    }\n\n    /* Add vertex */\n    void addVertex(int val) {\n        int n = size();\n        // Add the value of the new vertex to the vertex list\n        vertices.push_back(val);\n        // Add a row to the adjacency matrix\n        adjMat.emplace_back(vector<int>(n, 0));\n        // Add a column to the adjacency matrix\n        for (vector<int> &row : adjMat) {\n            row.push_back(0);\n        }\n    }\n\n    /* Remove vertex */\n    void removeVertex(int index) {\n        if (index >= size()) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        // Remove the vertex at index from the vertex list\n        vertices.erase(vertices.begin() + index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.erase(adjMat.begin() + index);\n        // Remove the column at index from the adjacency matrix\n        for (vector<int> &row : adjMat) {\n            row.erase(row.begin() + index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    void addEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    void removeEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    void print() {\n        cout << \"Vertex list = \";\n        printVector(vertices);\n        cout << \"Adjacency matrix =\" << endl;\n        printVectorMatrix(adjMat);\n    }\n};\n
graph_adjacency_matrix.java
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    List<Integer> vertices; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    List<List<Integer>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = new ArrayList<>();\n        this.adjMat = new ArrayList<>();\n        // Add vertex\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (int[] e : edges) {\n            addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    public int size() {\n        return vertices.size();\n    }\n\n    /* Add vertex */\n    public void addVertex(int val) {\n        int n = size();\n        // Add the value of the new vertex to the vertex list\n        vertices.add(val);\n        // Add a row to the adjacency matrix\n        List<Integer> newRow = new ArrayList<>(n);\n        for (int j = 0; j < n; j++) {\n            newRow.add(0);\n        }\n        adjMat.add(newRow);\n        // Add a column to the adjacency matrix\n        for (List<Integer> row : adjMat) {\n            row.add(0);\n        }\n    }\n\n    /* Remove vertex */\n    public void removeVertex(int index) {\n        if (index >= size())\n            throw new IndexOutOfBoundsException();\n        // Remove the vertex at index from the vertex list\n        vertices.remove(index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.remove(index);\n        // Remove the column at index from the adjacency matrix\n        for (List<Integer> row : adjMat) {\n            row.remove(index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void addEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat.get(i).set(j, 1);\n        adjMat.get(j).set(i, 1);\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void removeEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        adjMat.get(i).set(j, 0);\n        adjMat.get(j).set(i, 0);\n    }\n\n    /* Print adjacency matrix */\n    public void print() {\n        System.out.print(\"Vertex list = \");\n        System.out.println(vertices);\n        System.out.println(\"Adjacency matrix =\");\n        PrintUtil.printMatrix(adjMat);\n    }\n}\n
graph_adjacency_matrix.cs
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    List<int> vertices;     // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    List<List<int>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        foreach (int val in vertices) {\n            AddVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        foreach (int[] e in edges) {\n            AddEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int Size() {\n        return vertices.Count;\n    }\n\n    /* Add vertex */\n    public void AddVertex(int val) {\n        int n = Size();\n        // Add the value of the new vertex to the vertex list\n        vertices.Add(val);\n        // Add a row to the adjacency matrix\n        List<int> newRow = new(n);\n        for (int j = 0; j < n; j++) {\n            newRow.Add(0);\n        }\n        adjMat.Add(newRow);\n        // Add a column to the adjacency matrix\n        foreach (List<int> row in adjMat) {\n            row.Add(0);\n        }\n    }\n\n    /* Remove vertex */\n    public void RemoveVertex(int index) {\n        if (index >= Size())\n            throw new IndexOutOfRangeException();\n        // Remove the vertex at index from the vertex list\n        vertices.RemoveAt(index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.RemoveAt(index);\n        // Remove the column at index from the adjacency matrix\n        foreach (List<int> row in adjMat) {\n            row.RemoveAt(index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void AddEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void RemoveEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    public void Print() {\n        Console.Write(\"Vertex list = \");\n        PrintUtil.PrintList(vertices);\n        Console.WriteLine(\"Adjacency matrix =\");\n        PrintUtil.PrintMatrix(adjMat);\n    }\n}\n
graph_adjacency_matrix.go
/* Undirected graph class based on adjacency matrix */\ntype graphAdjMat struct {\n    // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    vertices []int\n    // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    adjMat [][]int\n}\n\n/* Constructor */\nfunc newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat {\n    // Add vertex\n    n := len(vertices)\n    adjMat := make([][]int, n)\n    for i := range adjMat {\n        adjMat[i] = make([]int, n)\n    }\n    // Initialize graph\n    g := &graphAdjMat{\n        vertices: vertices,\n        adjMat:   adjMat,\n    }\n    // Add edge\n    // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    for i := range edges {\n        g.addEdge(edges[i][0], edges[i][1])\n    }\n    return g\n}\n\n/* Get the number of vertices */\nfunc (g *graphAdjMat) size() int {\n    return len(g.vertices)\n}\n\n/* Add vertex */\nfunc (g *graphAdjMat) addVertex(val int) {\n    n := g.size()\n    // Add the value of the new vertex to the vertex list\n    g.vertices = append(g.vertices, val)\n    // Add a row to the adjacency matrix\n    newRow := make([]int, n)\n    g.adjMat = append(g.adjMat, newRow)\n    // Add a column to the adjacency matrix\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i], 0)\n    }\n}\n\n/* Remove vertex */\nfunc (g *graphAdjMat) removeVertex(index int) {\n    if index >= g.size() {\n        return\n    }\n    // Remove the vertex at index from the vertex list\n    g.vertices = append(g.vertices[:index], g.vertices[index+1:]...)\n    // Remove the row at index from the adjacency matrix\n    g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...)\n    // Remove the column at index from the adjacency matrix\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...)\n    }\n}\n\n/* Add edge */\n// Parameters i, j correspond to the vertices element indices\nfunc (g *graphAdjMat) addEdge(i, j int) {\n    // Handle index out of bounds and equality\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    g.adjMat[i][j] = 1\n    g.adjMat[j][i] = 1\n}\n\n/* Remove edge */\n// Parameters i, j correspond to the vertices element indices\nfunc (g *graphAdjMat) removeEdge(i, j int) {\n    // Handle index out of bounds and equality\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    g.adjMat[i][j] = 0\n    g.adjMat[j][i] = 0\n}\n\n/* Print adjacency matrix */\nfunc (g *graphAdjMat) print() {\n    fmt.Printf(\"\\tVertex list = %v\\n\", g.vertices)\n    fmt.Printf(\"\\tAdjacency matrix = \\n\")\n    for i := range g.adjMat {\n        fmt.Printf(\"\\t\\t\\t%v\\n\", g.adjMat[i])\n    }\n}\n
graph_adjacency_matrix.swift
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    private var vertices: [Int] // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    private var adjMat: [[Int]] // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    init(vertices: [Int], edges: [[Int]]) {\n        self.vertices = []\n        adjMat = []\n        // Add vertex\n        for val in vertices {\n            addVertex(val: val)\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for e in edges {\n            addEdge(i: e[0], j: e[1])\n        }\n    }\n\n    /* Get the number of vertices */\n    func size() -> Int {\n        vertices.count\n    }\n\n    /* Add vertex */\n    func addVertex(val: Int) {\n        let n = size()\n        // Add the value of the new vertex to the vertex list\n        vertices.append(val)\n        // Add a row to the adjacency matrix\n        let newRow = Array(repeating: 0, count: n)\n        adjMat.append(newRow)\n        // Add a column to the adjacency matrix\n        for i in adjMat.indices {\n            adjMat[i].append(0)\n        }\n    }\n\n    /* Remove vertex */\n    func removeVertex(index: Int) {\n        if index >= size() {\n            fatalError(\"Out of bounds\")\n        }\n        // Remove the vertex at index from the vertex list\n        vertices.remove(at: index)\n        // Remove the row at index from the adjacency matrix\n        adjMat.remove(at: index)\n        // Remove the column at index from the adjacency matrix\n        for i in adjMat.indices {\n            adjMat[i].remove(at: index)\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    func addEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Out of bounds\")\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    func removeEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Out of bounds\")\n        }\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* Print adjacency matrix */\n    func print() {\n        Swift.print(\"Vertex list = \", terminator: \"\")\n        Swift.print(vertices)\n        Swift.print(\"Adjacency matrix =\")\n        PrintUtil.printMatrix(matrix: adjMat)\n    }\n}\n
graph_adjacency_matrix.js
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vertices; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    constructor(vertices, edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size() {\n        return this.vertices.length;\n    }\n\n    /* Add vertex */\n    addVertex(val) {\n        const n = this.size();\n        // Add the value of the new vertex to the vertex list\n        this.vertices.push(val);\n        // Add a row to the adjacency matrix\n        const newRow = [];\n        for (let j = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Add a column to the adjacency matrix\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    removeVertex(index) {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Remove the vertex at index from the vertex list\n        this.vertices.splice(index, 1);\n\n        // Remove the row at index from the adjacency matrix\n        this.adjMat.splice(index, 1);\n        // Remove the column at index from the adjacency matrix\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    addEdge(i, j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // In undirected graph, adjacency matrix is symmetric about main diagonal, i.e., satisfies (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    removeEdge(i, j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    print() {\n        console.log('Vertex list = ', this.vertices);\n        console.log('Adjacency matrix =', this.adjMat);\n    }\n}\n
graph_adjacency_matrix.ts
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vertices: number[]; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    adjMat: number[][]; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    constructor(vertices: number[], edges: number[][]) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size(): number {\n        return this.vertices.length;\n    }\n\n    /* Add vertex */\n    addVertex(val: number): void {\n        const n: number = this.size();\n        // Add the value of the new vertex to the vertex list\n        this.vertices.push(val);\n        // Add a row to the adjacency matrix\n        const newRow: number[] = [];\n        for (let j: number = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Add a column to the adjacency matrix\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    removeVertex(index: number): void {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Remove the vertex at index from the vertex list\n        this.vertices.splice(index, 1);\n\n        // Remove the row at index from the adjacency matrix\n        this.adjMat.splice(index, 1);\n        // Remove the column at index from the adjacency matrix\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    addEdge(i: number, j: number): void {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // In undirected graph, adjacency matrix is symmetric about main diagonal, i.e., satisfies (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    removeEdge(i: number, j: number): void {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    print(): void {\n        console.log('Vertex list = ', this.vertices);\n        console.log('Adjacency matrix =', this.adjMat);\n    }\n}\n
graph_adjacency_matrix.dart
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n  List<int> vertices = []; // Vertex elements, elements represent \"vertex values\", indices represent \"vertex indices\"\n  List<List<int>> adjMat = []; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n  /* Constructor */\n  GraphAdjMat(List<int> vertices, List<List<int>> edges) {\n    this.vertices = [];\n    this.adjMat = [];\n    // Add vertex\n    for (int val in vertices) {\n      addVertex(val);\n    }\n    // Add edge\n    // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    for (List<int> e in edges) {\n      addEdge(e[0], e[1]);\n    }\n  }\n\n  /* Get the number of vertices */\n  int size() {\n    return vertices.length;\n  }\n\n  /* Add vertex */\n  void addVertex(int val) {\n    int n = size();\n    // Add the value of the new vertex to the vertex list\n    vertices.add(val);\n    // Add a row to the adjacency matrix\n    List<int> newRow = List.filled(n, 0, growable: true);\n    adjMat.add(newRow);\n    // Add a column to the adjacency matrix\n    for (List<int> row in adjMat) {\n      row.add(0);\n    }\n  }\n\n  /* Remove vertex */\n  void removeVertex(int index) {\n    if (index >= size()) {\n      throw IndexError;\n    }\n    // Remove the vertex at index from the vertex list\n    vertices.removeAt(index);\n    // Remove the row at index from the adjacency matrix\n    adjMat.removeAt(index);\n    // Remove the column at index from the adjacency matrix\n    for (List<int> row in adjMat) {\n      row.removeAt(index);\n    }\n  }\n\n  /* Add edge */\n  // Parameters i, j correspond to the vertices element indices\n  void addEdge(int i, int j) {\n    // Handle index out of bounds and equality\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    adjMat[i][j] = 1;\n    adjMat[j][i] = 1;\n  }\n\n  /* Remove edge */\n  // Parameters i, j correspond to the vertices element indices\n  void removeEdge(int i, int j) {\n    // Handle index out of bounds and equality\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    adjMat[i][j] = 0;\n    adjMat[j][i] = 0;\n  }\n\n  /* Print adjacency matrix */\n  void printAdjMat() {\n    print(\"Vertex list = $vertices\");\n    print(\"Adjacency matrix = \");\n    printMatrix(adjMat);\n  }\n}\n
graph_adjacency_matrix.rs
/* Undirected graph type based on adjacency matrix */\npub struct GraphAdjMat {\n    // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    pub vertices: Vec<i32>,\n    // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    pub adj_mat: Vec<Vec<i32>>,\n}\n\nimpl GraphAdjMat {\n    /* Constructor */\n    pub fn new(vertices: Vec<i32>, edges: Vec<[usize; 2]>) -> Self {\n        let mut graph = GraphAdjMat {\n            vertices: vec![],\n            adj_mat: vec![],\n        };\n        // Add vertex\n        for val in vertices {\n            graph.add_vertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for edge in edges {\n            graph.add_edge(edge[0], edge[1])\n        }\n\n        graph\n    }\n\n    /* Get the number of vertices */\n    pub fn size(&self) -> usize {\n        self.vertices.len()\n    }\n\n    /* Add vertex */\n    pub fn add_vertex(&mut self, val: i32) {\n        let n = self.size();\n        // Add the value of the new vertex to the vertex list\n        self.vertices.push(val);\n        // Add a row to the adjacency matrix\n        self.adj_mat.push(vec![0; n]);\n        // Add a column to the adjacency matrix\n        for row in self.adj_mat.iter_mut() {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    pub fn remove_vertex(&mut self, index: usize) {\n        if index >= self.size() {\n            panic!(\"index error\")\n        }\n        // Remove the vertex at index from the vertex list\n        self.vertices.remove(index);\n        // Remove the row at index from the adjacency matrix\n        self.adj_mat.remove(index);\n        // Remove the column at index from the adjacency matrix\n        for row in self.adj_mat.iter_mut() {\n            row.remove(index);\n        }\n    }\n\n    /* Add edge */\n    pub fn add_edge(&mut self, i: usize, j: usize) {\n        // Parameters i, j correspond to the vertices element indices\n        // Handle index out of bounds and equality\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        self.adj_mat[i][j] = 1;\n        self.adj_mat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    pub fn remove_edge(&mut self, i: usize, j: usize) {\n        // Parameters i, j correspond to the vertices element indices\n        // Handle index out of bounds and equality\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        self.adj_mat[i][j] = 0;\n        self.adj_mat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    pub fn print(&self) {\n        println!(\"Vertex list = {:?}\", self.vertices);\n        println!(\"Adjacency matrix =\");\n        println!(\"[\");\n        for row in &self.adj_mat {\n            println!(\"  {:?},\", row);\n        }\n        println!(\"]\")\n    }\n}\n
graph_adjacency_matrix.c
/* Undirected graph structure based on adjacency matrix */\ntypedef struct {\n    int vertices[MAX_SIZE];\n    int adjMat[MAX_SIZE][MAX_SIZE];\n    int size;\n} GraphAdjMat;\n\n/* Constructor */\nGraphAdjMat *newGraphAdjMat() {\n    GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat));\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        for (int j = 0; j < MAX_SIZE; j++) {\n            graph->adjMat[i][j] = 0;\n        }\n    }\n    return graph;\n}\n\n/* Destructor */\nvoid delGraphAdjMat(GraphAdjMat *graph) {\n    free(graph);\n}\n\n/* Add vertex */\nvoid addVertex(GraphAdjMat *graph, int val) {\n    if (graph->size == MAX_SIZE) {\n        fprintf(stderr, \"Graph vertex count has reached maximum\\n\");\n        return;\n    }\n    // Add nth vertex and zero nth row and column\n    int n = graph->size;\n    graph->vertices[n] = val;\n    for (int i = 0; i <= n; i++) {\n        graph->adjMat[n][i] = graph->adjMat[i][n] = 0;\n    }\n    graph->size++;\n}\n\n/* Remove vertex */\nvoid removeVertex(GraphAdjMat *graph, int index) {\n    if (index < 0 || index >= graph->size) {\n        fprintf(stderr, \"Vertex index out of bounds\\n\");\n        return;\n    }\n    // Remove the vertex at index from the vertex list\n    for (int i = index; i < graph->size - 1; i++) {\n        graph->vertices[i] = graph->vertices[i + 1];\n    }\n    // Remove the row at index from the adjacency matrix\n    for (int i = index; i < graph->size - 1; i++) {\n        for (int j = 0; j < graph->size; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i + 1][j];\n        }\n    }\n    // Remove the column at index from the adjacency matrix\n    for (int i = 0; i < graph->size; i++) {\n        for (int j = index; j < graph->size - 1; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i][j + 1];\n        }\n    }\n    graph->size--;\n}\n\n/* Add edge */\n// Parameters i, j correspond to the vertices element indices\nvoid addEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"Edge index out of bounds or equal\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 1;\n    graph->adjMat[j][i] = 1;\n}\n\n/* Remove edge */\n// Parameters i, j correspond to the vertices element indices\nvoid removeEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"Edge index out of bounds or equal\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 0;\n    graph->adjMat[j][i] = 0;\n}\n\n/* Print adjacency matrix */\nvoid printGraphAdjMat(GraphAdjMat *graph) {\n    printf(\"Vertex list = \");\n    printArray(graph->vertices, graph->size);\n    printf(\"Adjacency matrix =\\n\");\n    for (int i = 0; i < graph->size; i++) {\n        printArray(graph->adjMat[i], graph->size);\n    }\n}\n
graph_adjacency_matrix.kt
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat(vertices: IntArray, edges: Array<IntArray>) {\n    val vertices = mutableListOf<Int>() // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    val adjMat = mutableListOf<MutableList<Int>>() // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    init {\n        // Add vertex\n        for (vertex in vertices) {\n            addVertex(vertex)\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (edge in edges) {\n            addEdge(edge[0], edge[1])\n        }\n    }\n\n    /* Get the number of vertices */\n    fun size(): Int {\n        return vertices.size\n    }\n\n    /* Add vertex */\n    fun addVertex(_val: Int) {\n        val n = size()\n        // Add the value of the new vertex to the vertex list\n        vertices.add(_val)\n        // Add a row to the adjacency matrix\n        val newRow = mutableListOf<Int>()\n        for (j in 0..<n) {\n            newRow.add(0)\n        }\n        adjMat.add(newRow)\n        // Add a column to the adjacency matrix\n        for (row in adjMat) {\n            row.add(0)\n        }\n    }\n\n    /* Remove vertex */\n    fun removeVertex(index: Int) {\n        if (index >= size())\n            throw IndexOutOfBoundsException()\n        // Remove the vertex at index from the vertex list\n        vertices.removeAt(index)\n        // Remove the row at index from the adjacency matrix\n        adjMat.removeAt(index)\n        // Remove the column at index from the adjacency matrix\n        for (row in adjMat) {\n            row.removeAt(index)\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    fun addEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    fun removeEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* Print adjacency matrix */\n    fun print() {\n        print(\"Vertex list = \")\n        println(vertices)\n        println(\"Adjacency matrix =\")\n        printMatrix(adjMat)\n    }\n}\n
graph_adjacency_matrix.rb
### Undirected graph class based on adjacency matrix ###\nclass GraphAdjMat\n  def initialize(vertices, edges)\n    ### Constructor ###\n    # Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    @vertices = []\n    # Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    @adj_mat = []\n    # Add vertex\n    vertices.each { |val| add_vertex(val) }\n    # Add edge\n    # Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    edges.each { |e| add_edge(e[0], e[1]) }\n  end\n\n  ### Get number of vertices ###\n  def size\n    @vertices.length\n  end\n\n  ### Add vertex ###\n  def add_vertex(val)\n    n = size\n    # Add the value of the new vertex to the vertex list\n    @vertices << val\n    # Add a row to the adjacency matrix\n    new_row = Array.new(n, 0)\n    @adj_mat << new_row\n    # Add a column to the adjacency matrix\n    @adj_mat.each { |row| row << 0 }\n  end\n\n  ### Delete vertex ###\n  def remove_vertex(index)\n    raise IndexError if index >= size\n\n    # Remove the vertex at index from the vertex list\n    @vertices.delete_at(index)\n    # Remove the row at index from the adjacency matrix\n    @adj_mat.delete_at(index)\n    # Remove the column at index from the adjacency matrix\n    @adj_mat.each { |row| row.delete_at(index) }\n  end\n\n  ### Add edge ###\n  def add_edge(i, j)\n    # Parameters i, j correspond to the vertices element indices\n    # Handle index out of bounds and equality\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    # In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    @adj_mat[i][j] = 1\n    @adj_mat[j][i] = 1\n  end\n\n  ### Delete edge ###\n  def remove_edge(i, j)\n    # Parameters i, j correspond to the vertices element indices\n    # Handle index out of bounds and equality\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    @adj_mat[i][j] = 0\n    @adj_mat[j][i] = 0\n  end\n\n  ### Print adjacency matrix ###\n  def __print__\n    puts \"Vertex list = #{@vertices}\"\n    puts 'Adjacency matrix ='\n    print_matrix(@adj_mat)\n  end\nend\n
","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#922-implementation-based-on-adjacency-list","level":2,"title":"9.2.2   Implementation Based on Adjacency List","text":"

Given an undirected graph with a total of \\(n\\) vertices and \\(m\\) edges, the various operations can be implemented as shown in Figure 9-8.

  • Adding an edge: Add the edge at the end of the corresponding vertex's linked list, using \\(O(1)\\) time. Since it is an undirected graph, edges in both directions need to be added simultaneously.
  • Removing an edge: Find and remove the specified edge in the corresponding vertex's linked list, using \\(O(m)\\) time. In an undirected graph, edges in both directions need to be removed simultaneously.
  • Adding a vertex: Add a linked list in the adjacency list and set the new vertex as the head node of the list, using \\(O(1)\\) time.
  • Removing a vertex: Traverse the entire adjacency list and remove all edges containing the specified vertex, using \\(O(n + m)\\) time.
  • Initialization: Create \\(n\\) vertices and \\(2m\\) edges in the adjacency list, using \\(O(n + m)\\) time.
Initialize adjacency listAdd an edgeRemove an edgeAdd a vertexRemove a vertex

Figure 9-8   Initialization, adding and removing edges, adding and removing vertices in adjacency list

The following is the adjacency list code implementation. Compared to Figure 9-8, the actual code has the following differences.

  • For convenience in adding and removing vertices, and to simplify the code, we use lists (dynamic arrays) instead of linked lists.
  • A hash table is used to store the adjacency list, where key is the vertex instance and value is the list (linked list) of adjacent vertices for that vertex.

Additionally, we use the Vertex class to represent vertices in the adjacency list. The reason for this is: if we used list indices to distinguish different vertices as with adjacency matrices, then to delete the vertex at index \\(i\\), we would need to traverse the entire adjacency list and decrement all indices greater than \\(i\\) by \\(1\\), which is very inefficient. However, if each vertex is a unique Vertex instance, deleting a vertex does not require modifying other vertices.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_list.py
class GraphAdjList:\n    \"\"\"Undirected graph class based on adjacency list\"\"\"\n\n    def __init__(self, edges: list[list[Vertex]]):\n        \"\"\"Constructor\"\"\"\n        # Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n        self.adj_list = dict[Vertex, list[Vertex]]()\n        # Add all vertices and edges\n        for edge in edges:\n            self.add_vertex(edge[0])\n            self.add_vertex(edge[1])\n            self.add_edge(edge[0], edge[1])\n\n    def size(self) -> int:\n        \"\"\"Get the number of vertices\"\"\"\n        return len(self.adj_list)\n\n    def add_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"Add edge\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Add edge vet1 - vet2\n        self.adj_list[vet1].append(vet2)\n        self.adj_list[vet2].append(vet1)\n\n    def remove_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"Remove edge\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Remove edge vet1 - vet2\n        self.adj_list[vet1].remove(vet2)\n        self.adj_list[vet2].remove(vet1)\n\n    def add_vertex(self, vet: Vertex):\n        \"\"\"Add vertex\"\"\"\n        if vet in self.adj_list:\n            return\n        # Add a new linked list in the adjacency list\n        self.adj_list[vet] = []\n\n    def remove_vertex(self, vet: Vertex):\n        \"\"\"Remove vertex\"\"\"\n        if vet not in self.adj_list:\n            raise ValueError()\n        # Remove the linked list corresponding to vertex vet in the adjacency list\n        self.adj_list.pop(vet)\n        # Traverse the linked lists of other vertices and remove all edges containing vet\n        for vertex in self.adj_list:\n            if vet in self.adj_list[vertex]:\n                self.adj_list[vertex].remove(vet)\n\n    def print(self):\n        \"\"\"Print adjacency list\"\"\"\n        print(\"Adjacency list =\")\n        for vertex in self.adj_list:\n            tmp = [v.val for v in self.adj_list[vertex]]\n            print(f\"{vertex.val}: {tmp},\")\n
graph_adjacency_list.cpp
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n  public:\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    unordered_map<Vertex *, vector<Vertex *>> adjList;\n\n    /* Remove specified node from vector */\n    void remove(vector<Vertex *> &vec, Vertex *vet) {\n        for (int i = 0; i < vec.size(); i++) {\n            if (vec[i] == vet) {\n                vec.erase(vec.begin() + i);\n                break;\n            }\n        }\n    }\n\n    /* Constructor */\n    GraphAdjList(const vector<vector<Vertex *>> &edges) {\n        // Add all vertices and edges\n        for (const vector<Vertex *> &edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int size() {\n        return adjList.size();\n    }\n\n    /* Add edge */\n    void addEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"Vertex does not exist\");\n        // Add edge vet1 - vet2\n        adjList[vet1].push_back(vet2);\n        adjList[vet2].push_back(vet1);\n    }\n\n    /* Remove edge */\n    void removeEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"Vertex does not exist\");\n        // Remove edge vet1 - vet2\n        remove(adjList[vet1], vet2);\n        remove(adjList[vet2], vet1);\n    }\n\n    /* Add vertex */\n    void addVertex(Vertex *vet) {\n        if (adjList.count(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList[vet] = vector<Vertex *>();\n    }\n\n    /* Remove vertex */\n    void removeVertex(Vertex *vet) {\n        if (!adjList.count(vet))\n            throw invalid_argument(\"Vertex does not exist\");\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.erase(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (auto &adj : adjList) {\n            remove(adj.second, vet);\n        }\n    }\n\n    /* Print adjacency list */\n    void print() {\n        cout << \"Adjacency list =\" << endl;\n        for (auto &adj : adjList) {\n            const auto &key = adj.first;\n            const auto &vec = adj.second;\n            cout << key->val << \": \";\n            printVector(vetsToVals(vec));\n        }\n    }\n};\n
graph_adjacency_list.java
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    Map<Vertex, List<Vertex>> adjList;\n\n    /* Constructor */\n    public GraphAdjList(Vertex[][] edges) {\n        this.adjList = new HashMap<>();\n        // Add all vertices and edges\n        for (Vertex[] edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    public int size() {\n        return adjList.size();\n    }\n\n    /* Add edge */\n    public void addEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Add edge vet1 - vet2\n        adjList.get(vet1).add(vet2);\n        adjList.get(vet2).add(vet1);\n    }\n\n    /* Remove edge */\n    public void removeEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Remove edge vet1 - vet2\n        adjList.get(vet1).remove(vet2);\n        adjList.get(vet2).remove(vet1);\n    }\n\n    /* Add vertex */\n    public void addVertex(Vertex vet) {\n        if (adjList.containsKey(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList.put(vet, new ArrayList<>());\n    }\n\n    /* Remove vertex */\n    public void removeVertex(Vertex vet) {\n        if (!adjList.containsKey(vet))\n            throw new IllegalArgumentException();\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.remove(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (List<Vertex> list : adjList.values()) {\n            list.remove(vet);\n        }\n    }\n\n    /* Print adjacency list */\n    public void print() {\n        System.out.println(\"Adjacency list =\");\n        for (Map.Entry<Vertex, List<Vertex>> pair : adjList.entrySet()) {\n            List<Integer> tmp = new ArrayList<>();\n            for (Vertex vertex : pair.getValue())\n                tmp.add(vertex.val);\n            System.out.println(pair.getKey().val + \": \" + tmp + \",\");\n        }\n    }\n}\n
graph_adjacency_list.cs
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    public Dictionary<Vertex, List<Vertex>> adjList;\n\n    /* Constructor */\n    public GraphAdjList(Vertex[][] edges) {\n        adjList = [];\n        // Add all vertices and edges\n        foreach (Vertex[] edge in edges) {\n            AddVertex(edge[0]);\n            AddVertex(edge[1]);\n            AddEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int Size() {\n        return adjList.Count;\n    }\n\n    /* Add edge */\n    public void AddEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Add edge vet1 - vet2\n        adjList[vet1].Add(vet2);\n        adjList[vet2].Add(vet1);\n    }\n\n    /* Remove edge */\n    public void RemoveEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Remove edge vet1 - vet2\n        adjList[vet1].Remove(vet2);\n        adjList[vet2].Remove(vet1);\n    }\n\n    /* Add vertex */\n    public void AddVertex(Vertex vet) {\n        if (adjList.ContainsKey(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList.Add(vet, []);\n    }\n\n    /* Remove vertex */\n    public void RemoveVertex(Vertex vet) {\n        if (!adjList.ContainsKey(vet))\n            throw new InvalidOperationException();\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.Remove(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        foreach (List<Vertex> list in adjList.Values) {\n            list.Remove(vet);\n        }\n    }\n\n    /* Print adjacency list */\n    public void Print() {\n        Console.WriteLine(\"Adjacency list =\");\n        foreach (KeyValuePair<Vertex, List<Vertex>> pair in adjList) {\n            List<int> tmp = [];\n            foreach (Vertex vertex in pair.Value)\n                tmp.Add(vertex.val);\n            Console.WriteLine(pair.Key.val + \": [\" + string.Join(\", \", tmp) + \"],\");\n        }\n    }\n}\n
graph_adjacency_list.go
/* Undirected graph class based on adjacency list */\ntype graphAdjList struct {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList map[Vertex][]Vertex\n}\n\n/* Constructor */\nfunc newGraphAdjList(edges [][]Vertex) *graphAdjList {\n    g := &graphAdjList{\n        adjList: make(map[Vertex][]Vertex),\n    }\n    // Add all vertices and edges\n    for _, edge := range edges {\n        g.addVertex(edge[0])\n        g.addVertex(edge[1])\n        g.addEdge(edge[0], edge[1])\n    }\n    return g\n}\n\n/* Get the number of vertices */\nfunc (g *graphAdjList) size() int {\n    return len(g.adjList)\n}\n\n/* Add edge */\nfunc (g *graphAdjList) addEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // Add edge vet1 - vet2, add anonymous struct{},\n    g.adjList[vet1] = append(g.adjList[vet1], vet2)\n    g.adjList[vet2] = append(g.adjList[vet2], vet1)\n}\n\n/* Remove edge */\nfunc (g *graphAdjList) removeEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // Remove edge vet1 - vet2\n    g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2)\n    g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1)\n}\n\n/* Add vertex */\nfunc (g *graphAdjList) addVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if ok {\n        return\n    }\n    // Add a new linked list in the adjacency list\n    g.adjList[vet] = make([]Vertex, 0)\n}\n\n/* Remove vertex */\nfunc (g *graphAdjList) removeVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if !ok {\n        panic(\"error\")\n    }\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    delete(g.adjList, vet)\n    // Traverse the linked lists of other vertices and remove all edges containing vet\n    for v, list := range g.adjList {\n        g.adjList[v] = DeleteSliceElms(list, vet)\n    }\n}\n\n/* Print adjacency list */\nfunc (g *graphAdjList) print() {\n    var builder strings.Builder\n    fmt.Printf(\"Adjacency list = \\n\")\n    for k, v := range g.adjList {\n        builder.WriteString(\"\\t\\t\" + strconv.Itoa(k.Val) + \": \")\n        for _, vet := range v {\n            builder.WriteString(strconv.Itoa(vet.Val) + \" \")\n        }\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
graph_adjacency_list.swift
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    public private(set) var adjList: [Vertex: [Vertex]]\n\n    /* Constructor */\n    public init(edges: [[Vertex]]) {\n        adjList = [:]\n        // Add all vertices and edges\n        for edge in edges {\n            addVertex(vet: edge[0])\n            addVertex(vet: edge[1])\n            addEdge(vet1: edge[0], vet2: edge[1])\n        }\n    }\n\n    /* Get the number of vertices */\n    public func size() -> Int {\n        adjList.count\n    }\n\n    /* Add edge */\n    public func addEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Invalid parameter\")\n        }\n        // Add edge vet1 - vet2\n        adjList[vet1]?.append(vet2)\n        adjList[vet2]?.append(vet1)\n    }\n\n    /* Remove edge */\n    public func removeEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Invalid parameter\")\n        }\n        // Remove edge vet1 - vet2\n        adjList[vet1]?.removeAll { $0 == vet2 }\n        adjList[vet2]?.removeAll { $0 == vet1 }\n    }\n\n    /* Add vertex */\n    public func addVertex(vet: Vertex) {\n        if adjList[vet] != nil {\n            return\n        }\n        // Add a new linked list in the adjacency list\n        adjList[vet] = []\n    }\n\n    /* Remove vertex */\n    public func removeVertex(vet: Vertex) {\n        if adjList[vet] == nil {\n            fatalError(\"Invalid parameter\")\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.removeValue(forKey: vet)\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for key in adjList.keys {\n            adjList[key]?.removeAll { $0 == vet }\n        }\n    }\n\n    /* Print adjacency list */\n    public func print() {\n        Swift.print(\"Adjacency list =\")\n        for (vertex, list) in adjList {\n            let list = list.map { $0.val }\n            Swift.print(\"\\(vertex.val): \\(list),\")\n        }\n    }\n}\n
graph_adjacency_list.js
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList;\n\n    /* Constructor */\n    constructor(edges) {\n        this.adjList = new Map();\n        // Add all vertices and edges\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size() {\n        return this.adjList.size;\n    }\n\n    /* Add edge */\n    addEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Add edge vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Remove edge */\n    removeEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove edge vet1 - vet2\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* Add vertex */\n    addVertex(vet) {\n        if (this.adjList.has(vet)) return;\n        // Add a new linked list in the adjacency list\n        this.adjList.set(vet, []);\n    }\n\n    /* Remove vertex */\n    removeVertex(vet) {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        this.adjList.delete(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (const set of this.adjList.values()) {\n            const index = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* Print adjacency list */\n    print() {\n        console.log('Adjacency list =');\n        for (const [key, value] of this.adjList) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
graph_adjacency_list.ts
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList: Map<Vertex, Vertex[]>;\n\n    /* Constructor */\n    constructor(edges: Vertex[][]) {\n        this.adjList = new Map();\n        // Add all vertices and edges\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size(): number {\n        return this.adjList.size;\n    }\n\n    /* Add edge */\n    addEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Add edge vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Remove edge */\n    removeEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove edge vet1 - vet2\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* Add vertex */\n    addVertex(vet: Vertex): void {\n        if (this.adjList.has(vet)) return;\n        // Add a new linked list in the adjacency list\n        this.adjList.set(vet, []);\n    }\n\n    /* Remove vertex */\n    removeVertex(vet: Vertex): void {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        this.adjList.delete(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (const set of this.adjList.values()) {\n            const index: number = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* Print adjacency list */\n    print(): void {\n        console.log('Adjacency list =');\n        for (const [key, value] of this.adjList.entries()) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
graph_adjacency_list.dart
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n  // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n  Map<Vertex, List<Vertex>> adjList = {};\n\n  /* Constructor */\n  GraphAdjList(List<List<Vertex>> edges) {\n    for (List<Vertex> edge in edges) {\n      addVertex(edge[0]);\n      addVertex(edge[1]);\n      addEdge(edge[0], edge[1]);\n    }\n  }\n\n  /* Get the number of vertices */\n  int size() {\n    return adjList.length;\n  }\n\n  /* Add edge */\n  void addEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Add edge vet1 - vet2\n    adjList[vet1]!.add(vet2);\n    adjList[vet2]!.add(vet1);\n  }\n\n  /* Remove edge */\n  void removeEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Remove edge vet1 - vet2\n    adjList[vet1]!.remove(vet2);\n    adjList[vet2]!.remove(vet1);\n  }\n\n  /* Add vertex */\n  void addVertex(Vertex vet) {\n    if (adjList.containsKey(vet)) return;\n    // Add a new linked list in the adjacency list\n    adjList[vet] = [];\n  }\n\n  /* Remove vertex */\n  void removeVertex(Vertex vet) {\n    if (!adjList.containsKey(vet)) {\n      throw ArgumentError;\n    }\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    adjList.remove(vet);\n    // Traverse the linked lists of other vertices and remove all edges containing vet\n    adjList.forEach((key, value) {\n      value.remove(vet);\n    });\n  }\n\n  /* Print adjacency list */\n  void printAdjList() {\n    print(\"Adjacency list =\");\n    adjList.forEach((key, value) {\n      List<int> tmp = [];\n      for (Vertex vertex in value) {\n        tmp.add(vertex.val);\n      }\n      print(\"${key.val}: $tmp,\");\n    });\n  }\n}\n
graph_adjacency_list.rs
/* Undirected graph type based on adjacency list */\npub struct GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    pub adj_list: HashMap<Vertex, Vec<Vertex>>, // maybe HashSet<Vertex> for value part is better?\n}\n\nimpl GraphAdjList {\n    /* Constructor */\n    pub fn new(edges: Vec<[Vertex; 2]>) -> Self {\n        let mut graph = GraphAdjList {\n            adj_list: HashMap::new(),\n        };\n        // Add all vertices and edges\n        for edge in edges {\n            graph.add_vertex(edge[0]);\n            graph.add_vertex(edge[1]);\n            graph.add_edge(edge[0], edge[1]);\n        }\n\n        graph\n    }\n\n    /* Get the number of vertices */\n    #[allow(unused)]\n    pub fn size(&self) -> usize {\n        self.adj_list.len()\n    }\n\n    /* Add edge */\n    pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Add edge vet1 - vet2\n        self.adj_list.entry(vet1).or_default().push(vet2);\n        self.adj_list.entry(vet2).or_default().push(vet1);\n    }\n\n    /* Remove edge */\n    #[allow(unused)]\n    pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Remove edge vet1 - vet2\n        self.adj_list\n            .entry(vet1)\n            .and_modify(|v| v.retain(|&e| e != vet2));\n        self.adj_list\n            .entry(vet2)\n            .and_modify(|v| v.retain(|&e| e != vet1));\n    }\n\n    /* Add vertex */\n    pub fn add_vertex(&mut self, vet: Vertex) {\n        if self.adj_list.contains_key(&vet) {\n            return;\n        }\n        // Add a new linked list in the adjacency list\n        self.adj_list.insert(vet, vec![]);\n    }\n\n    /* Remove vertex */\n    #[allow(unused)]\n    pub fn remove_vertex(&mut self, vet: Vertex) {\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        self.adj_list.remove(&vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for list in self.adj_list.values_mut() {\n            list.retain(|&v| v != vet);\n        }\n    }\n\n    /* Print adjacency list */\n    pub fn print(&self) {\n        println!(\"Adjacency list =\");\n        for (vertex, list) in &self.adj_list {\n            let list = list.iter().map(|vertex| vertex.val).collect::<Vec<i32>>();\n            println!(\"{}: {:?},\", vertex.val, list);\n        }\n    }\n}\n
graph_adjacency_list.c
/* Node structure */\ntypedef struct AdjListNode {\n    Vertex *vertex;           // Vertex\n    struct AdjListNode *next; // Successor node\n} AdjListNode;\n\n/* Find node corresponding to vertex */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* Add edge helper function */\nvoid addEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode));\n    node->vertex = vet;\n    // Head insertion\n    node->next = head->next;\n    head->next = node;\n}\n\n/* Remove edge helper function */\nvoid removeEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *pre = head;\n    AdjListNode *cur = head->next;\n    // Search for node corresponding to vet in list\n    while (cur != NULL && cur->vertex != vet) {\n        pre = cur;\n        cur = cur->next;\n    }\n    if (cur == NULL)\n        return;\n    // Remove node corresponding to vet from list\n    pre->next = cur->next;\n    // Free memory\n    free(cur);\n}\n\n/* Undirected graph class based on adjacency list */\ntypedef struct {\n    AdjListNode *heads[MAX_SIZE]; // Node array\n    int size;                     // Node count\n} GraphAdjList;\n\n/* Constructor */\nGraphAdjList *newGraphAdjList() {\n    GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList));\n    if (!graph) {\n        return NULL;\n    }\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        graph->heads[i] = NULL;\n    }\n    return graph;\n}\n\n/* Destructor */\nvoid delGraphAdjList(GraphAdjList *graph) {\n    for (int i = 0; i < graph->size; i++) {\n        AdjListNode *cur = graph->heads[i];\n        while (cur != NULL) {\n            AdjListNode *next = cur->next;\n            if (cur != graph->heads[i]) {\n                free(cur);\n            }\n            cur = next;\n        }\n        free(graph->heads[i]->vertex);\n        free(graph->heads[i]);\n    }\n    free(graph);\n}\n\n/* Find node corresponding to vertex */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* Add edge */\nvoid addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL && head1 != head2);\n    // Add edge vet1 - vet2\n    addEdgeHelper(head1, vet2);\n    addEdgeHelper(head2, vet1);\n}\n\n/* Remove edge */\nvoid removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL);\n    // Remove edge vet1 - vet2\n    removeEdgeHelper(head1, head2->vertex);\n    removeEdgeHelper(head2, head1->vertex);\n}\n\n/* Add vertex */\nvoid addVertex(GraphAdjList *graph, Vertex *vet) {\n    assert(graph != NULL && graph->size < MAX_SIZE);\n    AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode));\n    head->vertex = vet;\n    head->next = NULL;\n    // Add a new linked list in the adjacency list\n    graph->heads[graph->size++] = head;\n}\n\n/* Remove vertex */\nvoid removeVertex(GraphAdjList *graph, Vertex *vet) {\n    AdjListNode *node = findNode(graph, vet);\n    assert(node != NULL);\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    AdjListNode *cur = node, *pre = NULL;\n    while (cur) {\n        pre = cur;\n        cur = cur->next;\n        free(pre);\n    }\n    // Traverse the linked lists of other vertices and remove all edges containing vet\n    for (int i = 0; i < graph->size; i++) {\n        cur = graph->heads[i];\n        pre = NULL;\n        while (cur) {\n            pre = cur;\n            cur = cur->next;\n            if (cur && cur->vertex == vet) {\n                pre->next = cur->next;\n                free(cur);\n                break;\n            }\n        }\n    }\n    // Move vertices after this vertex forward to fill gap\n    int i;\n    for (i = 0; i < graph->size; i++) {\n        if (graph->heads[i] == node)\n            break;\n    }\n    for (int j = i; j < graph->size - 1; j++) {\n        graph->heads[j] = graph->heads[j + 1];\n    }\n    graph->size--;\n    free(vet);\n}\n
graph_adjacency_list.kt
/* Undirected graph class based on adjacency list */\nclass GraphAdjList(edges: Array<Array<Vertex?>>) {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    val adjList = HashMap<Vertex, MutableList<Vertex>>()\n\n    /* Constructor */\n    init {\n        // Add all vertices and edges\n        for (edge in edges) {\n            addVertex(edge[0]!!)\n            addVertex(edge[1]!!)\n            addEdge(edge[0]!!, edge[1]!!)\n        }\n    }\n\n    /* Get the number of vertices */\n    fun size(): Int {\n        return adjList.size\n    }\n\n    /* Add edge */\n    fun addEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Add edge vet1 - vet2\n        adjList[vet1]?.add(vet2)\n        adjList[vet2]?.add(vet1)\n    }\n\n    /* Remove edge */\n    fun removeEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Remove edge vet1 - vet2\n        adjList[vet1]?.remove(vet2)\n        adjList[vet2]?.remove(vet1)\n    }\n\n    /* Add vertex */\n    fun addVertex(vet: Vertex) {\n        if (adjList.containsKey(vet))\n            return\n        // Add a new linked list in the adjacency list\n        adjList[vet] = mutableListOf()\n    }\n\n    /* Remove vertex */\n    fun removeVertex(vet: Vertex) {\n        if (!adjList.containsKey(vet))\n            throw IllegalArgumentException()\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.remove(vet)\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (list in adjList.values) {\n            list.remove(vet)\n        }\n    }\n\n    /* Print adjacency list */\n    fun print() {\n        println(\"Adjacency list =\")\n        for (pair in adjList.entries) {\n            val tmp = mutableListOf<Int>()\n            for (vertex in pair.value) {\n                tmp.add(vertex._val)\n            }\n            println(\"${pair.key._val}: $tmp,\")\n        }\n    }\n}\n
graph_adjacency_list.rb
### Undirected graph class based on adjacency list ###\nclass GraphAdjList\n  attr_reader :adj_list\n\n  ### Constructor ###\n  def initialize(edges)\n    # Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    @adj_list = {}\n    # Add all vertices and edges\n    for edge in edges\n      add_vertex(edge[0])\n      add_vertex(edge[1])\n      add_edge(edge[0], edge[1])\n    end\n  end\n\n  ### Get number of vertices ###\n  def size\n    @adj_list.length\n  end\n\n  ### Add edge ###\n  def add_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    @adj_list[vet1] << vet2\n    @adj_list[vet2] << vet1\n  end\n\n  ### Delete edge ###\n  def remove_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    # Remove edge vet1 - vet2\n    @adj_list[vet1].delete(vet2)\n    @adj_list[vet2].delete(vet1)\n  end\n\n  ### Add vertex ###\n  def add_vertex(vet)\n    return if @adj_list.include?(vet)\n\n    # Add a new linked list in the adjacency list\n    @adj_list[vet] = []\n  end\n\n  ### Delete vertex ###\n  def remove_vertex(vet)\n    raise ArgumentError unless @adj_list.include?(vet)\n\n    # Remove the linked list corresponding to vertex vet in the adjacency list\n    @adj_list.delete(vet)\n    # Traverse the linked lists of other vertices and remove all edges containing vet\n    for vertex in @adj_list\n      @adj_list[vertex.first].delete(vet) if @adj_list[vertex.first].include?(vet)\n    end\n  end\n\n  ### Print adjacency list ###\n  def __print__\n    puts 'Adjacency list ='\n    for vertex in @adj_list\n      tmp = @adj_list[vertex.first].map { |v| v.val }\n      puts \"#{vertex.first.val}: #{tmp},\"\n    end\n  end\nend\n
","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#923-efficiency-comparison","level":2,"title":"9.2.3   Efficiency Comparison","text":"

Assuming the graph has \\(n\\) vertices and \\(m\\) edges, Table 9-2 compares the time efficiency and space efficiency of adjacency matrices and adjacency lists. Note that the adjacency list (linked list) corresponds to the implementation in this text, while the adjacency list (hash table) refers specifically to the implementation where all linked lists are replaced with hash tables.

Table 9-2   Comparison of adjacency matrix and adjacency list

Adjacency matrix Adjacency list (linked list) Adjacency list (hash table) Determine adjacency \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Add an edge \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Remove an edge \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Add a vertex \\(O(n)\\) \\(O(1)\\) \\(O(1)\\) Remove a vertex \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n)\\) Memory space usage \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n + m)\\)

Observing Table 9-2, it appears that the adjacency list (hash table) has the best time efficiency and space efficiency. However, in practice, operating on edges in the adjacency matrix is more efficient, requiring only a single array access or assignment operation. Overall, adjacency matrices embody the principle of \"trading space for time\", while adjacency lists embody \"trading time for space\".

","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   Graph Traversal","text":"

Trees represent \"one-to-many\" relationships, while graphs have a higher degree of freedom and can represent any \"many-to-many\" relationships. Therefore, we can view trees as a special case of graphs. Clearly, tree traversal operations are also a special case of graph traversal operations.

Both graphs and trees require the application of search algorithms to implement traversal operations. Graph traversal methods can also be divided into two types: breadth-first traversal and depth-first traversal.

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931-breadth-first-search","level":2,"title":"9.3.1   Breadth-First Search","text":"

Breadth-first search is a near-to-far traversal method that, starting from a certain node, always prioritizes visiting the nearest vertices and expands outward layer by layer. As shown in Figure 9-9, starting from the top-left vertex, first traverse all adjacent vertices of that vertex, then traverse all adjacent vertices of the next vertex, and so on, until all vertices have been visited.

Figure 9-9   Breadth-first search of a graph

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1-algorithm-implementation","level":3,"title":"1.   Algorithm Implementation","text":"

BFS is typically implemented with the help of a queue, as shown in the code below. The queue has a \"first in, first out\" property, which aligns with the BFS idea of \"near to far\".

  1. Add the starting vertex startVet to the queue and begin the loop.
  2. In each iteration of the loop, pop the vertex at the front of the queue and record it as visited, then add all adjacent vertices of that vertex to the back of the queue.
  3. Repeat step 2. until all vertices have been visited.

To prevent revisiting vertices, we use a hash set visited to record which nodes have been visited.

Tip

A hash set can be viewed as a hash table that stores only key without storing value. It can perform addition, deletion, lookup, and modification operations on key in \\(O(1)\\) time complexity. Based on the uniqueness of key, hash sets are typically used for data deduplication and similar scenarios.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_bfs.py
def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Breadth-first traversal\"\"\"\n    # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n    # Vertex traversal sequence\n    res = []\n    # Hash set for recording vertices that have been visited\n    visited = set[Vertex]([start_vet])\n    # Queue used to implement BFS\n    que = deque[Vertex]([start_vet])\n    # Starting from vertex vet, loop until all vertices are visited\n    while len(que) > 0:\n        vet = que.popleft()  # Dequeue the front vertex\n        res.append(vet)  # Record visited vertex\n        # Traverse all adjacent vertices of this vertex\n        for adj_vet in graph.adj_list[vet]:\n            if adj_vet in visited:\n                continue  # Skip vertices that have been visited\n            que.append(adj_vet)  # Only enqueue unvisited vertices\n            visited.add(adj_vet)  # Mark this vertex as visited\n    # Return vertex traversal sequence\n    return res\n
graph_bfs.cpp
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {\n    // Vertex traversal sequence\n    vector<Vertex *> res;\n    // Hash set for recording vertices that have been visited\n    unordered_set<Vertex *> visited = {startVet};\n    // Queue used to implement BFS\n    queue<Vertex *> que;\n    que.push(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.empty()) {\n        Vertex *vet = que.front();\n        que.pop();          // Dequeue the front vertex\n        res.push_back(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (auto adjVet : graph.adjList[vet]) {\n            if (visited.count(adjVet))\n                continue;            // Skip vertices that have been visited\n            que.push(adjVet);        // Only enqueue unvisited vertices\n            visited.emplace(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
graph_bfs.java
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = new ArrayList<>();\n    // Hash set for recording vertices that have been visited\n    Set<Vertex> visited = new HashSet<>();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    Queue<Vertex> que = new LinkedList<>();\n    que.offer(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.isEmpty()) {\n        Vertex vet = que.poll(); // Dequeue the front vertex\n        res.add(vet);            // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (Vertex adjVet : graph.adjList.get(vet)) {\n            if (visited.contains(adjVet))\n                continue;        // Skip vertices that have been visited\n            que.offer(adjVet);   // Only enqueue unvisited vertices\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
graph_bfs.cs
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = [];\n    // Hash set for recording vertices that have been visited\n    HashSet<Vertex> visited = [startVet];\n    // Queue used to implement BFS\n    Queue<Vertex> que = new();\n    que.Enqueue(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.Count > 0) {\n        Vertex vet = que.Dequeue(); // Dequeue the front vertex\n        res.Add(vet);               // Record visited vertex\n        foreach (Vertex adjVet in graph.adjList[vet]) {\n            if (visited.Contains(adjVet)) {\n                continue;          // Skip vertices that have been visited\n            }\n            que.Enqueue(adjVet);   // Only enqueue unvisited vertices\n            visited.Add(adjVet);   // Mark this vertex as visited\n        }\n    }\n\n    // Return vertex traversal sequence\n    return res;\n}\n
graph_bfs.go
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphBFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Vertex traversal sequence\n    res := make([]Vertex, 0)\n    // Hash set for recording vertices that have been visited\n    visited := make(map[Vertex]struct{})\n    visited[startVet] = struct{}{}\n    // Queue used to implement BFS, using slice to simulate queue\n    queue := make([]Vertex, 0)\n    queue = append(queue, startVet)\n    // Starting from vertex vet, loop until all vertices are visited\n    for len(queue) > 0 {\n        // Dequeue the front vertex\n        vet := queue[0]\n        queue = queue[1:]\n        // Record visited vertex\n        res = append(res, vet)\n        // Traverse all adjacent vertices of this vertex\n        for _, adjVet := range g.adjList[vet] {\n            _, isExist := visited[adjVet]\n            // Only enqueue unvisited vertices\n            if !isExist {\n                queue = append(queue, adjVet)\n                visited[adjVet] = struct{}{}\n            }\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
graph_bfs.swift
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Vertex traversal sequence\n    var res: [Vertex] = []\n    // Hash set for recording vertices that have been visited\n    var visited: Set<Vertex> = [startVet]\n    // Queue used to implement BFS\n    var que: [Vertex] = [startVet]\n    // Starting from vertex vet, loop until all vertices are visited\n    while !que.isEmpty {\n        let vet = que.removeFirst() // Dequeue the front vertex\n        res.append(vet) // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for adjVet in graph.adjList[vet] ?? [] {\n            if visited.contains(adjVet) {\n                continue // Skip vertices that have been visited\n            }\n            que.append(adjVet) // Only enqueue unvisited vertices\n            visited.insert(adjVet) // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
graph_bfs.js
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphBFS(graph, startVet) {\n    // Vertex traversal sequence\n    const res = [];\n    // Hash set for recording vertices that have been visited\n    const visited = new Set();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    const que = [startVet];\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.length) {\n        const vet = que.shift(); // Dequeue the front vertex\n        res.push(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Skip vertices that have been visited\n            }\n            que.push(adjVet); // Only enqueue unvisited vertices\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
graph_bfs.ts
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Vertex traversal sequence\n    const res: Vertex[] = [];\n    // Hash set for recording vertices that have been visited\n    const visited: Set<Vertex> = new Set();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    const que = [startVet];\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.length) {\n        const vet = que.shift(); // Dequeue the front vertex\n        res.push(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Skip vertices that have been visited\n            }\n            que.push(adjVet); // Only enqueue unvisited\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
graph_bfs.dart
/* Breadth-first traversal */\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n  // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  // Vertex traversal sequence\n  List<Vertex> res = [];\n  // Hash set for recording vertices that have been visited\n  Set<Vertex> visited = {};\n  visited.add(startVet);\n  // Queue used to implement BFS\n  Queue<Vertex> que = Queue();\n  que.add(startVet);\n  // Starting from vertex vet, loop until all vertices are visited\n  while (que.isNotEmpty) {\n    Vertex vet = que.removeFirst(); // Dequeue the front vertex\n    res.add(vet); // Record visited vertex\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex adjVet in graph.adjList[vet]!) {\n      if (visited.contains(adjVet)) {\n        continue; // Skip vertices that have been visited\n      }\n      que.add(adjVet); // Only enqueue unvisited vertices\n      visited.add(adjVet); // Mark this vertex as visited\n    }\n  }\n  // Return vertex traversal sequence\n  return res;\n}\n
graph_bfs.rs
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Vertex traversal sequence\n    let mut res = vec![];\n    // Hash set for recording vertices that have been visited\n    let mut visited = HashSet::new();\n    visited.insert(start_vet);\n    // Queue used to implement BFS\n    let mut que = VecDeque::new();\n    que.push_back(start_vet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while let Some(vet) = que.pop_front() {\n        res.push(vet); // Record visited vertex\n\n        // Traverse all adjacent vertices of this vertex\n        if let Some(adj_vets) = graph.adj_list.get(&vet) {\n            for &adj_vet in adj_vets {\n                if visited.contains(&adj_vet) {\n                    continue; // Skip vertices that have been visited\n                }\n                que.push_back(adj_vet); // Only enqueue unvisited vertices\n                visited.insert(adj_vet); // Mark this vertex as visited\n            }\n        }\n    }\n    // Return vertex traversal sequence\n    res\n}\n
graph_bfs.c
/* Node queue structure */\ntypedef struct {\n    Vertex *vertices[MAX_SIZE];\n    int front, rear, size;\n} Queue;\n\n/* Constructor */\nQueue *newQueue() {\n    Queue *q = (Queue *)malloc(sizeof(Queue));\n    q->front = q->rear = q->size = 0;\n    return q;\n}\n\n/* Check if the queue is empty */\nint isEmpty(Queue *q) {\n    return q->size == 0;\n}\n\n/* Enqueue operation */\nvoid enqueue(Queue *q, Vertex *vet) {\n    q->vertices[q->rear] = vet;\n    q->rear = (q->rear + 1) % MAX_SIZE;\n    q->size++;\n}\n\n/* Dequeue operation */\nVertex *dequeue(Queue *q) {\n    Vertex *vet = q->vertices[q->front];\n    q->front = (q->front + 1) % MAX_SIZE;\n    q->size--;\n    return vet;\n}\n\n/* Check if vertex has been visited */\nint isVisited(Vertex **visited, int size, Vertex *vet) {\n    // Traverse to find node using O(n) time\n    for (int i = 0; i < size; i++) {\n        if (visited[i] == vet)\n            return 1;\n    }\n    return 0;\n}\n\n/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvoid graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) {\n    // Queue used to implement BFS\n    Queue *queue = newQueue();\n    enqueue(queue, startVet);\n    visited[(*visitedSize)++] = startVet;\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!isEmpty(queue)) {\n        Vertex *vet = dequeue(queue); // Dequeue the front vertex\n        res[(*resSize)++] = vet;      // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        AdjListNode *node = findNode(graph, vet);\n        while (node != NULL) {\n            // Skip vertices that have been visited\n            if (!isVisited(visited, *visitedSize, node->vertex)) {\n                enqueue(queue, node->vertex);             // Only enqueue unvisited vertices\n                visited[(*visitedSize)++] = node->vertex; // Mark this vertex as visited\n            }\n            node = node->next;\n        }\n    }\n    // Free memory\n    free(queue);\n}\n
graph_bfs.kt
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {\n    // Vertex traversal sequence\n    val res = mutableListOf<Vertex?>()\n    // Hash set for recording vertices that have been visited\n    val visited = HashSet<Vertex>()\n    visited.add(startVet)\n    // Queue used to implement BFS\n    val que = LinkedList<Vertex>()\n    que.offer(startVet)\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.isEmpty()) {\n        val vet = que.poll() // Dequeue the front vertex\n        res.add(vet)         // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (adjVet in graph.adjList[vet]!!) {\n            if (visited.contains(adjVet))\n                continue        // Skip vertices that have been visited\n            que.offer(adjVet)   // Only enqueue unvisited vertices\n            visited.add(adjVet) // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
graph_bfs.rb
### Breadth-first traversal ###\ndef graph_bfs(graph, start_vet)\n  # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  # Vertex traversal sequence\n  res = []\n  # Hash set for recording vertices that have been visited\n  visited = Set.new([start_vet])\n  # Queue used to implement BFS\n  que = [start_vet]\n  # Starting from vertex vet, loop until all vertices are visited\n  while que.length > 0\n    vet = que.shift # Dequeue the front vertex\n    res << vet # Record visited vertex\n    # Traverse all adjacent vertices of this vertex\n    for adj_vet in graph.adj_list[vet]\n      next if visited.include?(adj_vet) # Skip vertices that have been visited\n      que << adj_vet # Only enqueue unvisited vertices\n      visited.add(adj_vet) # Mark this vertex as visited\n    end\n  end\n  # Return vertex traversal sequence\n  res\nend\n

The code is relatively abstract; it is recommended to refer to Figure 9-10 to deepen understanding.

<1><2><3><4><5><6><7><8><9><10><11>

Figure 9-10   Steps of breadth-first search of a graph

Is the breadth-first traversal sequence unique?

Not unique. Breadth-first search only requires traversing in a \"near to far\" order, and the traversal order of vertices at the same distance can be arbitrarily shuffled. Taking Figure 9-10 as an example, the visit order of vertices \\(1\\) and \\(3\\) can be swapped, as can the visit order of vertices \\(2\\), \\(4\\), and \\(6\\).

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2-complexity-analysis","level":3,"title":"2.   Complexity Analysis","text":"

Time complexity: All vertices will be enqueued and dequeued once, using \\(O(|V|)\\) time; in the process of traversing adjacent vertices, since it is an undirected graph, all edges will be visited \\(2\\) times, using \\(O(2|E|)\\) time; overall using \\(O(|V| + |E|)\\) time.

Space complexity: The list res, hash set visited, and queue que can contain at most \\(|V|\\) vertices, using \\(O(|V|)\\) space.

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#932-depth-first-search","level":2,"title":"9.3.2   Depth-First Search","text":"

Depth-first search is a traversal method that prioritizes going as far as possible, then backtracks when no path remains. As shown in Figure 9-11, starting from the top-left vertex, visit an adjacent vertex of the current vertex, continuing until reaching a dead end, then return and continue going as far as possible before returning again, and so on, until all vertices have been traversed.

Figure 9-11   Depth-first search of a graph

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1-algorithm-implementation_1","level":3,"title":"1.   Algorithm Implementation","text":"

This \"go as far as possible then return\" algorithm paradigm is typically implemented using recursion. Similar to breadth-first search, in depth-first search we also need a hash set visited to record visited vertices and avoid revisiting.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_dfs.py
def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):\n    \"\"\"Depth-first traversal helper function\"\"\"\n    res.append(vet)  # Record visited vertex\n    visited.add(vet)  # Mark this vertex as visited\n    # Traverse all adjacent vertices of this vertex\n    for adjVet in graph.adj_list[vet]:\n        if adjVet in visited:\n            continue  # Skip vertices that have been visited\n        # Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet)\n\ndef graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Depth-first traversal\"\"\"\n    # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n    # Vertex traversal sequence\n    res = []\n    # Hash set for recording vertices that have been visited\n    visited = set[Vertex]()\n    dfs(graph, visited, res, start_vet)\n    return res\n
graph_dfs.cpp
/* Depth-first traversal helper function */\nvoid dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {\n    res.push_back(vet);   // Record visited vertex\n    visited.emplace(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex *adjVet : graph.adjList[vet]) {\n        if (visited.count(adjVet))\n            continue; // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {\n    // Vertex traversal sequence\n    vector<Vertex *> res;\n    // Hash set for recording vertices that have been visited\n    unordered_set<Vertex *> visited;\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.java
/* Depth-first traversal helper function */\nvoid dfs(GraphAdjList graph, Set<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.add(vet);     // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex adjVet : graph.adjList.get(vet)) {\n        if (visited.contains(adjVet))\n            continue; // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = new ArrayList<>();\n    // Hash set for recording vertices that have been visited\n    Set<Vertex> visited = new HashSet<>();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.cs
/* Depth-first traversal helper function */\nvoid DFS(GraphAdjList graph, HashSet<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.Add(vet);     // Record visited vertex\n    visited.Add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    foreach (Vertex adjVet in graph.adjList[vet]) {\n        if (visited.Contains(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        DFS(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = [];\n    // Hash set for recording vertices that have been visited\n    HashSet<Vertex> visited = [];\n    DFS(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.go
/* Depth-first traversal helper function */\nfunc dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) {\n    // append operation returns a new reference, must reassign original reference to new slice's reference\n    *res = append(*res, vet)\n    visited[vet] = struct{}{}\n    // Traverse all adjacent vertices of this vertex\n    for _, adjVet := range g.adjList[vet] {\n        _, isExist := visited[adjVet]\n        // Recursively visit adjacent vertices\n        if !isExist {\n            dfs(g, visited, res, adjVet)\n        }\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphDFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Vertex traversal sequence\n    res := make([]Vertex, 0)\n    // Hash set for recording vertices that have been visited\n    visited := make(map[Vertex]struct{})\n    dfs(g, visited, &res, startVet)\n    // Return vertex traversal sequence\n    return res\n}\n
graph_dfs.swift
/* Depth-first traversal helper function */\nfunc dfs(graph: GraphAdjList, visited: inout Set<Vertex>, res: inout [Vertex], vet: Vertex) {\n    res.append(vet) // Record visited vertex\n    visited.insert(vet) // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for adjVet in graph.adjList[vet] ?? [] {\n        if visited.contains(adjVet) {\n            continue // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph: graph, visited: &visited, res: &res, vet: adjVet)\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Vertex traversal sequence\n    var res: [Vertex] = []\n    // Hash set for recording vertices that have been visited\n    var visited: Set<Vertex> = []\n    dfs(graph: graph, visited: &visited, res: &res, vet: startVet)\n    return res\n}\n
graph_dfs.js
/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction dfs(graph, visited, res, vet) {\n    res.push(vet); // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphDFS(graph, startVet) {\n    // Vertex traversal sequence\n    const res = [];\n    // Hash set for recording vertices that have been visited\n    const visited = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.ts
/* Depth-first traversal helper function */\nfunction dfs(\n    graph: GraphAdjList,\n    visited: Set<Vertex>,\n    res: Vertex[],\n    vet: Vertex\n): void {\n    res.push(vet); // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Vertex traversal sequence\n    const res: Vertex[] = [];\n    // Hash set for recording vertices that have been visited\n    const visited: Set<Vertex> = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.dart
/* Depth-first traversal helper function */\nvoid dfs(\n  GraphAdjList graph,\n  Set<Vertex> visited,\n  List<Vertex> res,\n  Vertex vet,\n) {\n  res.add(vet); // Record visited vertex\n  visited.add(vet); // Mark this vertex as visited\n  // Traverse all adjacent vertices of this vertex\n  for (Vertex adjVet in graph.adjList[vet]!) {\n    if (visited.contains(adjVet)) {\n      continue; // Skip vertices that have been visited\n    }\n    // Recursively visit adjacent vertices\n    dfs(graph, visited, res, adjVet);\n  }\n}\n\n/* Depth-first traversal */\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n  // Vertex traversal sequence\n  List<Vertex> res = [];\n  // Hash set for recording vertices that have been visited\n  Set<Vertex> visited = {};\n  dfs(graph, visited, res, startVet);\n  return res;\n}\n
graph_dfs.rs
/* Depth-first traversal helper function */\nfn dfs(graph: &GraphAdjList, visited: &mut HashSet<Vertex>, res: &mut Vec<Vertex>, vet: Vertex) {\n    res.push(vet); // Record visited vertex\n    visited.insert(vet); // Mark this vertex as visited\n                         // Traverse all adjacent vertices of this vertex\n    if let Some(adj_vets) = graph.adj_list.get(&vet) {\n        for &adj_vet in adj_vets {\n            if visited.contains(&adj_vet) {\n                continue; // Skip vertices that have been visited\n            }\n            // Recursively visit adjacent vertices\n            dfs(graph, visited, res, adj_vet);\n        }\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Vertex traversal sequence\n    let mut res = vec![];\n    // Hash set for recording vertices that have been visited\n    let mut visited = HashSet::new();\n    dfs(&graph, &mut visited, &mut res, start_vet);\n\n    res\n}\n
graph_dfs.c
/* Check if vertex has been visited */\nint isVisited(Vertex **res, int size, Vertex *vet) {\n    // Traverse to find node using O(n) time\n    for (int i = 0; i < size; i++) {\n        if (res[i] == vet) {\n            return 1;\n        }\n    }\n    return 0;\n}\n\n/* Depth-first traversal helper function */\nvoid dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) {\n    // Record visited vertex\n    res[(*resSize)++] = vet;\n    // Traverse all adjacent vertices of this vertex\n    AdjListNode *node = findNode(graph, vet);\n    while (node != NULL) {\n        // Skip vertices that have been visited\n        if (!isVisited(res, *resSize, node->vertex)) {\n            // Recursively visit adjacent vertices\n            dfs(graph, res, resSize, node->vertex);\n        }\n        node = node->next;\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvoid graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) {\n    dfs(graph, res, resSize, startVet);\n}\n
graph_dfs.kt
/* Depth-first traversal helper function */\nfun dfs(\n    graph: GraphAdjList,\n    visited: MutableSet<Vertex?>,\n    res: MutableList<Vertex?>,\n    vet: Vertex?\n) {\n    res.add(vet)     // Record visited vertex\n    visited.add(vet) // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (adjVet in graph.adjList[vet]!!) {\n        if (visited.contains(adjVet))\n            continue  // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet)\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {\n    // Vertex traversal sequence\n    val res = mutableListOf<Vertex?>()\n    // Hash set for recording vertices that have been visited\n    val visited = HashSet<Vertex?>()\n    dfs(graph, visited, res, startVet)\n    return res\n}\n
graph_dfs.rb
### Depth-first traversal helper function ###\ndef dfs(graph, visited, res, vet)\n  res << vet # Record visited vertex\n  visited.add(vet) # Mark this vertex as visited\n  # Traverse all adjacent vertices of this vertex\n  for adj_vet in graph.adj_list[vet]\n    next if visited.include?(adj_vet) # Skip vertices that have been visited\n    # Recursively visit adjacent vertices\n    dfs(graph, visited, res, adj_vet)\n  end\nend\n\n### Depth-first traversal ###\ndef graph_dfs(graph, start_vet)\n  # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  # Vertex traversal sequence\n  res = []\n  # Hash set for recording vertices that have been visited\n  visited = Set.new\n  dfs(graph, visited, res, start_vet)\n  res\nend\n

The algorithm flow of depth-first search is shown in Figure 9-12.

  • Straight dashed lines represent downward recursion, indicating that a new recursive method has been initiated to visit a new vertex.
  • Curved dashed lines represent upward backtracking, indicating that this recursive method has returned to the position where it was initiated.

To deepen understanding, it is recommended to combine Figure 9-12 with the code to mentally simulate (or draw out) the entire DFS process, including when each recursive method is initiated and when it returns.

<1><2><3><4><5><6><7><8><9><10><11>

Figure 9-12   Steps of depth-first search of a graph

Is the depth-first traversal sequence unique?

Similar to breadth-first search, the order of depth-first traversal sequences is also not unique. Given a certain vertex, exploring in any direction first is valid, meaning the order of adjacent vertices can be arbitrarily shuffled, all being depth-first search.

Taking tree traversal as an example, \"root \\(\\rightarrow\\) left \\(\\rightarrow\\) right\", \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\", and \"left \\(\\rightarrow\\) right \\(\\rightarrow\\) root\" correspond to pre-order, in-order, and post-order traversals, respectively. They represent three different traversal priorities, yet all three belong to depth-first search.

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2-complexity-analysis_1","level":3,"title":"2.   Complexity Analysis","text":"

Time complexity: All vertices will be visited \\(1\\) time, using \\(O(|V|)\\) time; all edges will be visited \\(2\\) times, using \\(O(2|E|)\\) time; overall using \\(O(|V| + |E|)\\) time.

Space complexity: The list res and hash set visited can contain at most \\(|V|\\) vertices, and the maximum recursion depth is \\(|V|\\), therefore using \\(O(|V|)\\) space.

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/summary/","level":1,"title":"9.4   Summary","text":"","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_graph/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Graphs consist of vertices and edges and can be represented as a set of vertices and a set of edges.
  • Compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex.
  • Directed graphs have edges with directionality, connected graphs have all vertices reachable from any vertex, and weighted graphs have edges that each contain a weight variable.
  • Adjacency matrices use matrices to represent graphs, where each row (column) represents a vertex, and matrix elements represent edges, using \\(1\\) or \\(0\\) to indicate whether two vertices have an edge or not. Adjacency matrices are highly efficient for addition, deletion, lookup, and modification operations, but consume significant space.
  • Adjacency lists use multiple linked lists to represent graphs, where the \\(i\\)-th linked list corresponds to vertex \\(i\\) and stores all adjacent vertices of that vertex. Adjacency lists are more space-efficient than adjacency matrices, but have lower time efficiency because they require traversing linked lists to find edges.
  • When linked lists in adjacency lists become too long, they can be converted to red-black trees or hash tables, thereby improving lookup efficiency.
  • From an algorithmic perspective, adjacency matrices embody \"trading space for time\", while adjacency lists embody \"trading time for space\".
  • Graphs can be used to model various real-world systems, such as social networks and subway lines.
  • Trees are a special case of graphs, and tree traversal is a special case of graph traversal.
  • Breadth-first search of graphs is a near-to-far, layer-by-layer expansion search method, typically implemented using a queue.
  • Depth-first search of graphs is a search method that prioritizes going as far as possible and backtracks when no path remains, commonly implemented using recursion.
","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Is a path defined as a sequence of vertices or a sequence of edges?

The definitions in different language versions of Wikipedia are inconsistent: the English version states \"a path is a sequence of edges\", while the Chinese version states \"a path is a sequence of vertices\". The following is the original English text: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

In this text, a path is viewed as a sequence of edges, not a sequence of vertices. This is because there may be multiple edges connecting two vertices, in which case each edge corresponds to a path.

Q: In a disconnected graph, will there be unreachable vertices?

In a disconnected graph, starting from a certain vertex, at least one vertex cannot be reached. Traversing a disconnected graph requires setting multiple starting points to traverse all connected components of the graph.

Q: In an adjacency list, is there a requirement for the order of \"all vertices connected to that vertex\"?

It can be in any order. However, in practical applications, it may be necessary to sort according to specified rules, such as the order in which vertices were added, or the order of vertex values, which helps quickly find vertices \"with certain extreme values\".

","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"Chapter 15.   Greedy","text":"

Abstract

Sunflowers turn toward the sun, constantly pursuing the maximum potential for their own growth.

Through rounds of simple choices, greedy strategies gradually lead to the best answer.

","path":["Chapter 15. Greedy","Chapter 15.   Greedy"],"tags":[]},{"location":"chapter_greedy/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 15.1   Greedy Algorithm
  • 15.2   Fractional Knapsack Problem
  • 15.3   Maximum Capacity Problem
  • 15.4   Maximum Product Cutting Problem
  • 15.5   Summary
","path":["Chapter 15. Greedy","Chapter 15.   Greedy"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   Fractional Knapsack Problem","text":"

Question

Given \\(n\\) items, where the weight of the \\(i\\)-th item is \\(wgt[i-1]\\) and its value is \\(val[i-1]\\), and a knapsack with capacity \\(cap\\). Each item can be selected only once, but a portion of an item can be selected, with the value calculated based on the proportion of weight selected, what is the maximum value of items in the knapsack under the limited capacity? An example is shown in Figure 15-3.

Figure 15-3   Example data for the fractional knapsack problem

The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, with states including the current item \\(i\\) and capacity \\(c\\), and the goal being to maximize value under the limited knapsack capacity.

The difference is that this problem allows selecting only a portion of an item. As shown in Figure 15-4, we can arbitrarily split items and calculate the corresponding value based on the weight proportion.

  1. For item \\(i\\), its value per unit weight is \\(val[i-1] / wgt[i-1]\\), referred to as unit value.
  2. Suppose we put a portion of item \\(i\\) with weight \\(w\\) into the knapsack, then the value added to the knapsack is \\(w \\times val[i-1] / wgt[i-1]\\).

Figure 15-4   Value of items per unit weight

","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#1-greedy-strategy-determination","level":3,"title":"1.   Greedy Strategy Determination","text":"

Maximizing the total value of items in the knapsack is essentially maximizing the value per unit weight of items. From this, we can derive the greedy strategy shown in Figure 15-5.

  1. Sort items by unit value from high to low.
  2. Iterate through all items, greedily selecting the item with the highest unit value in each round.
  3. If the remaining knapsack capacity is insufficient, use a portion of the current item to fill the knapsack.

Figure 15-5   Greedy strategy for the fractional knapsack problem

","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

We created an Item class to facilitate sorting items by unit value. We loop to make greedy selections, breaking when the knapsack is full and returning the solution:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fractional_knapsack.py
class Item:\n    \"\"\"Item\"\"\"\n\n    def __init__(self, w: int, v: int):\n        self.w = w  # Item weight\n        self.v = v  # Item value\n\ndef fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Fractional knapsack: Greedy algorithm\"\"\"\n    # Create item list with two attributes: weight, value\n    items = [Item(w, v) for w, v in zip(wgt, val)]\n    # Sort by unit value item.v / item.w from high to low\n    items.sort(key=lambda item: item.v / item.w, reverse=True)\n    # Loop for greedy selection\n    res = 0\n    for item in items:\n        if item.w <= cap:\n            # If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v\n            cap -= item.w\n        else:\n            # If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap\n            # No remaining capacity, so break out of the loop\n            break\n    return res\n
fractional_knapsack.cpp
/* Item */\nclass Item {\n  public:\n    int w; // Item weight\n    int v; // Item value\n\n    Item(int w, int v) : w(w), v(v) {\n    }\n};\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {\n    // Create item list with two attributes: weight, value\n    vector<Item> items;\n    for (int i = 0; i < wgt.size(); i++) {\n        items.push_back(Item(wgt[i], val[i]));\n    }\n    // Sort by unit value item.v / item.w from high to low\n    sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });\n    // Loop for greedy selection\n    double res = 0;\n    for (auto &item : items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double)item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.java
/* Item */\nclass Item {\n    int w; // Item weight\n    int v; // Item value\n\n    public Item(int w, int v) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Create item list with two attributes: weight, value\n    Item[] items = new Item[wgt.length];\n    for (int i = 0; i < wgt.length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // Sort by unit value item.v / item.w from high to low\n    Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));\n    // Loop for greedy selection\n    double res = 0;\n    for (Item item : items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double) item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.cs
/* Item */\nclass Item(int w, int v) {\n    public int w = w; // Item weight\n    public int v = v; // Item value\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble FractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Create item list with two attributes: weight, value\n    Item[] items = new Item[wgt.Length];\n    for (int i = 0; i < wgt.Length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // Sort by unit value item.v / item.w from high to low\n    Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));\n    // Loop for greedy selection\n    double res = 0;\n    foreach (Item item in items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double)item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.go
/* Item */\ntype Item struct {\n    w int // Item weight\n    v int // Item value\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunc fractionalKnapsack(wgt []int, val []int, cap int) float64 {\n    // Create item list with two attributes: weight, value\n    items := make([]Item, len(wgt))\n    for i := 0; i < len(wgt); i++ {\n        items[i] = Item{wgt[i], val[i]}\n    }\n    // Sort by unit value item.v / item.w from high to low\n    sort.Slice(items, func(i, j int) bool {\n        return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)\n    })\n    // Loop for greedy selection\n    res := 0.0\n    for _, item := range items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += float64(item.v)\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += float64(item.v) / float64(item.w) * float64(cap)\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.swift
/* Item */\nclass Item {\n    var w: Int // Item weight\n    var v: Int // Item value\n\n    init(w: Int, v: Int) {\n        self.w = w\n        self.v = v\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunc fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {\n    // Create item list with two attributes: weight, value\n    var items = zip(wgt, val).map { Item(w: $0, v: $1) }\n    // Sort by unit value item.v / item.w from high to low\n    items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }\n    // Loop for greedy selection\n    var res = 0.0\n    var cap = cap\n    for item in items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += Double(item.v)\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += Double(item.v) / Double(item.w) * Double(cap)\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.js
/* Item */\nclass Item {\n    constructor(w, v) {\n        this.w = w; // Item weight\n        this.v = v; // Item value\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunction fractionalKnapsack(wgt, val, cap) {\n    // Create item list with two attributes: weight, value\n    const items = wgt.map((w, i) => new Item(w, val[i]));\n    // Sort by unit value item.v / item.w from high to low\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Loop for greedy selection\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.ts
/* Item */\nclass Item {\n    w: number; // Item weight\n    v: number; // Item value\n\n    constructor(w: number, v: number) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunction fractionalKnapsack(wgt: number[], val: number[], cap: number): number {\n    // Create item list with two attributes: weight, value\n    const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));\n    // Sort by unit value item.v / item.w from high to low\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Loop for greedy selection\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.dart
/* Item */\nclass Item {\n  int w; // Item weight\n  int v; // Item value\n\n  Item(this.w, this.v);\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(List<int> wgt, List<int> val, int cap) {\n  // Create item list with two attributes: weight, value\n  List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));\n  // Sort by unit value item.v / item.w from high to low\n  items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));\n  // Loop for greedy selection\n  double res = 0;\n  for (Item item in items) {\n    if (item.w <= cap) {\n      // If remaining capacity is sufficient, put the entire current item into the knapsack\n      res += item.v;\n      cap -= item.w;\n    } else {\n      // If remaining capacity is insufficient, put part of the current item into the knapsack\n      res += item.v / item.w * cap;\n      // No remaining capacity, so break out of the loop\n      break;\n    }\n  }\n  return res;\n}\n
fractional_knapsack.rs
/* Item */\nstruct Item {\n    w: i32, // Item weight\n    v: i32, // Item value\n}\n\nimpl Item {\n    fn new(w: i32, v: i32) -> Self {\n        Self { w, v }\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {\n    // Create item list with two attributes: weight, value\n    let mut items = wgt\n        .iter()\n        .zip(val.iter())\n        .map(|(&w, &v)| Item::new(w, v))\n        .collect::<Vec<Item>>();\n    // Sort by unit value item.v / item.w from high to low\n    items.sort_by(|a, b| {\n        (b.v as f64 / b.w as f64)\n            .partial_cmp(&(a.v as f64 / a.w as f64))\n            .unwrap()\n    });\n    // Loop for greedy selection\n    let mut res = 0.0;\n    for item in &items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v as f64;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += item.v as f64 / item.w as f64 * cap as f64;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    res\n}\n
fractional_knapsack.c
/* Item */\ntypedef struct {\n    int w; // Item weight\n    int v; // Item value\n} Item;\n\n/* Fractional knapsack: Greedy algorithm */\nfloat fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {\n    // Create item list with two attributes: weight, value\n    Item *items = malloc(sizeof(Item) * itemCount);\n    for (int i = 0; i < itemCount; i++) {\n        items[i] = (Item){.w = wgt[i], .v = val[i]};\n    }\n    // Sort by unit value item.v / item.w from high to low\n    qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);\n    // Loop for greedy selection\n    float res = 0.0;\n    for (int i = 0; i < itemCount; i++) {\n        if (items[i].w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += items[i].v;\n            cap -= items[i].w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (float)cap / items[i].w * items[i].v;\n            cap = 0;\n            break;\n        }\n    }\n    free(items);\n    return res;\n}\n
fractional_knapsack.kt
/* Item */\nclass Item(\n    val w: Int, // Item\n    val v: Int  // Item value\n)\n\n/* Fractional knapsack: Greedy algorithm */\nfun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {\n    // Create item list with two attributes: weight, value\n    var cap = c\n    val items = arrayOfNulls<Item>(wgt.size)\n    for (i in wgt.indices) {\n        items[i] = Item(wgt[i], _val[i])\n    }\n    // Sort by unit value item.v / item.w from high to low\n    items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }\n    // Loop for greedy selection\n    var res = 0.0\n    for (item in items) {\n        if (item!!.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += item.v.toDouble() / item.w * cap\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.rb
### Item ###\nclass Item\n  attr_accessor :w # Item weight\n  attr_accessor :v # Item value\n\n  def initialize(w, v)\n    @w = w\n    @v = v\n  end\nend\n\n### Fractional knapsack: greedy ###\ndef fractional_knapsack(wgt, val, cap)\n  # Create item list with two attributes: weight, value\n  items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }\n  # Sort by unit value item.v / item.w from high to low\n  items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }\n  # Loop for greedy selection\n  res = 0\n  for item in items\n    if item.w <= cap\n      # If remaining capacity is sufficient, put the entire current item into the knapsack\n      res += item.v\n      cap -= item.w\n    else\n      # If remaining capacity is insufficient, put part of the current item into the knapsack\n      res += (item.v.to_f / item.w) * cap\n      # No remaining capacity, so break out of the loop\n      break\n    end\n  end\n  res\nend\n

The time complexity of built-in sorting algorithms is usually \\(O(\\log n)\\), and the space complexity is usually \\(O(\\log n)\\) or \\(O(n)\\), depending on the specific implementation of the programming language.

Apart from sorting, in the worst case the entire item list needs to be traversed, therefore the time complexity is \\(O(n)\\), where \\(n\\) is the number of items.

Since an Item object list is initialized, the space complexity is \\(O(n)\\).

","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

Using proof by contradiction. Suppose item \\(x\\) has the highest unit value, and some algorithm yields a maximum value of res, but this solution does not include item \\(x\\).

Now remove a unit weight of any item from the knapsack and replace it with a unit weight of item \\(x\\). Since item \\(x\\) has the highest unit value, the total value after replacement will definitely be greater than res. This contradicts the assumption that res is the optimal solution, proving that the optimal solution must include item \\(x\\).

For other items in this solution, we can also construct the above contradiction. In summary, items with greater unit value are always better choices, which proves that the greedy strategy is effective.

As shown in Figure 15-6, if we view item weight and item unit value as the horizontal and vertical axes of a two-dimensional chart respectively, then the fractional knapsack problem can be transformed into \"finding the maximum area enclosed within a limited horizontal axis range\". This analogy can help us understand the effectiveness of the greedy strategy from a geometric perspective.

Figure 15-6   Geometric representation of the fractional knapsack problem

","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/","level":1,"title":"15.1   Greedy Algorithm","text":"

Greedy algorithm is a common algorithm for solving optimization problems. Its basic idea is to make the seemingly best choice at each decision stage of the problem, that is, to greedily make locally optimal decisions in hopes of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely applied in many practical problems.

Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as both relying on the optimal substructure property, but they work differently.

  • Dynamic programming considers all previous decisions when making the current decision, and uses solutions to past subproblems to construct the solution to the current subproblem.
  • Greedy algorithms do not consider past decisions, but instead make greedy choices moving forward, continually reducing the problem size until the problem is solved.

We will first understand how greedy algorithms work through the example problem \"coin change\". This problem has already been introduced in the \"Complete Knapsack Problem\" chapter, so I believe you are not unfamiliar with it.

Question

Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type of coin is \\(coins[i - 1]\\), and the target amount is \\(amt\\), with each type of coin available for repeated selection, what is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return \\(-1\\).

The greedy strategy adopted for this problem is shown in Figure 15-1. Given a target amount, we greedily select the coin that is not greater than and closest to it, and continuously repeat this step until the target amount is reached.

Figure 15-1   Greedy strategy for coin change

The implementation code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_greedy.py
def coin_change_greedy(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Greedy algorithm\"\"\"\n    # Assume coins list is sorted\n    i = len(coins) - 1\n    count = 0\n    # Loop to make greedy choices until no remaining amount\n    while amt > 0:\n        # Find the coin that is less than and closest to the remaining amount\n        while i > 0 and coins[i] > amt:\n            i -= 1\n        # Choose coins[i]\n        amt -= coins[i]\n        count += 1\n    # If no feasible solution is found, return -1\n    return count if amt == 0 else -1\n
coin_change_greedy.cpp
/* Coin change: Greedy algorithm */\nint coinChangeGreedy(vector<int> &coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.size() - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.java
/* Coin change: Greedy algorithm */\nint coinChangeGreedy(int[] coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.length - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.cs
/* Coin change: Greedy algorithm */\nint CoinChangeGreedy(int[] coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.Length - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.go
/* Coin change: Greedy algorithm */\nfunc coinChangeGreedy(coins []int, amt int) int {\n    // Assume coins list is sorted\n    i := len(coins) - 1\n    count := 0\n    // Loop to make greedy choices until no remaining amount\n    for amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        for i > 0 && coins[i] > amt {\n            i--\n        }\n        // Choose coins[i]\n        amt -= coins[i]\n        count++\n    }\n    // If no feasible solution is found, return -1\n    if amt != 0 {\n        return -1\n    }\n    return count\n}\n
coin_change_greedy.swift
/* Coin change: Greedy algorithm */\nfunc coinChangeGreedy(coins: [Int], amt: Int) -> Int {\n    // Assume coins list is sorted\n    var i = coins.count - 1\n    var count = 0\n    var amt = amt\n    // Loop to make greedy choices until no remaining amount\n    while amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        while i > 0 && coins[i] > amt {\n            i -= 1\n        }\n        // Choose coins[i]\n        amt -= coins[i]\n        count += 1\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1\n}\n
coin_change_greedy.js
/* Coin change: Greedy algorithm */\nfunction coinChangeGreedy(coins, amt) {\n    // Assume coins array is sorted\n    let i = coins.length - 1;\n    let count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt === 0 ? count : -1;\n}\n
coin_change_greedy.ts
/* Coin change: Greedy algorithm */\nfunction coinChangeGreedy(coins: number[], amt: number): number {\n    // Assume coins array is sorted\n    let i = coins.length - 1;\n    let count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt === 0 ? count : -1;\n}\n
coin_change_greedy.dart
/* Coin change: Greedy algorithm */\nint coinChangeGreedy(List<int> coins, int amt) {\n  // Assume coins list is sorted\n  int i = coins.length - 1;\n  int count = 0;\n  // Loop to make greedy choices until no remaining amount\n  while (amt > 0) {\n    // Find the coin that is less than and closest to the remaining amount\n    while (i > 0 && coins[i] > amt) {\n      i--;\n    }\n    // Choose coins[i]\n    amt -= coins[i];\n    count++;\n  }\n  // If no feasible solution is found, return -1\n  return amt == 0 ? count : -1;\n}\n
coin_change_greedy.rs
/* Coin change: Greedy algorithm */\nfn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {\n    // Assume coins list is sorted\n    let mut i = coins.len() - 1;\n    let mut count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        while i > 0 && coins[i] > amt {\n            i -= 1;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count += 1;\n    }\n    // If no feasible solution is found, return -1\n    if amt == 0 {\n        count\n    } else {\n        -1\n    }\n}\n
coin_change_greedy.c
/* Coin change: Greedy algorithm */\nint coinChangeGreedy(int *coins, int size, int amt) {\n    // Assume coins list is sorted\n    int i = size - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.kt
/* Coin change: Greedy algorithm */\nfun coinChangeGreedy(coins: IntArray, amt: Int): Int {\n    // Assume coins list is sorted\n    var am = amt\n    var i = coins.size - 1\n    var count = 0\n    // Loop to make greedy choices until no remaining amount\n    while (am > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > am) {\n            i--\n        }\n        // Choose coins[i]\n        am -= coins[i]\n        count++\n    }\n    // If no feasible solution is found, return -1\n    return if (am == 0) count else -1\n}\n
coin_change_greedy.rb
### Coin change: greedy ###\ndef coin_change_greedy(coins, amt)\n  # Assume coins list is sorted\n  i = coins.length - 1\n  count = 0\n  # Loop to make greedy choices until no remaining amount\n  while amt > 0\n    # Find the coin that is less than and closest to the remaining amount\n    while i > 0 && coins[i] > amt\n      i -= 1\n    end\n    # Choose coins[i]\n    amt -= coins[i]\n    count += 1\n  end\n  # Return -1 if no solution found\n  amt == 0 ? count : -1\nend\n

You might exclaim: So clean! The greedy algorithm solves the coin change problem in about ten lines of code.

","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1511-advantages-and-limitations-of-greedy-algorithms","level":2,"title":"15.1.1   Advantages and Limitations of Greedy Algorithms","text":"

Greedy algorithms are not only straightforward and simple to implement, but are also usually very efficient. In the code above, if the smallest coin denomination is \\(\\min(coins)\\), the greedy choice loops at most \\(amt / \\min(coins)\\) times, giving a time complexity of \\(O(amt / \\min(coins))\\). This is an order of magnitude smaller than the time complexity of the dynamic programming solution \\(O(n \\times amt)\\).

However, for certain coin denomination combinations, greedy algorithms cannot find the optimal solution. Figure 15-2 provides two examples.

  • Positive example \\(coins = [1, 5, 10, 20, 50, 100]\\): With this coin combination, given any \\(amt\\), the greedy algorithm can find the optimal solution.
  • Negative example \\(coins = [1, 20, 50]\\): Suppose \\(amt = 60\\), the greedy algorithm can only find the combination \\(50 + 1 \\times 10\\), totaling \\(11\\) coins, but dynamic programming can find the optimal solution \\(20 + 20 + 20\\), requiring only \\(3\\) coins.
  • Negative example \\(coins = [1, 49, 50]\\): Suppose \\(amt = 98\\), the greedy algorithm can only find the combination \\(50 + 1 \\times 48\\), totaling \\(49\\) coins, but dynamic programming can find the optimal solution \\(49 + 49\\), requiring only \\(2\\) coins.

Figure 15-2   Examples where greedy algorithms cannot find the optimal solution

In other words, for the coin change problem, greedy algorithms cannot guarantee finding the global optimal solution, and may even find very poor solutions. It is better suited for solving with dynamic programming.

Generally, the applicability of greedy algorithms falls into the following two situations.

  1. Can guarantee finding the optimal solution: In this situation, greedy algorithms are often the best choice, because they tend to be more efficient than backtracking and dynamic programming.
  2. Can find an approximate optimal solution: Greedy algorithms are also applicable in this situation. For many complex problems, finding the global optimal solution is very difficult, and being able to find a suboptimal solution with high efficiency is also very good.
","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512-characteristics-of-greedy-algorithms","level":2,"title":"15.1.2   Characteristics of Greedy Algorithms","text":"

So the question arises: what kind of problems are suitable for solving with greedy algorithms? Or in other words, under what conditions can greedy algorithms guarantee finding the optimal solution?

Compared to dynamic programming, the conditions for using greedy algorithms are stricter, mainly focusing on two properties of the problem.

  • Greedy choice property: Only when locally optimal choices can always lead to a globally optimal solution can greedy algorithms guarantee obtaining the optimal solution.
  • Optimal substructure: The optimal solution to the original problem contains the optimal solutions to subproblems.

Optimal substructure has already been introduced in the \"Dynamic Programming\" chapter, so we won't elaborate on it here. It's worth noting that the optimal substructure of some problems is not obvious, but they can still be solved using greedy algorithms.

We mainly explore methods for determining the greedy choice property. Although its description seems relatively simple, in practice, for many problems, proving the greedy choice property is not easy.

For example, in the coin change problem, although we can easily provide counterexamples to disprove the greedy choice property, proving it is quite difficult. If asked: what conditions must a coin combination satisfy to be solvable using a greedy algorithm? We often can only rely on intuition or examples to give an ambiguous answer, and find it difficult to provide a rigorous mathematical proof.

Quote

There is a paper that presents an algorithm with \\(O(n^3)\\) time complexity for determining whether a coin combination can use a greedy algorithm to find the optimal solution for any amount.

Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.

","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1513-steps-for-solving-problems-with-greedy-algorithms","level":2,"title":"15.1.3   Steps for Solving Problems with Greedy Algorithms","text":"

The problem-solving process for greedy problems can generally be divided into the following three steps.

  1. Problem analysis: Sort out and understand the problem characteristics, including state definition, optimization objectives, and constraints, etc. This step is also involved in backtracking and dynamic programming.
  2. Determine the greedy strategy: Determine how to make greedy choices at each step. This strategy should be able to reduce the problem size at each step, ultimately solving the entire problem.
  3. Correctness proof: It is usually necessary to prove that the problem has both greedy choice property and optimal substructure. This step may require mathematical proofs, such as mathematical induction or proof by contradiction.

Determining the greedy strategy is the core step in solving the problem, but it may not be easy to implement, mainly for the following reasons.

  • Greedy strategies differ greatly between different problems. For many problems, the greedy strategy is relatively straightforward, and we can derive it through some general thinking and attempts. However, for some complex problems, the greedy strategy may be very elusive, which really tests one's problem-solving experience and algorithmic ability.
  • Some greedy strategies are highly misleading. When we confidently design a greedy strategy, write the solution code and submit it for testing, we may find that some test cases cannot pass. This is because the designed greedy strategy is only \"partially correct\", as exemplified by the coin change problem discussed above.

To ensure correctness, we should rigorously mathematically prove the greedy strategy, usually using proof by contradiction or mathematical induction.

However, correctness proofs may also not be easy. If we have no clue, we usually choose to debug the code based on test cases, step by step modifying and verifying the greedy strategy.

","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514-typical-problems-solved-by-greedy-algorithms","level":2,"title":"15.1.4   Typical Problems Solved by Greedy Algorithms","text":"

Greedy algorithms are often applied to optimization problems that satisfy greedy choice property and optimal substructure. Below are some typical greedy algorithm problems.

  • Coin change problem: With certain coin combinations, greedy algorithms can always obtain the optimal solution.
  • Interval scheduling problem: Suppose you have some tasks, each taking place during a period of time, and your goal is to complete as many tasks as possible. If you always choose the task that ends earliest, then the greedy algorithm can obtain the optimal solution.
  • Fractional knapsack problem: Given a set of items and a carrying capacity, your goal is to select a set of items such that the total weight does not exceed the carrying capacity and the total value is maximized. If you always choose the item with the highest value-to-weight ratio (value / weight), then the greedy algorithm can obtain the optimal solution in some cases.
  • Stock trading problem: Given a set of historical stock prices, you can make multiple trades, but if you already hold stocks, you cannot buy again before selling, and the goal is to obtain the maximum profit.
  • Huffman coding: Huffman coding is a greedy algorithm used for lossless data compression. By constructing a Huffman tree and always merging the two nodes with the lowest frequency, the resulting Huffman tree has the minimum weighted path length (encoding length).
  • Dijkstra's algorithm: It is a greedy algorithm for solving the shortest path problem from a given source vertex to all other vertices.
","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   Max Capacity Problem","text":"

Question

Input an array \\(ht\\), where each element represents the height of a vertical partition. Any two partitions in the array, along with the space between them, can form a container.

The capacity of the container equals the product of height and width (area), where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions.

Please select two partitions in the array such that the capacity of the formed container is maximized, and return the maximum capacity. An example is shown in Figure 15-7.

Figure 15-7   Example data for the max capacity problem

The container is formed by any two partitions, therefore the state of this problem is the indices of two partitions, denoted as \\([i, j]\\).

According to the problem description, capacity equals height multiplied by width, where height is determined by the shorter partition, and width is the difference in array indices between the two partitions. Let the capacity be \\(cap[i, j]\\), then the calculation formula is:

\\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\]

Let the array length be \\(n\\), then the number of combinations of two partitions (total number of states) is \\(C_n^2 = \\frac{n(n - 1)}{2}\\). Most directly, we can exhaustively enumerate all states to find the maximum capacity, with time complexity \\(O(n^2)\\).

","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#1-greedy-strategy-determination","level":3,"title":"1.   Greedy Strategy Determination","text":"

This problem has a more efficient solution. As shown in Figure 15-8, select a state \\([i, j]\\) where index \\(i < j\\) and height \\(ht[i] < ht[j]\\), meaning \\(i\\) is the short partition and \\(j\\) is the long partition.

Figure 15-8   Initial state

As shown in Figure 15-9, if we now move the long partition \\(j\\) closer to the short partition \\(i\\), the capacity will definitely decrease.

This is because after moving the long partition \\(j\\), the width \\(j-i\\) definitely decreases; and since height is determined by the short partition, the height can only remain unchanged (\\(i\\) is still the short partition) or decrease (the moved \\(j\\) becomes the short partition).

Figure 15-9   State after moving the long partition inward

Conversely, we can only possibly increase capacity by contracting the short partition \\(i\\) inward. Because although width will definitely decrease, height may increase (the moved short partition \\(i\\) may become taller). For example, in Figure 15-10, the area increases after moving the short partition.

Figure 15-10   State after moving the short partition inward

From this we can derive the greedy strategy for this problem: initialize two pointers at both ends of the container, and in each round contract the pointer corresponding to the short partition inward, until the two pointers meet.

Figure 15-11 shows the execution process of the greedy strategy.

  1. In the initial state, pointers \\(i\\) and \\(j\\) are at both ends of the array.
  2. Calculate the capacity of the current state \\(cap[i, j]\\), and update the maximum capacity.
  3. Compare the heights of partition \\(i\\) and partition \\(j\\), and move the short partition inward by one position.
  4. Loop through steps 2. and 3. until \\(i\\) and \\(j\\) meet.
<1><2><3><4><5><6><7><8><9>

Figure 15-11   Greedy process for the max capacity problem

","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

The code loops at most \\(n\\) rounds, therefore the time complexity is \\(O(n)\\).

Variables \\(i\\), \\(j\\), and \\(res\\) use a constant amount of extra space, therefore the space complexity is \\(O(1)\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_capacity.py
def max_capacity(ht: list[int]) -> int:\n    \"\"\"Max capacity: Greedy algorithm\"\"\"\n    # Initialize i, j to be at both ends of the array\n    i, j = 0, len(ht) - 1\n    # Initial max capacity is 0\n    res = 0\n    # Loop for greedy selection until the two boards meet\n    while i < j:\n        # Update max capacity\n        cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        # Move the shorter board inward\n        if ht[i] < ht[j]:\n            i += 1\n        else:\n            j -= 1\n    return res\n
max_capacity.cpp
/* Max capacity: Greedy algorithm */\nint maxCapacity(vector<int> &ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.size() - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = min(ht[i], ht[j]) * (j - i);\n        res = max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.java
/* Max capacity: Greedy algorithm */\nint maxCapacity(int[] ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.length - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.cs
/* Max capacity: Greedy algorithm */\nint MaxCapacity(int[] ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.Length - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = Math.Min(ht[i], ht[j]) * (j - i);\n        res = Math.Max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.go
/* Max capacity: Greedy algorithm */\nfunc maxCapacity(ht []int) int {\n    // Initialize i, j to be at both ends of the array\n    i, j := 0, len(ht)-1\n    // Initial max capacity is 0\n    res := 0\n    // Loop for greedy selection until the two boards meet\n    for i < j {\n        // Update max capacity\n        capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)\n        res = int(math.Max(float64(res), float64(capacity)))\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
max_capacity.swift
/* Max capacity: Greedy algorithm */\nfunc maxCapacity(ht: [Int]) -> Int {\n    // Initialize i, j to be at both ends of the array\n    var i = ht.startIndex, j = ht.endIndex - 1\n    // Initial max capacity is 0\n    var res = 0\n    // Loop for greedy selection until the two boards meet\n    while i < j {\n        // Update max capacity\n        let cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i += 1\n        } else {\n            j -= 1\n        }\n    }\n    return res\n}\n
max_capacity.js
/* Max capacity: Greedy algorithm */\nfunction maxCapacity(ht) {\n    // Initialize i, j to be at both ends of the array\n    let i = 0,\n        j = ht.length - 1;\n    // Initial max capacity is 0\n    let res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        const cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
max_capacity.ts
/* Max capacity: Greedy algorithm */\nfunction maxCapacity(ht: number[]): number {\n    // Initialize i, j to be at both ends of the array\n    let i = 0,\n        j = ht.length - 1;\n    // Initial max capacity is 0\n    let res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        const cap: number = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
max_capacity.dart
/* Max capacity: Greedy algorithm */\nint maxCapacity(List<int> ht) {\n  // Initialize i, j to be at both ends of the array\n  int i = 0, j = ht.length - 1;\n  // Initial max capacity is 0\n  int res = 0;\n  // Loop for greedy selection until the two boards meet\n  while (i < j) {\n    // Update max capacity\n    int cap = min(ht[i], ht[j]) * (j - i);\n    res = max(res, cap);\n    // Move the shorter board inward\n    if (ht[i] < ht[j]) {\n      i++;\n    } else {\n      j--;\n    }\n  }\n  return res;\n}\n
max_capacity.rs
/* Max capacity: Greedy algorithm */\nfn max_capacity(ht: &[i32]) -> i32 {\n    // Initialize i, j to be at both ends of the array\n    let mut i = 0;\n    let mut j = ht.len() - 1;\n    // Initial max capacity is 0\n    let mut res = 0;\n    // Loop for greedy selection until the two boards meet\n    while i < j {\n        // Update max capacity\n        let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;\n        res = std::cmp::max(res, cap);\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    res\n}\n
max_capacity.c
/* Max capacity: Greedy algorithm */\nint maxCapacity(int ht[], int htLength) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0;\n    int j = htLength - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int capacity = myMin(ht[i], ht[j]) * (j - i);\n        res = myMax(res, capacity);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.kt
/* Max capacity: Greedy algorithm */\nfun maxCapacity(ht: IntArray): Int {\n    // Initialize i, j to be at both ends of the array\n    var i = 0\n    var j = ht.size - 1\n    // Initial max capacity is 0\n    var res = 0\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        val cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
max_capacity.rb
### Maximum capacity: greedy ###\ndef max_capacity(ht)\n  # Initialize i, j to be at both ends of the array\n  i, j = 0, ht.length - 1\n  # Initial max capacity is 0\n  res = 0\n\n  # Loop for greedy selection until the two boards meet\n  while i < j\n    # Update max capacity\n    cap = [ht[i], ht[j]].min * (j - i)\n    res = [res, cap].max\n    # Move the shorter board inward\n    if ht[i] < ht[j]\n      i += 1\n    else\n      j -= 1\n    end\n  end\n\n  res\nend\n
","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

The reason greedy is faster than exhaustive enumeration is that each round of greedy selection \"skips\" some states.

For example, in state \\(cap[i, j]\\) where \\(i\\) is the short partition and \\(j\\) is the long partition, if we greedily move the short partition \\(i\\) inward by one position, the states shown in Figure 15-12 will be \"skipped\". This means that the capacities of these states cannot be verified later.

\\[ cap[i, i+1], cap[i, i+2], \\dots, cap[i, j-2], cap[i, j-1] \\]

Figure 15-12   States skipped by moving the short partition

Observing carefully, these skipped states are actually all the states obtained by moving the long partition \\(j\\) inward. We have already proven that moving the long partition inward will definitely decrease capacity. That is, the skipped states cannot possibly be the optimal solution, skipping them will not cause us to miss the optimal solution.

The above analysis shows that the operation of moving the short partition is \"safe\", and the greedy strategy is effective.

","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/","level":1,"title":"15.4   Max Product Cutting Problem","text":"

Question

Given a positive integer \\(n\\), split it into the sum of at least two positive integers, and find the maximum product of all integers after splitting, as shown in Figure 15-13.

Figure 15-13   Problem definition of max product cutting

Suppose we split \\(n\\) into \\(m\\) integer factors, where the \\(i\\)-th factor is denoted as \\(n_i\\), that is

\\[ n = \\sum_{i=1}^{m}n_i \\]

The goal of this problem is to find the maximum product of all integer factors, namely

\\[ \\max(\\prod_{i=1}^{m}n_i) \\]

We need to think about: how large should the splitting count \\(m\\) be, and what should each \\(n_i\\) be?

","path":["Chapter 15. Greedy","15.4   Max Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#1-greedy-strategy-determination","level":3,"title":"1.   Greedy Strategy Determination","text":"

Based on experience, the product of two integers is often greater than their sum. Suppose we split out a factor of \\(2\\) from \\(n\\), then their product is \\(2(n-2)\\). We compare this product with \\(n\\):

\\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\]

As shown in Figure 15-14, when \\(n \\geq 4\\), splitting out a \\(2\\) will increase the product, which indicates that integers greater than or equal to \\(4\\) should all be split.

Greedy strategy one: If the splitting scheme includes factors \\(\\geq 4\\), then they should continue to be split. The final splitting scheme should only contain factors \\(1\\), \\(2\\), and \\(3\\).

Figure 15-14   Splitting causes product to increase

Next, consider which factor is optimal. Among the three factors \\(1\\), \\(2\\), and \\(3\\), clearly \\(1\\) is the worst, because \\(1 \\times (n-1) < n\\) always holds, meaning splitting out \\(1\\) will actually decrease the product.

As shown in Figure 15-15, when \\(n = 6\\), we have \\(3 \\times 3 > 2 \\times 2 \\times 2\\). This means that splitting out \\(3\\) is better than splitting out \\(2\\).

Greedy strategy two: In the splitting scheme, there should be at most two \\(2\\)s. Because three \\(2\\)s can always be replaced by two \\(3\\)s to obtain a larger product.

Figure 15-15   Optimal splitting factor

In summary, the following greedy strategies can be derived.

  1. Input integer \\(n\\), continuously split out factor \\(3\\) until the remainder is \\(0\\), \\(1\\), or \\(2\\).
  2. When the remainder is \\(0\\), it means \\(n\\) is a multiple of \\(3\\), so no further action is needed.
  3. When the remainder is \\(2\\), do not continue splitting, keep it.
  4. When the remainder is \\(1\\), since \\(2 \\times 2 > 1 \\times 3\\), the last \\(3\\) should be replaced with \\(2\\).
","path":["Chapter 15. Greedy","15.4   Max Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

As shown in Figure 15-16, we don't need to use loops to split the integer, but can use integer division to get the count of \\(3\\)s as \\(a\\), and modulo operation to get the remainder as \\(b\\), at which point we have:

\\[ n = 3 a + b \\]

Please note that for the edge case of \\(n \\leq 3\\), a \\(1\\) must be split out, with product \\(1 \\times (n - 1)\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_product_cutting.py
def max_product_cutting(n: int) -> int:\n    \"\"\"Max product cutting: Greedy algorithm\"\"\"\n    # When n <= 3, must cut out a 1\n    if n <= 3:\n        return 1 * (n - 1)\n    # Greedily cut out 3, a is the number of 3s, b is the remainder\n    a, b = n // 3, n % 3\n    if b == 1:\n        # When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return int(math.pow(3, a - 1)) * 2 * 2\n    if b == 2:\n        # When the remainder is 2, do nothing\n        return int(math.pow(3, a)) * 2\n    # When the remainder is 0, do nothing\n    return int(math.pow(3, a))\n
max_product_cutting.cpp
/* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int)pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int)pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int)pow(3, a);\n}\n
max_product_cutting.java
/* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int) Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int) Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int) Math.pow(3, a);\n}\n
max_product_cutting.cs
/* Max product cutting: Greedy algorithm */\nint MaxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int)Math.Pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int)Math.Pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int)Math.Pow(3, a);\n}\n
max_product_cutting.go
/* Max product cutting: Greedy algorithm */\nfunc maxProductCutting(n int) int {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    a := n / 3\n    b := n % 3\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return int(math.Pow(3, float64(a-1))) * 2 * 2\n    }\n    if b == 2 {\n        // When the remainder is 2, do nothing\n        return int(math.Pow(3, float64(a))) * 2\n    }\n    // When the remainder is 0, do nothing\n    return int(math.Pow(3, float64(a)))\n}\n
max_product_cutting.swift
/* Max product cutting: Greedy algorithm */\nfunc maxProductCutting(n: Int) -> Int {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = n / 3\n    let b = n % 3\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return pow(3, a - 1) * 2 * 2\n    }\n    if b == 2 {\n        // When the remainder is 2, do nothing\n        return pow(3, a) * 2\n    }\n    // When the remainder is 0, do nothing\n    return pow(3, a)\n}\n
max_product_cutting.js
/* Max product cutting: Greedy algorithm */\nfunction maxProductCutting(n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = Math.floor(n / 3);\n    let b = n % 3;\n    if (b === 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // When the remainder is 2, do nothing\n        return Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return Math.pow(3, a);\n}\n
max_product_cutting.ts
/* Max product cutting: Greedy algorithm */\nfunction maxProductCutting(n: number): number {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a: number = Math.floor(n / 3);\n    let b: number = n % 3;\n    if (b === 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // When the remainder is 2, do nothing\n        return Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return Math.pow(3, a);\n}\n
max_product_cutting.dart
/* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n  // When n <= 3, must cut out a 1\n  if (n <= 3) {\n    return 1 * (n - 1);\n  }\n  // Greedily cut out 3, a is the number of 3s, b is the remainder\n  int a = n ~/ 3;\n  int b = n % 3;\n  if (b == 1) {\n    // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n    return (pow(3, a - 1) * 2 * 2).toInt();\n  }\n  if (b == 2) {\n    // When the remainder is 2, do nothing\n    return (pow(3, a) * 2).toInt();\n  }\n  // When the remainder is 0, do nothing\n  return pow(3, a).toInt();\n}\n
max_product_cutting.rs
/* Max product cutting: Greedy algorithm */\nfn max_product_cutting(n: i32) -> i32 {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = n / 3;\n    let b = n % 3;\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        3_i32.pow(a as u32 - 1) * 2 * 2\n    } else if b == 2 {\n        // When the remainder is 2, do nothing\n        3_i32.pow(a as u32) * 2\n    } else {\n        // When the remainder is 0, do nothing\n        3_i32.pow(a as u32)\n    }\n}\n
max_product_cutting.c
/* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return pow(3, a);\n}\n
max_product_cutting.kt
/* Max product cutting: Greedy algorithm */\nfun maxProductCutting(n: Int): Int {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    val a = n / 3\n    val b = n % 3\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return 3.0.pow((a - 1)).toInt() * 2 * 2\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return 3.0.pow(a).toInt() * 2 * 2\n    }\n    // When the remainder is 0, do nothing\n    return 3.0.pow(a).toInt()\n}\n
max_product_cutting.rb
### Maximum cutting product: greedy ###\ndef max_product_cutting(n)\n  # When n <= 3, must cut out a 1\n  return 1 * (n - 1) if n <= 3\n  # Greedily cut out 3, a is the number of 3s, b is the remainder\n  a, b = n / 3, n % 3\n  # When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n  return (3.pow(a - 1) * 2 * 2).to_i if b == 1\n  # When the remainder is 2, do nothing\n  return (3.pow(a) * 2).to_i if b == 2\n  # When the remainder is 0, do nothing\n  3.pow(a).to_i\nend\n

Figure 15-16   Calculation method for max product cutting

The time complexity depends on the implementation of the exponentiation operation in the programming language. Taking Python as an example, there are three commonly used power calculation functions.

  • Both the operator ** and the function pow() have time complexity \\(O(\\log⁡ a)\\).
  • The function math.pow() internally calls the C library's pow() function, which performs floating-point exponentiation, with time complexity \\(O(1)\\).

Variables \\(a\\) and \\(b\\) use a constant amount of extra space, therefore the space complexity is \\(O(1)\\).

","path":["Chapter 15. Greedy","15.4   Max Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

Using proof by contradiction, only analyzing the case where \\(n \\geq 4\\).

  1. All factors \\(\\leq 3\\): Suppose the optimal splitting scheme includes a factor \\(x \\geq 4\\), then it can definitely continue to be split into \\(2(x-2)\\) to obtain a larger (or equal) product. This contradicts the assumption.
  2. The splitting scheme does not contain \\(1\\): Suppose the optimal splitting scheme includes a factor of \\(1\\), then it can definitely be merged into another factor to obtain a larger product. This contradicts the assumption.
  3. The splitting scheme contains at most two \\(2\\)s: Suppose the optimal splitting scheme includes three \\(2\\)s, then they can definitely be replaced by two \\(3\\)s for a larger product. This contradicts the assumption.
","path":["Chapter 15. Greedy","15.4   Max Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/summary/","level":1,"title":"15.5   Summary","text":"","path":["Chapter 15. Greedy","15.5   Summary"],"tags":[]},{"location":"chapter_greedy/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Greedy algorithms are typically used to solve optimization problems. The principle is to make locally optimal decisions at each decision stage in hopes of obtaining a globally optimal solution.
  • Greedy algorithms iteratively make one greedy choice after another, transforming the problem into a smaller subproblem in each round, until the problem is solved.
  • Greedy algorithms are not only simple to implement, but also have high problem-solving efficiency. Compared to dynamic programming, greedy algorithms typically have lower time complexity.
  • In the coin change problem, for certain coin combinations, greedy algorithms can guarantee finding the optimal solution; for other coin combinations, however, greedy algorithms may find very poor solutions.
  • Problems suitable for solving with greedy algorithms have two major properties: greedy choice property and optimal substructure. The greedy choice property represents the effectiveness of the greedy strategy.
  • For some complex problems, proving the greedy choice property is not simple. Relatively speaking, disproving it is easier, such as in the coin change problem.
  • Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and correctness proof. Among these, determining the greedy strategy is the core step, and correctness proof is often the difficult point.
  • The fractional knapsack problem, based on the 0-1 knapsack problem, allows selecting a portion of items, and therefore can be solved using greedy algorithms. The correctness of the greedy strategy can be proven using proof by contradiction.
  • The max capacity problem can be solved using exhaustive enumeration with time complexity \\(O(n^2)\\). By designing a greedy strategy to move the short partition inward in each round, the time complexity can be optimized to \\(O(n)\\).
  • In the max product cutting problem, we successively derive two greedy strategies: integers \\(\\geq 4\\) should all continue to be split, and the optimal splitting factor is \\(3\\). The code includes exponentiation operations, and the time complexity depends on the implementation method of exponentiation, typically being \\(O(1)\\) or \\(O(\\log n)\\).
","path":["Chapter 15. Greedy","15.5   Summary"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"Chapter 6.   Hashing","text":"

Abstract

In the world of computing, a hash table is like a clever librarian.

They know how to calculate call numbers, enabling them to quickly locate the target book.

","path":["Chapter 6. Hashing","Chapter 6.   Hashing"],"tags":[]},{"location":"chapter_hashing/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 6.1   Hash Table
  • 6.2   Hash Collision
  • 6.3   Hash Algorithm
  • 6.4   Summary
","path":["Chapter 6. Hashing","Chapter 6.   Hashing"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   Hash Algorithm","text":"

The previous two sections introduced the working principle of hash tables and the methods to handle hash collisions. However, both open addressing and separate chaining can only ensure that the hash table functions normally when hash collisions occur, but cannot reduce the frequency of hash collisions.

If hash collisions occur too frequently, the performance of the hash table will deteriorate drastically. As shown in Figure 6-8, for a separate chaining hash table, in the ideal case, the key-value pairs are evenly distributed across the buckets, achieving optimal query efficiency; in the worst case, all key-value pairs are stored in the same bucket, degrading the time complexity to \\(O(n)\\).

Figure 6-8   Ideal and worst cases of hash collisions

The distribution of key-value pairs is determined by the hash function. Recalling the calculation steps of the hash function, first compute the hash value, then take the modulo by the array length:

index = hash(key) % capacity\n

Observing the above formula, when the hash table capacity capacity is fixed, the hash algorithm hash() determines the output value, thereby determining the distribution of key-value pairs in the hash table.

This means that, to reduce the probability of hash collisions, we should focus on the design of the hash algorithm hash().

","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#631-goals-of-hash-algorithms","level":2,"title":"6.3.1   Goals of Hash Algorithms","text":"

To achieve a \"fast and stable\" hash table data structure, hash algorithms should have the following characteristics:

  • Determinism: For the same input, the hash algorithm should always produce the same output. Only then can the hash table be reliable.
  • High efficiency: The process of computing the hash value should be fast enough. The smaller the computational overhead, the more practical the hash table.
  • Uniform distribution: The hash algorithm should ensure that key-value pairs are evenly distributed in the hash table. The more uniform the distribution, the lower the probability of hash collisions.

In fact, hash algorithms are not only used to implement hash tables but are also widely applied in other fields.

  • Password storage: To protect the security of user passwords, systems usually do not store the plaintext passwords but rather the hash values of the passwords. When a user enters a password, the system calculates the hash value of the input and compares it with the stored hash value. If they match, the password is considered correct.
  • Data integrity check: The data sender can calculate the hash value of the data and send it along; the receiver can recalculate the hash value of the received data and compare it with the received hash value. If they match, the data is considered intact.

For cryptographic applications, to prevent reverse engineering such as deducing the original password from the hash value, hash algorithms need higher-level security features.

  • Unidirectionality: It should be impossible to deduce any information about the input data from the hash value.
  • Collision resistance: It should be extremely difficult to find two different inputs that produce the same hash value.
  • Avalanche effect: Minor changes in the input should lead to significant and unpredictable changes in the output.

Note that \"uniform distribution\" and \"collision resistance\" are two independent concepts. Satisfying uniform distribution does not necessarily mean collision resistance. For example, under random input key, the hash function key % 100 can produce a uniformly distributed output. However, this hash algorithm is too simple, and all key with the same last two digits will have the same output, making it easy to deduce a usable key from the hash value, thereby cracking the password.

","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632-design-of-hash-algorithms","level":2,"title":"6.3.2   Design of Hash Algorithms","text":"

The design of hash algorithms is a complex issue that requires consideration of many factors. However, for some less demanding scenarios, we can also design some simple hash algorithms.

  • Additive hash: Add up the ASCII codes of each character in the input and use the total sum as the hash value.
  • Multiplicative hash: Utilize the non-correlation of multiplication, multiplying each round by a constant, accumulating the ASCII codes of each character into the hash value.
  • XOR hash: Accumulate the hash value by XORing each element of the input data.
  • Rotating hash: Accumulate the ASCII code of each character into a hash value, performing a rotation operation on the hash value before each accumulation.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby simple_hash.py
def add_hash(key: str) -> int:\n    \"\"\"Additive hash\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash += ord(c)\n    return hash % modulus\n\ndef mul_hash(key: str) -> int:\n    \"\"\"Multiplicative hash\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = 31 * hash + ord(c)\n    return hash % modulus\n\ndef xor_hash(key: str) -> int:\n    \"\"\"XOR hash\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash ^= ord(c)\n    return hash % modulus\n\ndef rot_hash(key: str) -> int:\n    \"\"\"Rotational hash\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = (hash << 4) ^ (hash >> 28) ^ ord(c)\n    return hash % modulus\n
simple_hash.cpp
/* Additive hash */\nint addHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Multiplicative hash */\nint mulHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (31 * hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR hash */\nint xorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash ^= (int)c;\n    }\n    return hash & MODULUS;\n}\n\n/* Rotational hash */\nint rotHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n
simple_hash.java
/* Additive hash */\nint addHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* Multiplicative hash */\nint mulHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (31 * hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* XOR hash */\nint xorHash(String key) {\n    int hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash ^= (int) c;\n    }\n    return hash & MODULUS;\n}\n\n/* Rotational hash */\nint rotHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n
simple_hash.cs
/* Additive hash */\nint AddHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Multiplicative hash */\nint MulHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (31 * hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR hash */\nint XorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash ^= c;\n    }\n    return hash & MODULUS;\n}\n\n/* Rotational hash */\nint RotHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS;\n    }\n    return (int)hash;\n}\n
simple_hash.go
/* Additive hash */\nfunc addHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* Multiplicative hash */\nfunc mulHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (31*hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* XOR hash */\nfunc xorHash(key string) int {\n    hash := 0\n    modulus := 1000000007\n    for _, b := range []byte(key) {\n        fmt.Println(int(b))\n        hash ^= int(b)\n        hash = (31*hash + int(b)) % modulus\n    }\n    return hash & modulus\n}\n\n/* Rotational hash */\nfunc rotHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus\n    }\n    return int(hash)\n}\n
simple_hash.swift
/* Additive hash */\nfunc addHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* Multiplicative hash */\nfunc mulHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (31 * hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* XOR hash */\nfunc xorHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash ^= Int(scalar.value)\n        }\n    }\n    return hash & MODULUS\n}\n\n/* Rotational hash */\nfunc rotHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n
simple_hash.js
/* Additive hash */\nfunction addHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* Multiplicative hash */\nfunction mulHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR hash */\nfunction xorHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* Rotational hash */\nfunction rotHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
simple_hash.ts
/* Additive hash */\nfunction addHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* Multiplicative hash */\nfunction mulHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR hash */\nfunction xorHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* Rotational hash */\nfunction rotHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
simple_hash.dart
/* Additive hash */\nint addHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* Multiplicative hash */\nint mulHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (31 * hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* XOR hash */\nint xorHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash ^= key.codeUnitAt(i);\n  }\n  return hash & MODULUS;\n}\n\n/* Rotational hash */\nint rotHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n
simple_hash.rs
/* Additive hash */\nfn add_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* Multiplicative hash */\nfn mul_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (31 * hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* XOR hash */\nfn xor_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash ^= c as i64;\n    }\n\n    (hash & MODULUS) as i32\n}\n\n/* Rotational hash */\nfn rot_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n
simple_hash.c
/* Additive hash */\nint addHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Multiplicative hash */\nint mulHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (31 * hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR hash */\nint xorHash(char *key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n\n    for (int i = 0; i < strlen(key); i++) {\n        hash ^= (unsigned char)key[i];\n    }\n    return hash & MODULUS;\n}\n\n/* Rotational hash */\nint rotHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS;\n    }\n\n    return (int)hash;\n}\n
simple_hash.kt
/* Additive hash */\nfun addHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* Multiplicative hash */\nfun mulHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (31 * hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* XOR hash */\nfun xorHash(key: String): Int {\n    var hash = 0\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = hash xor c.code\n    }\n    return hash and MODULUS\n}\n\n/* Rotational hash */\nfun rotHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS\n    }\n    return hash.toInt()\n}\n
simple_hash.rb
### Additive hash ###\ndef add_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash += c.ord }\n\n  hash % modulus\nend\n\n### Multiplicative hash ###\ndef mul_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = 31 * hash + c.ord }\n\n  hash % modulus\nend\n\n### XOR hash ###\ndef xor_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash ^= c.ord }\n\n  hash % modulus\nend\n\n### Rotational hash ###\ndef rot_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord }\n\n  hash % modulus\nend\n

It is observed that the last step of each hash algorithm is to take the modulus of the large prime number \\(1000000007\\) to ensure that the hash value is within an appropriate range. It is worth pondering why emphasis is placed on modulo a prime number, or what are the disadvantages of modulo a composite number? This is an interesting question.

To conclude: Using a large prime number as the modulus can maximize the uniform distribution of hash values. Since a prime number does not share common factors with other numbers, it can reduce the periodic patterns caused by the modulo operation, thus avoiding hash collisions.

For example, suppose we choose the composite number \\(9\\) as the modulus, which can be divided by \\(3\\), then all key divisible by \\(3\\) will be mapped to hash values \\(0\\), \\(3\\), \\(6\\).

\\[ \\begin{aligned} \\text{modulus} & = 9 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\\dots \\} \\end{aligned} \\]

If the input key happens to have this kind of arithmetic sequence distribution, then the hash values will cluster, thereby exacerbating hash collisions. Now, suppose we replace modulus with the prime number \\(13\\), since there are no common factors between key and modulus, the uniformity of the output hash values will be significantly improved.

\\[ \\begin{aligned} \\text{modulus} & = 13 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \\dots \\} \\end{aligned} \\]

It is worth noting that if the key is guaranteed to be randomly and uniformly distributed, then choosing a prime number or a composite number as the modulus can both produce uniformly distributed hash values. However, when the distribution of key has some periodicity, modulo a composite number is more likely to result in clustering.

In summary, we usually choose a prime number as the modulus, and this prime number should be large enough to eliminate periodic patterns as much as possible, enhancing the robustness of the hash algorithm.

","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#633-common-hash-algorithms","level":2,"title":"6.3.3   Common Hash Algorithms","text":"

It is not hard to see that the simple hash algorithms mentioned above are quite \"fragile\" and far from reaching the design goals of hash algorithms. For example, since addition and XOR obey the commutative law, additive hash and XOR hash cannot distinguish strings with the same content but in different order, which may exacerbate hash collisions and cause security issues.

In practice, we usually use some standard hash algorithms, such as MD5, SHA-1, SHA-2, and SHA-3. They can map input data of any length to a fixed-length hash value.

Over the past century, hash algorithms have been in a continuous process of upgrading and optimization. Some researchers strive to improve the performance of hash algorithms, while others, including hackers, are dedicated to finding security issues in hash algorithms. Table 6-2 shows hash algorithms commonly used in practical applications.

  • MD5 and SHA-1 have been successfully attacked multiple times and are thus abandoned in various security applications.
  • SHA-2 series, especially SHA-256, is one of the most secure hash algorithms to date, with no successful attacks reported, hence commonly used in various security applications and protocols.
  • SHA-3 has lower implementation costs and higher computational efficiency compared to SHA-2, but its current usage coverage is not as extensive as the SHA-2 series.

Table 6-2   Common hash algorithms

MD5 SHA-1 SHA-2 SHA-3 Release Year 1992 1995 2002 2008 Output Length 128 bit 160 bit 256/512 bit 224/256/384/512 bit Hash Collisions Frequent Frequent Rare Rare Security Level Low, has been successfully attacked Low, has been successfully attacked High High Applications Abandoned, still used for data integrity checks Abandoned Cryptocurrency transaction verification, digital signatures, etc. Can be used to replace SHA-2","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#hash-values-in-data-structures","level":1,"title":"Hash Values in Data Structures","text":"

We know that the keys in a hash table can be of various data types such as integers, decimals, or strings. Programming languages usually provide built-in hash algorithms for these data types to calculate the bucket indices in the hash table. Taking Python as an example, we can use the hash() function to compute the hash values for various data types.

  • The hash values of integers and booleans are their own values.
  • The calculation of hash values for floating-point numbers and strings is more complex, and interested readers are encouraged to study this on their own.
  • The hash value of a tuple is a combination of the hash values of each of its elements, resulting in a single hash value.
  • The hash value of an object is generated based on its memory address. By overriding the hash method of an object, hash values can be generated based on content.

Tip

Be aware that the definition and methods of the built-in hash value calculation functions in different programming languages vary.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby built_in_hash.py
num = 3\nhash_num = hash(num)\n# Hash value of integer 3 is 3\n\nbol = True\nhash_bol = hash(bol)\n# Hash value of boolean True is 1\n\ndec = 3.14159\nhash_dec = hash(dec)\n# Hash value of decimal 3.14159 is 326484311674566659\n\nstr = \"Hello 算法\"\nhash_str = hash(str)\n# Hash value of string \"Hello 算法\" is 4617003410720528961\n\ntup = (12836, \"小哈\")\nhash_tup = hash(tup)\n# Hash value of tuple (12836, '小哈') is 1029005403108185979\n\nobj = ListNode(0)\nhash_obj = hash(obj)\n# Hash value of ListNode object at 0x1058fd810 is 274267521\n
built_in_hash.cpp
int num = 3;\nsize_t hashNum = hash<int>()(num);\n// Hash value of integer 3 is 3\n\nbool bol = true;\nsize_t hashBol = hash<bool>()(bol);\n// Hash value of boolean 1 is 1\n\ndouble dec = 3.14159;\nsize_t hashDec = hash<double>()(dec);\n// Hash value of decimal 3.14159 is 4614256650576692846\n\nstring str = \"Hello 算法\";\nsize_t hashStr = hash<string>()(str);\n// Hash value of string \"Hello 算法\" is 15466937326284535026\n\n// In C++, built-in std::hash() only provides hash values for basic data types\n// Hash values for arrays and objects need to be implemented separately\n
built_in_hash.java
int num = 3;\nint hashNum = Integer.hashCode(num);\n// Hash value of integer 3 is 3\n\nboolean bol = true;\nint hashBol = Boolean.hashCode(bol);\n// Hash value of boolean true is 1231\n\ndouble dec = 3.14159;\nint hashDec = Double.hashCode(dec);\n// Hash value of decimal 3.14159 is -1340954729\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode();\n// Hash value of string \"Hello 算法\" is -727081396\n\nObject[] arr = { 12836, \"小哈\" };\nint hashTup = Arrays.hashCode(arr);\n// Hash value of array [12836, 小哈] is 1151158\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode();\n// Hash value of ListNode object utils.ListNode@7dc5e7b4 is 2110121908\n
built_in_hash.cs
int num = 3;\nint hashNum = num.GetHashCode();\n// Hash value of integer 3 is 3;\n\nbool bol = true;\nint hashBol = bol.GetHashCode();\n// Hash value of boolean true is 1;\n\ndouble dec = 3.14159;\nint hashDec = dec.GetHashCode();\n// Hash value of decimal 3.14159 is -1340954729;\n\nstring str = \"Hello 算法\";\nint hashStr = str.GetHashCode();\n// Hash value of string \"Hello 算法\" is -586107568;\n\nobject[] arr = [12836, \"小哈\"];\nint hashTup = arr.GetHashCode();\n// Hash value of array [12836, 小哈] is 42931033;\n\nListNode obj = new(0);\nint hashObj = obj.GetHashCode();\n// Hash value of ListNode object 0 is 39053774;\n
built_in_hash.go
// Go does not provide built-in hash code functions\n
built_in_hash.swift
let num = 3\nlet hashNum = num.hashValue\n// Hash value of integer 3 is 9047044699613009734\n\nlet bol = true\nlet hashBol = bol.hashValue\n// Hash value of boolean true is -4431640247352757451\n\nlet dec = 3.14159\nlet hashDec = dec.hashValue\n// Hash value of decimal 3.14159 is -2465384235396674631\n\nlet str = \"Hello 算法\"\nlet hashStr = str.hashValue\n// Hash value of string \"Hello 算法\" is -7850626797806988787\n\nlet arr = [AnyHashable(12836), AnyHashable(\"小哈\")]\nlet hashTup = arr.hashValue\n// Hash value of array [AnyHashable(12836), AnyHashable(\"小哈\")] is -2308633508154532996\n\nlet obj = ListNode(x: 0)\nlet hashObj = obj.hashValue\n// Hash value of ListNode object utils.ListNode is -2434780518035996159\n
built_in_hash.js
// JavaScript does not provide built-in hash code functions\n
built_in_hash.ts
// TypeScript does not provide built-in hash code functions\n
built_in_hash.dart
int num = 3;\nint hashNum = num.hashCode;\n// Hash value of integer 3 is 34803\n\nbool bol = true;\nint hashBol = bol.hashCode;\n// Hash value of boolean true is 1231\n\ndouble dec = 3.14159;\nint hashDec = dec.hashCode;\n// Hash value of decimal 3.14159 is 2570631074981783\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode;\n// Hash value of string \"Hello 算法\" is 468167534\n\nList arr = [12836, \"小哈\"];\nint hashArr = arr.hashCode;\n// Hash value of array [12836, 小哈] is 976512528\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode;\n// Hash value of ListNode object Instance of 'ListNode' is 1033450432\n
built_in_hash.rs
use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\nlet num = 3;\nlet mut num_hasher = DefaultHasher::new();\nnum.hash(&mut num_hasher);\nlet hash_num = num_hasher.finish();\n// Hash value of integer 3 is 568126464209439262\n\nlet bol = true;\nlet mut bol_hasher = DefaultHasher::new();\nbol.hash(&mut bol_hasher);\nlet hash_bol = bol_hasher.finish();\n// Hash value of boolean true is 4952851536318644461\n\nlet dec: f32 = 3.14159;\nlet mut dec_hasher = DefaultHasher::new();\ndec.to_bits().hash(&mut dec_hasher);\nlet hash_dec = dec_hasher.finish();\n// Hash value of decimal 3.14159 is 2566941990314602357\n\nlet str = \"Hello 算法\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// Hash value of string \"Hello 算法\" is 16092673739211250988\n\nlet arr = (&12836, &\"小哈\");\nlet mut tup_hasher = DefaultHasher::new();\narr.hash(&mut tup_hasher);\nlet hash_tup = tup_hasher.finish();\n// Hash value of tuple (12836, \"小哈\") is 1885128010422702749\n\nlet node = ListNode::new(42);\nlet mut hasher = DefaultHasher::new();\nnode.borrow().val.hash(&mut hasher);\nlet hash = hasher.finish();\n// Hash value of ListNode object RefCell { value: ListNode { val: 42, next: None } } is 15387811073369036852\n
built_in_hash.c
// C does not provide built-in hash code functions\n
built_in_hash.kt
val num = 3\nval hashNum = num.hashCode()\n// Hash value of integer 3 is 3\n\nval bol = true\nval hashBol = bol.hashCode()\n// Hash value of boolean true is 1231\n\nval dec = 3.14159\nval hashDec = dec.hashCode()\n// Hash value of decimal 3.14159 is -1340954729\n\nval str = \"Hello 算法\"\nval hashStr = str.hashCode()\n// Hash value of string \"Hello 算法\" is -727081396\n\nval arr = arrayOf<Any>(12836, \"小哈\")\nval hashTup = arr.hashCode()\n// Hash value of array [12836, 小哈] is 189568618\n\nval obj = ListNode(0)\nval hashObj = obj.hashCode()\n// Hash value of ListNode object utils.ListNode@1d81eb93 is 495053715\n
built_in_hash.rb
num = 3\nhash_num = num.hash\n# Hash value of integer 3 is -4385856518450339636\n\nbol = true\nhash_bol = bol.hash\n# Hash value of boolean true is -1617938112149317027\n\ndec = 3.14159\nhash_dec = dec.hash\n# Hash value of decimal 3.14159 is -1479186995943067893\n\nstr = \"Hello 算法\"\nhash_str = str.hash\n# Hash value of string \"Hello 算法\" is -4075943250025831763\n\ntup = [12836, '小哈']\nhash_tup = tup.hash\n# Hash value of tuple (12836, '小哈') is 1999544809202288822\n\nobj = ListNode.new(0)\nhash_obj = obj.hash\n# Hash value of ListNode object #<ListNode:0x000078133140ab70> is 4302940560806366381\n
Visualized Execution

https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

In many programming languages, only immutable objects can serve as the key in a hash table. If we use a list (dynamic array) as a key, when the contents of the list change, its hash value also changes, and we would no longer be able to find the original value in the hash table.

Although the member variables of a custom object (such as a linked list node) are mutable, it is hashable. This is because the hash value of an object is usually generated based on its memory address, and even if the contents of the object change, the memory address remains the same, so the hash value remains unchanged.

You might have noticed that the hash values output in different consoles are different. This is because the Python interpreter adds a random salt to the string hash function each time it starts up. This approach effectively prevents HashDoS attacks and enhances the security of the hash algorithm.

","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_collision/","level":1,"title":"6.2   Hash Collision","text":"

The previous section mentioned that, in most cases, the input space of a hash function is much larger than the output space, so theoretically, hash collisions are inevitable. For example, if the input space is all integers and the output space is the array capacity size, then multiple integers will inevitably be mapped to the same bucket index.

Hash collisions can lead to incorrect query results, severely impacting the usability of the hash table. To address this issue, whenever a hash collision occurs, we can perform hash table expansion until the collision disappears. This approach is simple, straightforward, and effective, but it is very inefficient because hash table expansion involves a large amount of data migration and hash value recalculation. To improve efficiency, we can adopt the following strategies:

  1. Improve the hash table data structure so that the hash table can function normally when hash collisions occur.
  2. Only expand when necessary, that is, only when hash collisions are severe.

The main methods for improving the structure of hash tables include \"separate chaining\" and \"open addressing\".

","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#621-separate-chaining","level":2,"title":"6.2.1   Separate Chaining","text":"

In the original hash table, each bucket can store only one key-value pair. Separate chaining converts a single element into a linked list, treating key-value pairs as linked list nodes and storing all colliding key-value pairs in the same linked list. Figure 6-5 shows an example of a separate chaining hash table.

Figure 6-5   Separate chaining hash table

The operations of a hash table implemented with separate chaining have changed as follows:

  • Querying elements: Input key, obtain the bucket index through the hash function, then access the head node of the linked list, then traverse the linked list and compare key to find the target key-value pair.
  • Adding elements: First access the linked list head node through the hash function, then append the node (key-value pair) to the linked list.
  • Deleting elements: Access the head of the linked list based on the result of the hash function, then traverse the linked list to find the target node and delete it.

Separate chaining has the following limitations:

  • Increased Space Usage: The linked list contains node pointers, which consume more memory space than arrays.
  • Reduced Query Efficiency: This is because linear traversal of the linked list is required to find the corresponding element.

The code below provides a simple implementation of a separate chaining hash table, with two things to note:

  • Lists (dynamic arrays) are used instead of linked lists to simplify the code. In this setup, the hash table (array) contains multiple buckets, each of which is a list.
  • This implementation includes a hash table expansion method. When the load factor exceeds \\(\\frac{2}{3}\\), we expand the hash table to \\(2\\) times its original size.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_chaining.py
class HashMapChaining:\n    \"\"\"Hash table with separate chaining\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self.size = 0  # Number of key-value pairs\n        self.capacity = 4  # Hash table capacity\n        self.load_thres = 2.0 / 3.0  # Load factor threshold for triggering expansion\n        self.extend_ratio = 2  # Expansion multiplier\n        self.buckets = [[] for _ in range(self.capacity)]  # Bucket array\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Load factor\"\"\"\n        return self.size / self.capacity\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Query operation\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket, if key is found, return corresponding val\n        for pair in bucket:\n            if pair.key == key:\n                return pair.val\n        # If key is not found, return None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Add operation\"\"\"\n        # When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in bucket:\n            if pair.key == key:\n                pair.val = val\n                return\n        # If key does not exist, append key-value pair to the end\n        pair = Pair(key, val)\n        bucket.append(pair)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Remove operation\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket and remove key-value pair from it\n        for pair in bucket:\n            if pair.key == key:\n                bucket.remove(pair)\n                self.size -= 1\n                break\n\n    def extend(self):\n        \"\"\"Expand hash table\"\"\"\n        # Temporarily store the original hash table\n        buckets = self.buckets\n        # Initialize expanded new hash table\n        self.capacity *= self.extend_ratio\n        self.buckets = [[] for _ in range(self.capacity)]\n        self.size = 0\n        # Move key-value pairs from original hash table to new hash table\n        for bucket in buckets:\n            for pair in bucket:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"Print hash table\"\"\"\n        for bucket in self.buckets:\n            res = []\n            for pair in bucket:\n                res.append(str(pair.key) + \" -> \" + pair.val)\n            print(res)\n
hash_map_chaining.cpp
/* Hash table with separate chaining */\nclass HashMapChaining {\n  private:\n    int size;                       // Number of key-value pairs\n    int capacity;                   // Hash table capacity\n    double loadThres;               // Load factor threshold for triggering expansion\n    int extendRatio;                // Expansion multiplier\n    vector<vector<Pair *>> buckets; // Bucket array\n\n  public:\n    /* Constructor */\n    HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {\n        buckets.resize(capacity);\n    }\n\n    /* Destructor */\n    ~HashMapChaining() {\n        for (auto &bucket : buckets) {\n            for (Pair *pair : bucket) {\n                // Free memory\n                delete pair;\n            }\n        }\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double)size / (double)capacity;\n    }\n\n    /* Query operation */\n    string get(int key) {\n        int index = hashFunc(key);\n        // Traverse bucket, if key is found, return corresponding val\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                return pair->val;\n            }\n        }\n        // Return empty string if key not found\n        return \"\";\n    }\n\n    /* Add operation */\n    void put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                pair->val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        buckets[index].push_back(new Pair(key, val));\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        auto &bucket = buckets[index];\n        // Traverse bucket and remove key-value pair from it\n        for (int i = 0; i < bucket.size(); i++) {\n            if (bucket[i]->key == key) {\n                Pair *tmp = bucket[i];\n                bucket.erase(bucket.begin() + i); // Remove key-value pair from it\n                delete tmp;                       // Free memory\n                size--;\n                return;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        vector<vector<Pair *>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets.clear();\n        buckets.resize(capacity);\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (auto &bucket : bucketsTmp) {\n            for (Pair *pair : bucket) {\n                put(pair->key, pair->val);\n                // Free memory\n                delete pair;\n            }\n        }\n    }\n\n    /* Print hash table */\n    void print() {\n        for (auto &bucket : buckets) {\n            cout << \"[\";\n            for (Pair *pair : bucket) {\n                cout << pair->key << \" -> \" << pair->val << \", \";\n            }\n            cout << \"]\\n\";\n        }\n    }\n};\n
hash_map_chaining.java
/* Hash table with separate chaining */\nclass HashMapChaining {\n    int size; // Number of key-value pairs\n    int capacity; // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio; // Expansion multiplier\n    List<List<Pair>> buckets; // Bucket array\n\n    /* Constructor */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Query operation */\n    String get(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket, if key is found, return corresponding val\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    void put(int key, String val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        Pair pair = new Pair(key, val);\n        bucket.add(pair);\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket and remove key-value pair from it\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        List<List<Pair>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (List<Pair> bucket : bucketsTmp) {\n            for (Pair pair : bucket) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    void print() {\n        for (List<Pair> bucket : buckets) {\n            List<String> res = new ArrayList<>();\n            for (Pair pair : bucket) {\n                res.add(pair.key + \" -> \" + pair.val);\n            }\n            System.out.println(res);\n        }\n    }\n}\n
hash_map_chaining.cs
/* Hash table with separate chaining */\nclass HashMapChaining {\n    int size; // Number of key-value pairs\n    int capacity; // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio; // Expansion multiplier\n    List<List<Pair>> buckets; // Bucket array\n\n    /* Constructor */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n    }\n\n    /* Hash function */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Query operation */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        // Traverse bucket, if key is found, return corresponding val\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void Put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        int index = HashFunc(key);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        buckets[index].Add(new Pair(key, val));\n        size++;\n    }\n\n    /* Remove operation */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Traverse bucket and remove key-value pair from it\n        foreach (Pair pair in buckets[index].ToList()) {\n            if (pair.key == key) {\n                buckets[index].Remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    void Extend() {\n        // Temporarily store the original hash table\n        List<List<Pair>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        foreach (List<Pair> bucket in bucketsTmp) {\n            foreach (Pair pair in bucket) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    public void Print() {\n        foreach (List<Pair> bucket in buckets) {\n            List<string> res = [];\n            foreach (Pair pair in bucket) {\n                res.Add(pair.key + \" -> \" + pair.val);\n            }\n            foreach (string kv in res) {\n                Console.WriteLine(kv);\n            }\n        }\n    }\n}\n
hash_map_chaining.go
/* Hash table with separate chaining */\ntype hashMapChaining struct {\n    size        int      // Number of key-value pairs\n    capacity    int      // Hash table capacity\n    loadThres   float64  // Load factor threshold for triggering expansion\n    extendRatio int      // Expansion multiplier\n    buckets     [][]pair // Bucket array\n}\n\n/* Constructor */\nfunc newHashMapChaining() *hashMapChaining {\n    buckets := make([][]pair, 4)\n    for i := 0; i < 4; i++ {\n        buckets[i] = make([]pair, 0)\n    }\n    return &hashMapChaining{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     buckets,\n    }\n}\n\n/* Hash function */\nfunc (m *hashMapChaining) hashFunc(key int) int {\n    return key % m.capacity\n}\n\n/* Load factor */\nfunc (m *hashMapChaining) loadFactor() float64 {\n    return float64(m.size) / float64(m.capacity)\n}\n\n/* Query operation */\nfunc (m *hashMapChaining) get(key int) string {\n    idx := m.hashFunc(key)\n    bucket := m.buckets[idx]\n    // Traverse bucket, if key is found, return corresponding val\n    for _, p := range bucket {\n        if p.key == key {\n            return p.val\n        }\n    }\n    // Return empty string if key not found\n    return \"\"\n}\n\n/* Add operation */\nfunc (m *hashMapChaining) put(key int, val string) {\n    // When load factor exceeds threshold, perform expansion\n    if m.loadFactor() > m.loadThres {\n        m.extend()\n    }\n    idx := m.hashFunc(key)\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\n    for i := range m.buckets[idx] {\n        if m.buckets[idx][i].key == key {\n            m.buckets[idx][i].val = val\n            return\n        }\n    }\n    // If key does not exist, append key-value pair to the end\n    p := pair{\n        key: key,\n        val: val,\n    }\n    m.buckets[idx] = append(m.buckets[idx], p)\n    m.size += 1\n}\n\n/* Remove operation */\nfunc (m *hashMapChaining) remove(key int) {\n    idx := m.hashFunc(key)\n    // Traverse bucket and remove key-value pair from it\n    for i, p := range m.buckets[idx] {\n        if p.key == key {\n            // Slice deletion\n            m.buckets[idx] = append(m.buckets[idx][:i], m.buckets[idx][i+1:]...)\n            m.size -= 1\n            break\n        }\n    }\n}\n\n/* Expand hash table */\nfunc (m *hashMapChaining) extend() {\n    // Temporarily store the original hash table\n    tmpBuckets := make([][]pair, len(m.buckets))\n    for i := 0; i < len(m.buckets); i++ {\n        tmpBuckets[i] = make([]pair, len(m.buckets[i]))\n        copy(tmpBuckets[i], m.buckets[i])\n    }\n    // Initialize expanded new hash table\n    m.capacity *= m.extendRatio\n    m.buckets = make([][]pair, m.capacity)\n    for i := 0; i < m.capacity; i++ {\n        m.buckets[i] = make([]pair, 0)\n    }\n    m.size = 0\n    // Move key-value pairs from original hash table to new hash table\n    for _, bucket := range tmpBuckets {\n        for _, p := range bucket {\n            m.put(p.key, p.val)\n        }\n    }\n}\n\n/* Print hash table */\nfunc (m *hashMapChaining) print() {\n    var builder strings.Builder\n\n    for _, bucket := range m.buckets {\n        builder.WriteString(\"[\")\n        for _, p := range bucket {\n            builder.WriteString(strconv.Itoa(p.key) + \" -> \" + p.val + \" \")\n        }\n        builder.WriteString(\"]\")\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
hash_map_chaining.swift
/* Hash table with separate chaining */\nclass HashMapChaining {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    var loadThres: Double // Load factor threshold for triggering expansion\n    var extendRatio: Int // Expansion multiplier\n    var buckets: [[Pair]] // Bucket array\n\n    /* Constructor */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: [], count: capacity)\n    }\n\n    /* Hash function */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Load factor */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket, if key is found, return corresponding val\n        for pair in bucket {\n            if pair.key == key {\n                return pair.val\n            }\n        }\n        // Return nil if key not found\n        return nil\n    }\n\n    /* Add operation */\n    func put(key: Int, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if loadFactor() > loadThres {\n            extend()\n        }\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in bucket {\n            if pair.key == key {\n                pair.val = val\n                return\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        let pair = Pair(key: key, val: val)\n        buckets[index].append(pair)\n        size += 1\n    }\n\n    /* Remove operation */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket and remove key-value pair from it\n        for (pairIndex, pair) in bucket.enumerated() {\n            if pair.key == key {\n                buckets[index].remove(at: pairIndex)\n                size -= 1\n                break\n            }\n        }\n    }\n\n    /* Expand hash table */\n    func extend() {\n        // Temporarily store the original hash table\n        let bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = Array(repeating: [], count: capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for bucket in bucketsTmp {\n            for pair in bucket {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Print hash table */\n    func print() {\n        for bucket in buckets {\n            let res = bucket.map { \"\\($0.key) -> \\($0.val)\" }\n            Swift.print(res)\n        }\n    }\n}\n
hash_map_chaining.js
/* Hash table with separate chaining */\nclass HashMapChaining {\n    #size; // Number of key-value pairs\n    #capacity; // Hash table capacity\n    #loadThres; // Load factor threshold for triggering expansion\n    #extendRatio; // Expansion multiplier\n    #buckets; // Bucket array\n\n    /* Constructor */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* Hash function */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Query operation */\n    get(key) {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if key is found, return corresponding val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key, val) {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key) {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Traverse bucket and remove key-value pair from it\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    #extend() {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    print() {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
hash_map_chaining.ts
/* Hash table with separate chaining */\nclass HashMapChaining {\n    #size: number; // Number of key-value pairs\n    #capacity: number; // Hash table capacity\n    #loadThres: number; // Load factor threshold for triggering expansion\n    #extendRatio: number; // Expansion multiplier\n    #buckets: Pair[][]; // Bucket array\n\n    /* Constructor */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* Hash function */\n    #hashFunc(key: number): number {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor(): number {\n        return this.#size / this.#capacity;\n    }\n\n    /* Query operation */\n    get(key: number): string | null {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if key is found, return corresponding val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key: number, val: string): void {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key: number): void {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Traverse bucket and remove key-value pair from it\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    #extend(): void {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    print(): void {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
hash_map_chaining.dart
/* Hash table with separate chaining */\nclass HashMapChaining {\n  late int size; // Number of key-value pairs\n  late int capacity; // Hash table capacity\n  late double loadThres; // Load factor threshold for triggering expansion\n  late int extendRatio; // Expansion multiplier\n  late List<List<Pair>> buckets; // Bucket array\n\n  /* Constructor */\n  HashMapChaining() {\n    size = 0;\n    capacity = 4;\n    loadThres = 2.0 / 3.0;\n    extendRatio = 2;\n    buckets = List.generate(capacity, (_) => []);\n  }\n\n  /* Hash function */\n  int hashFunc(int key) {\n    return key % capacity;\n  }\n\n  /* Load factor */\n  double loadFactor() {\n    return size / capacity;\n  }\n\n  /* Query operation */\n  String? get(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket, if key is found, return corresponding val\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        return pair.val;\n      }\n    }\n    // If key is not found, return null\n    return null;\n  }\n\n  /* Add operation */\n  void put(int key, String val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor() > loadThres) {\n      extend();\n    }\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        pair.val = val;\n        return;\n      }\n    }\n    // If key does not exist, append key-value pair to the end\n    Pair pair = Pair(key, val);\n    bucket.add(pair);\n    size++;\n  }\n\n  /* Remove operation */\n  void remove(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket and remove key-value pair from it\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        bucket.remove(pair);\n        size--;\n        break;\n      }\n    }\n  }\n\n  /* Expand hash table */\n  void extend() {\n    // Temporarily store the original hash table\n    List<List<Pair>> bucketsTmp = buckets;\n    // Initialize expanded new hash table\n    capacity *= extendRatio;\n    buckets = List.generate(capacity, (_) => []);\n    size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (List<Pair> bucket in bucketsTmp) {\n      for (Pair pair in bucket) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Print hash table */\n  void printHashMap() {\n    for (List<Pair> bucket in buckets) {\n      List<String> res = [];\n      for (Pair pair in bucket) {\n        res.add(\"${pair.key} -> ${pair.val}\");\n      }\n      print(res);\n    }\n  }\n}\n
hash_map_chaining.rs
/* Hash table with separate chaining */\nstruct HashMapChaining {\n    size: usize,\n    capacity: usize,\n    load_thres: f32,\n    extend_ratio: usize,\n    buckets: Vec<Vec<Pair>>,\n}\n\nimpl HashMapChaining {\n    /* Constructor */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![vec![]; 4],\n        }\n    }\n\n    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % self.capacity\n    }\n\n    /* Load factor */\n    fn load_factor(&self) -> f32 {\n        self.size as f32 / self.capacity as f32\n    }\n\n    /* Remove operation */\n    fn remove(&mut self, key: i32) -> Option<String> {\n        let index = self.hash_func(key);\n\n        // Traverse bucket and remove key-value pair from it\n        for (i, p) in self.buckets[index].iter_mut().enumerate() {\n            if p.key == key {\n                let pair = self.buckets[index].remove(i);\n                self.size -= 1;\n                return Some(pair.val);\n            }\n        }\n\n        // If key is not found, return None\n        None\n    }\n\n    /* Expand hash table */\n    fn extend(&mut self) {\n        // Temporarily store the original hash table\n        let buckets_tmp = std::mem::take(&mut self.buckets);\n\n        // Initialize expanded new hash table\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![Vec::new(); self.capacity as usize];\n        self.size = 0;\n\n        // Move key-value pairs from original hash table to new hash table\n        for bucket in buckets_tmp {\n            for pair in bucket {\n                self.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    fn print(&self) {\n        for bucket in &self.buckets {\n            let mut res = Vec::new();\n            for pair in bucket {\n                res.push(format!(\"{} -> {}\", pair.key, pair.val));\n            }\n            println!(\"{:?}\", res);\n        }\n    }\n\n    /* Add operation */\n    fn put(&mut self, key: i32, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n\n        let index = self.hash_func(key);\n\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in self.buckets[index].iter_mut() {\n            if pair.key == key {\n                pair.val = val;\n                return;\n            }\n        }\n\n        // If key does not exist, append key-value pair to the end\n        let pair = Pair { key, val };\n        self.buckets[index].push(pair);\n        self.size += 1;\n    }\n\n    /* Query operation */\n    fn get(&self, key: i32) -> Option<&str> {\n        let index = self.hash_func(key);\n\n        // Traverse bucket, if key is found, return corresponding val\n        for pair in self.buckets[index].iter() {\n            if pair.key == key {\n                return Some(&pair.val);\n            }\n        }\n\n        // If key is not found, return None\n        None\n    }\n}\n
hash_map_chaining.c
/* Linked list node */\ntypedef struct Node {\n    Pair *pair;\n    struct Node *next;\n} Node;\n\n/* Hash table with separate chaining */\ntypedef struct {\n    int size;         // Number of key-value pairs\n    int capacity;     // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio;  // Expansion multiplier\n    Node **buckets;   // Bucket array\n} HashMapChaining;\n\n/* Constructor */\nHashMapChaining *newHashMapChaining() {\n    HashMapChaining *hashMap = (HashMapChaining *)malloc(sizeof(HashMapChaining));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    return hashMap;\n}\n\n/* Destructor */\nvoid delHashMapChaining(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        while (cur) {\n            Node *tmp = cur;\n            cur = cur->next;\n            free(tmp->pair);\n            free(tmp);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap);\n}\n\n/* Hash function */\nint hashFunc(HashMapChaining *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Load factor */\ndouble loadFactor(HashMapChaining *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Query operation */\nchar *get(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    // Traverse bucket, if key is found, return corresponding val\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            return cur->pair->val;\n        }\n        cur = cur->next;\n    }\n    return \"\"; // Return empty string if key not found\n}\n\n/* Add operation */\nvoid put(HashMapChaining *hashMap, int key, const char *val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    int index = hashFunc(hashMap, key);\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            strcpy(cur->pair->val, val); // If specified key is found, update corresponding val and return\n            return;\n        }\n        cur = cur->next;\n    }\n    // If key not found, add key-value pair to list head\n    Pair *newPair = (Pair *)malloc(sizeof(Pair));\n    newPair->key = key;\n    strcpy(newPair->val, val);\n    Node *newNode = (Node *)malloc(sizeof(Node));\n    newNode->pair = newPair;\n    newNode->next = hashMap->buckets[index];\n    hashMap->buckets[index] = newNode;\n    hashMap->size++;\n}\n\n/* Expand hash table */\nvoid extend(HashMapChaining *hashMap) {\n    // Temporarily store the original hash table\n    int oldCapacity = hashMap->capacity;\n    Node **oldBuckets = hashMap->buckets;\n    // Initialize expanded new hash table\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    hashMap->size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (int i = 0; i < oldCapacity; i++) {\n        Node *cur = oldBuckets[i];\n        while (cur) {\n            put(hashMap, cur->pair->key, cur->pair->val);\n            Node *temp = cur;\n            cur = cur->next;\n            // Free memory\n            free(temp->pair);\n            free(temp);\n        }\n    }\n\n    free(oldBuckets);\n}\n\n/* Remove operation */\nvoid removeItem(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    Node *cur = hashMap->buckets[index];\n    Node *pre = NULL;\n    while (cur) {\n        if (cur->pair->key == key) {\n            // Remove key-value pair from it\n            if (pre) {\n                pre->next = cur->next;\n            } else {\n                hashMap->buckets[index] = cur->next;\n            }\n            // Free memory\n            free(cur->pair);\n            free(cur);\n            hashMap->size--;\n            return;\n        }\n        pre = cur;\n        cur = cur->next;\n    }\n}\n\n/* Print hash table */\nvoid print(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        printf(\"[\");\n        while (cur) {\n            printf(\"%d -> %s, \", cur->pair->key, cur->pair->val);\n            cur = cur->next;\n        }\n        printf(\"]\\n\");\n    }\n}\n
hash_map_chaining.kt
/* Hash table with separate chaining */\nclass HashMapChaining {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    val loadThres: Double // Load factor threshold for triggering expansion\n    val extendRatio: Int // Expansion multiplier\n    var buckets: MutableList<MutableList<Pair>> // Bucket array\n\n    /* Constructor */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n    }\n\n    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Load factor */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Query operation */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket, if key is found, return corresponding val\n        for (pair in bucket) {\n            if (pair.key == key) return pair._val\n        }\n        // If key is not found, return null\n        return null\n    }\n\n    /* Add operation */\n    fun put(key: Int, _val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (pair in bucket) {\n            if (pair.key == key) {\n                pair._val = _val\n                return\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        val pair = Pair(key, _val)\n        bucket.add(pair)\n        size++\n    }\n\n    /* Remove operation */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket and remove key-value pair from it\n        for (pair in bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair)\n                size--\n                break\n            }\n        }\n    }\n\n    /* Expand hash table */\n    fun extend() {\n        // Temporarily store the original hash table\n        val bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        // mutablelist has no fixed size\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for (bucket in bucketsTmp) {\n            for (pair in bucket) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Print hash table */\n    fun print() {\n        for (bucket in buckets) {\n            val res = mutableListOf<String>()\n            for (pair in bucket) {\n                val k = pair.key\n                val v = pair._val\n                res.add(\"$k -> $v\")\n            }\n            println(res)\n        }\n    }\n}\n
hash_map_chaining.rb
### Hash map with chaining ###\nclass HashMapChaining\n  ### Constructor ###\n  def initialize\n    @size = 0 # Number of key-value pairs\n    @capacity = 4 # Hash table capacity\n    @load_thres = 2.0 / 3.0 # Load factor threshold for triggering expansion\n    @extend_ratio = 2 # Expansion multiplier\n    @buckets = Array.new(@capacity) { [] } # Bucket array\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Load factor ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Query operation ###\n  def get(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket, if key is found, return corresponding val\n    for pair in bucket\n      return pair.val if pair.key == key\n    end\n    # Return nil if key not found\n    nil\n  end\n\n  ### Add operation ###\n  def put(key, val)\n    # When load factor exceeds threshold, perform expansion\n    extend if load_factor > @load_thres\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket, if specified key is encountered, update corresponding val and return\n    for pair in bucket\n      if pair.key == key\n        pair.val = val\n        return\n      end\n    end\n    # If key does not exist, append key-value pair to the end\n    pair = Pair.new(key, val)\n    bucket << pair\n    @size += 1\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket and remove key-value pair from it\n    for pair in bucket\n      if pair.key == key\n        bucket.delete(pair)\n        @size -= 1\n        break\n      end\n    end\n  end\n\n  ### Expand hash table ###\n  def extend\n    # Temporarily store original hash table\n    buckets = @buckets\n    # Initialize expanded new hash table\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity) { [] }\n    @size = 0\n    # Move key-value pairs from original hash table to new hash table\n    for bucket in buckets\n      for pair in bucket\n        put(pair.key, pair.val)\n      end\n    end\n  end\n\n  ### Print hash table ###\n  def print\n    for bucket in @buckets\n      res = []\n      for pair in bucket\n        res << \"#{pair.key} -> #{pair.val}\"\n      end\n      pp res\n    end\n  end\nend\n

It's worth noting that when the linked list is very long, the query efficiency \\(O(n)\\) is poor. In this case, the list can be converted to an \"AVL tree\" or \"Red-Black tree\" to optimize the time complexity of the query operation to \\(O(\\log n)\\).

","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#622-open-addressing","level":2,"title":"6.2.2   Open Addressing","text":"

Open addressing does not introduce additional data structures but instead handles hash collisions through \"multiple probes\". The probing methods mainly include linear probing, quadratic probing, and double hashing.

Let's use linear probing as an example to introduce the mechanism of open addressing hash tables.

","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#1-linear-probing","level":3,"title":"1.   Linear Probing","text":"

Linear probing uses a fixed-step linear search for probing, and its operation method differs from ordinary hash tables.

  • Inserting elements: Calculate the bucket index using the hash function. If the bucket already contains an element, linearly traverse forward from the conflict position (usually with a step size of \\(1\\)) until an empty bucket is found, then insert the element.
  • Searching for elements: If a hash collision is encountered, use the same step size to linearly traverse forward until the corresponding element is found and return value; if an empty bucket is encountered, it means the target element is not in the hash table, so return None.

Figure 6-6 shows the distribution of key-value pairs in an open addressing (linear probing) hash table. According to this hash function, keys with the same last two digits will be mapped to the same bucket. Through linear probing, they are stored sequentially in that bucket and the buckets below it.

Figure 6-6   Distribution of key-value pairs in open addressing (linear probing) hash table

However, linear probing is prone to create \"clustering\". Specifically, the longer the continuously occupied positions in the array, the greater the probability of hash collisions occurring in these continuous positions, further promoting clustering growth at that position, forming a vicious cycle, and ultimately leading to degraded efficiency of insertion, deletion, query, and update operations.

It's important to note that we cannot directly delete elements in an open addressing hash table. Deleting an element creates an empty bucket None in the array. When searching for elements, if linear probing encounters this empty bucket, it will return, making the elements below this empty bucket inaccessible. The program may incorrectly assume these elements do not exist, as shown in Figure 6-7.

Figure 6-7   Query issues caused by deletion in open addressing

To solve this problem, we can adopt the lazy deletion mechanism: instead of directly removing elements from the hash table, use a constant TOMBSTONE to mark the bucket. In this mechanism, both None and TOMBSTONE represent empty buckets and can hold key-value pairs. However, when linear probing encounters TOMBSTONE, it should continue traversing since there may still be key-value pairs below it.

However, lazy deletion may accelerate the performance degradation of the hash table. Every deletion operation produces a deletion mark, and as TOMBSTONE increases, the search time will also increase because linear probing may need to skip multiple TOMBSTONE to find the target element.

To address this, consider recording the index of the first encountered TOMBSTONE during linear probing and swapping the searched target element with that TOMBSTONE. The benefit of doing this is that each time an element is queried or added, the element will be moved to a bucket closer to its ideal position (the starting point of probing), thereby optimizing query efficiency.

The code below implements an open addressing (linear probing) hash table with lazy deletion. To make better use of the hash table space, we treat the hash table as a \"circular array\". When going beyond the end of the array, we return to the beginning and continue traversing.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_open_addressing.py
class HashMapOpenAddressing:\n    \"\"\"Hash table with open addressing\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self.size = 0  # Number of key-value pairs\n        self.capacity = 4  # Hash table capacity\n        self.load_thres = 2.0 / 3.0  # Load factor threshold for triggering expansion\n        self.extend_ratio = 2  # Expansion multiplier\n        self.buckets: list[Pair | None] = [None] * self.capacity  # Bucket array\n        self.TOMBSTONE = Pair(-1, \"-1\")  # Removal marker\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Load factor\"\"\"\n        return self.size / self.capacity\n\n    def find_bucket(self, key: int) -> int:\n        \"\"\"Search for bucket index corresponding to key\"\"\"\n        index = self.hash_func(key)\n        first_tombstone = -1\n        # Linear probing, break when encountering an empty bucket\n        while self.buckets[index] is not None:\n            # If key is encountered, return the corresponding bucket index\n            if self.buckets[index].key == key:\n                # If a removal marker was encountered before, move the key-value pair to that index\n                if first_tombstone != -1:\n                    self.buckets[first_tombstone] = self.buckets[index]\n                    self.buckets[index] = self.TOMBSTONE\n                    return first_tombstone  # Return the moved bucket index\n                return index  # Return bucket index\n            # Record the first removal marker encountered\n            if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:\n                first_tombstone = index\n            # Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % self.capacity\n        # If key does not exist, return the index for insertion\n        return index if first_tombstone == -1 else first_tombstone\n\n    def get(self, key: int) -> str:\n        \"\"\"Query operation\"\"\"\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, return corresponding val\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            return self.buckets[index].val\n        # If key-value pair does not exist, return None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Add operation\"\"\"\n        # When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, overwrite val and return\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index].val = val\n            return\n        # If key-value pair does not exist, add the key-value pair\n        self.buckets[index] = Pair(key, val)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Remove operation\"\"\"\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, overwrite it with removal marker\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index] = self.TOMBSTONE\n            self.size -= 1\n\n    def extend(self):\n        \"\"\"Expand hash table\"\"\"\n        # Temporarily store the original hash table\n        buckets_tmp = self.buckets\n        # Initialize expanded new hash table\n        self.capacity *= self.extend_ratio\n        self.buckets = [None] * self.capacity\n        self.size = 0\n        # Move key-value pairs from original hash table to new hash table\n        for pair in buckets_tmp:\n            if pair not in [None, self.TOMBSTONE]:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"Print hash table\"\"\"\n        for pair in self.buckets:\n            if pair is None:\n                print(\"None\")\n            elif pair is self.TOMBSTONE:\n                print(\"TOMBSTONE\")\n            else:\n                print(pair.key, \"->\", pair.val)\n
hash_map_open_addressing.cpp
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n  private:\n    int size;                             // Number of key-value pairs\n    int capacity = 4;                     // Hash table capacity\n    const double loadThres = 2.0 / 3.0;     // Load factor threshold for triggering expansion\n    const int extendRatio = 2;            // Expansion multiplier\n    vector<Pair *> buckets;               // Bucket array\n    Pair *TOMBSTONE = new Pair(-1, \"-1\"); // Removal marker\n\n  public:\n    /* Constructor */\n    HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {\n    }\n\n    /* Destructor */\n    ~HashMapOpenAddressing() {\n        for (Pair *pair : buckets) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                delete pair;\n            }\n        }\n        delete TOMBSTONE;\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != nullptr) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index]->key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    string get(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            return buckets[index]->val;\n        }\n        // Return empty string if key-value pair does not exist\n        return \"\";\n    }\n\n    /* Add operation */\n    void put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            buckets[index]->val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            delete buckets[index];\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        vector<Pair *> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = vector<Pair *>(capacity, nullptr);\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (Pair *pair : bucketsTmp) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                put(pair->key, pair->val);\n                delete pair;\n            }\n        }\n    }\n\n    /* Print hash table */\n    void print() {\n        for (Pair *pair : buckets) {\n            if (pair == nullptr) {\n                cout << \"nullptr\" << endl;\n            } else if (pair == TOMBSTONE) {\n                cout << \"TOMBSTONE\" << endl;\n            } else {\n                cout << pair->key << \" -> \" << pair->val << endl;\n            }\n        }\n    }\n};\n
hash_map_open_addressing.java
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private int size; // Number of key-value pairs\n    private int capacity = 4; // Hash table capacity\n    private final double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n    private final int extendRatio = 2; // Expansion multiplier\n    private Pair[] buckets; // Bucket array\n    private final Pair TOMBSTONE = new Pair(-1, \"-1\"); // Removal marker\n\n    /* Constructor */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Hash function */\n    private int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    private double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    private int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index].key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    public String get(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void put(int key, String val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    public void remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    private void extend() {\n        // Temporarily store the original hash table\n        Pair[] bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (Pair pair : bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    public void print() {\n        for (Pair pair : buckets) {\n            if (pair == null) {\n                System.out.println(\"null\");\n            } else if (pair == TOMBSTONE) {\n                System.out.println(\"TOMBSTONE\");\n            } else {\n                System.out.println(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.cs
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    int size; // Number of key-value pairs\n    int capacity = 4; // Hash table capacity\n    double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n    int extendRatio = 2; // Expansion multiplier\n    Pair[] buckets; // Bucket array\n    Pair TOMBSTONE = new(-1, \"-1\"); // Removal marker\n\n    /* Constructor */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Hash function */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    int FindBucket(int key) {\n        int index = HashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index].key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    public string? Get(int key) {\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void Put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    public void Remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    void Extend() {\n        // Temporarily store the original hash table\n        Pair[] bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        foreach (Pair pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    public void Print() {\n        foreach (Pair pair in buckets) {\n            if (pair == null) {\n                Console.WriteLine(\"null\");\n            } else if (pair == TOMBSTONE) {\n                Console.WriteLine(\"TOMBSTONE\");\n            } else {\n                Console.WriteLine(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.go
/* Hash table with open addressing */\ntype hashMapOpenAddressing struct {\n    size        int     // Number of key-value pairs\n    capacity    int     // Hash table capacity\n    loadThres   float64 // Load factor threshold for triggering expansion\n    extendRatio int     // Expansion multiplier\n    buckets     []*pair // Bucket array\n    TOMBSTONE   *pair   // Removal marker\n}\n\n/* Constructor */\nfunc newHashMapOpenAddressing() *hashMapOpenAddressing {\n    return &hashMapOpenAddressing{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     make([]*pair, 4),\n        TOMBSTONE:   &pair{-1, \"-1\"},\n    }\n}\n\n/* Hash function */\nfunc (h *hashMapOpenAddressing) hashFunc(key int) int {\n    return key % h.capacity // Calculate hash value based on key\n}\n\n/* Load factor */\nfunc (h *hashMapOpenAddressing) loadFactor() float64 {\n    return float64(h.size) / float64(h.capacity) // Calculate current load factor\n}\n\n/* Search for bucket index corresponding to key */\nfunc (h *hashMapOpenAddressing) findBucket(key int) int {\n    index := h.hashFunc(key) // Get initial index\n    firstTombstone := -1     // Record position of first TOMBSTONE encountered\n    for h.buckets[index] != nil {\n        if h.buckets[index].key == key {\n            if firstTombstone != -1 {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                h.buckets[firstTombstone] = h.buckets[index]\n                h.buckets[index] = h.TOMBSTONE\n                return firstTombstone // Return the moved bucket index\n            }\n            return index // Return found index\n        }\n        if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE {\n            firstTombstone = index // Record position of first deletion marker encountered\n        }\n        index = (index + 1) % h.capacity // Linear probing, wrap around to head if past tail\n    }\n    // If key does not exist, return the index for insertion\n    if firstTombstone != -1 {\n        return firstTombstone\n    }\n    return index\n}\n\n/* Query operation */\nfunc (h *hashMapOpenAddressing) get(key int) string {\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        return h.buckets[index].val // If key-value pair is found, return corresponding val\n    }\n    return \"\" // Return \"\" if key-value pair does not exist\n}\n\n/* Add operation */\nfunc (h *hashMapOpenAddressing) put(key int, val string) {\n    if h.loadFactor() > h.loadThres {\n        h.extend() // When load factor exceeds threshold, perform expansion\n    }\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE {\n        h.buckets[index] = &pair{key, val} // If key-value pair does not exist, add the key-value pair\n        h.size++\n    } else {\n        h.buckets[index].val = val // If key-value pair found, overwrite val\n    }\n}\n\n/* Remove operation */\nfunc (h *hashMapOpenAddressing) remove(key int) {\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        h.buckets[index] = h.TOMBSTONE // If key-value pair is found, overwrite it with removal marker\n        h.size--\n    }\n}\n\n/* Expand hash table */\nfunc (h *hashMapOpenAddressing) extend() {\n    oldBuckets := h.buckets               // Temporarily store the original hash table\n    h.capacity *= h.extendRatio           // Update capacity\n    h.buckets = make([]*pair, h.capacity) // Initialize expanded new hash table\n    h.size = 0                            // Reset size\n    // Move key-value pairs from original hash table to new hash table\n    for _, pair := range oldBuckets {\n        if pair != nil && pair != h.TOMBSTONE {\n            h.put(pair.key, pair.val)\n        }\n    }\n}\n\n/* Print hash table */\nfunc (h *hashMapOpenAddressing) print() {\n    for _, pair := range h.buckets {\n        if pair == nil {\n            fmt.Println(\"nil\")\n        } else if pair == h.TOMBSTONE {\n            fmt.Println(\"TOMBSTONE\")\n        } else {\n            fmt.Printf(\"%d -> %s\\n\", pair.key, pair.val)\n        }\n    }\n}\n
hash_map_open_addressing.swift
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    var loadThres: Double // Load factor threshold for triggering expansion\n    var extendRatio: Int // Expansion multiplier\n    var buckets: [Pair?] // Bucket array\n    var TOMBSTONE: Pair // Removal marker\n\n    /* Constructor */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: nil, count: capacity)\n        TOMBSTONE = Pair(key: -1, val: \"-1\")\n    }\n\n    /* Hash function */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Load factor */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Search for bucket index corresponding to key */\n    func findBucket(key: Int) -> Int {\n        var index = hashFunc(key: key)\n        var firstTombstone = -1\n        // Linear probing, break when encountering an empty bucket\n        while buckets[index] != nil {\n            // If key is encountered, return the corresponding bucket index\n            if buckets[index]!.key == key {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if firstTombstone != -1 {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Return the moved bucket index\n                }\n                return index // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if firstTombstone == -1 && buckets[index] == TOMBSTONE {\n                firstTombstone = index\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, return corresponding val\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            return buckets[index]!.val\n        }\n        // If key-value pair does not exist, return null\n        return nil\n    }\n\n    /* Add operation */\n    func put(key: Int, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if loadFactor() > loadThres {\n            extend()\n        }\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, overwrite val and return\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index]!.val = val\n            return\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = Pair(key: key, val: val)\n        size += 1\n    }\n\n    /* Remove operation */\n    func remove(key: Int) {\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, overwrite it with removal marker\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index] = TOMBSTONE\n            size -= 1\n        }\n    }\n\n    /* Expand hash table */\n    func extend() {\n        // Temporarily store the original hash table\n        let bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = Array(repeating: nil, count: capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for pair in bucketsTmp {\n            if let pair, pair != TOMBSTONE {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Print hash table */\n    func print() {\n        for pair in buckets {\n            if pair == nil {\n                Swift.print(\"null\")\n            } else if pair == TOMBSTONE {\n                Swift.print(\"TOMBSTONE\")\n            } else {\n                Swift.print(\"\\(pair!.key) -> \\(pair!.val)\")\n            }\n        }\n    }\n}\n
hash_map_open_addressing.js
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    #size; // Number of key-value pairs\n    #capacity; // Hash table capacity\n    #loadThres; // Load factor threshold for triggering expansion\n    #extendRatio; // Expansion multiplier\n    #buckets; // Bucket array\n    #TOMBSTONE; // Removal marker\n\n    /* Constructor */\n    constructor() {\n        this.#size = 0; // Number of key-value pairs\n        this.#capacity = 4; // Hash table capacity\n        this.#loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n        this.#extendRatio = 2; // Expansion multiplier\n        this.#buckets = Array(this.#capacity).fill(null); // Bucket array\n        this.#TOMBSTONE = new Pair(-1, '-1'); // Removal marker\n    }\n\n    /* Hash function */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    #findBucket(key) {\n        let index = this.#hashFunc(key);\n        let firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (this.#buckets[index] !== null) {\n            // If key is encountered, return the corresponding bucket index\n            if (this.#buckets[index].key === key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone !== -1) {\n                    this.#buckets[firstTombstone] = this.#buckets[index];\n                    this.#buckets[index] = this.#TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (\n                firstTombstone === -1 &&\n                this.#buckets[index] === this.#TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % this.#capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    get(key) {\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            return this.#buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key, val) {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        this.#buckets[index] = new Pair(key, val);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key) {\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index] = this.#TOMBSTONE;\n            this.#size--;\n        }\n    }\n\n    /* Expand hash table */\n    #extend() {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = Array(this.#capacity).fill(null);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.#TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    print() {\n        for (const pair of this.#buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.#TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.ts
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private size: number; // Number of key-value pairs\n    private capacity: number; // Hash table capacity\n    private loadThres: number; // Load factor threshold for triggering expansion\n    private extendRatio: number; // Expansion multiplier\n    private buckets: Array<Pair | null>; // Bucket array\n    private TOMBSTONE: Pair; // Removal marker\n\n    /* Constructor */\n    constructor() {\n        this.size = 0; // Number of key-value pairs\n        this.capacity = 4; // Hash table capacity\n        this.loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n        this.extendRatio = 2; // Expansion multiplier\n        this.buckets = Array(this.capacity).fill(null); // Bucket array\n        this.TOMBSTONE = new Pair(-1, '-1'); // Removal marker\n    }\n\n    /* Hash function */\n    private hashFunc(key: number): number {\n        return key % this.capacity;\n    }\n\n    /* Load factor */\n    private loadFactor(): number {\n        return this.size / this.capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    private findBucket(key: number): number {\n        let index = this.hashFunc(key);\n        let firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (this.buckets[index] !== null) {\n            // If key is encountered, return the corresponding bucket index\n            if (this.buckets[index]!.key === key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone !== -1) {\n                    this.buckets[firstTombstone] = this.buckets[index];\n                    this.buckets[index] = this.TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (\n                firstTombstone === -1 &&\n                this.buckets[index] === this.TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % this.capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    get(key: number): string | null {\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            return this.buckets[index]!.val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key: number, val: string): void {\n        // When load factor exceeds threshold, perform expansion\n        if (this.loadFactor() > this.loadThres) {\n            this.extend();\n        }\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index]!.val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        this.buckets[index] = new Pair(key, val);\n        this.size++;\n    }\n\n    /* Remove operation */\n    remove(key: number): void {\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index] = this.TOMBSTONE;\n            this.size--;\n        }\n    }\n\n    /* Expand hash table */\n    private extend(): void {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.buckets;\n        // Initialize expanded new hash table\n        this.capacity *= this.extendRatio;\n        this.buckets = Array(this.capacity).fill(null);\n        this.size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    print(): void {\n        for (const pair of this.buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.dart
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n  late int _size; // Number of key-value pairs\n  int _capacity = 4; // Hash table capacity\n  double _loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n  int _extendRatio = 2; // Expansion multiplier\n  late List<Pair?> _buckets; // Bucket array\n  Pair _TOMBSTONE = Pair(-1, \"-1\"); // Removal marker\n\n  /* Constructor */\n  HashMapOpenAddressing() {\n    _size = 0;\n    _buckets = List.generate(_capacity, (index) => null);\n  }\n\n  /* Hash function */\n  int hashFunc(int key) {\n    return key % _capacity;\n  }\n\n  /* Load factor */\n  double loadFactor() {\n    return _size / _capacity;\n  }\n\n  /* Search for bucket index corresponding to key */\n  int findBucket(int key) {\n    int index = hashFunc(key);\n    int firstTombstone = -1;\n    // Linear probing, break when encountering an empty bucket\n    while (_buckets[index] != null) {\n      // If key is encountered, return the corresponding bucket index\n      if (_buckets[index]!.key == key) {\n        // If a removal marker was encountered before, move the key-value pair to that index\n        if (firstTombstone != -1) {\n          _buckets[firstTombstone] = _buckets[index];\n          _buckets[index] = _TOMBSTONE;\n          return firstTombstone; // Return the moved bucket index\n        }\n        return index; // Return bucket index\n      }\n      // Record the first removal marker encountered\n      if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) {\n        firstTombstone = index;\n      }\n      // Calculate bucket index, wrap around to the head if past the tail\n      index = (index + 1) % _capacity;\n    }\n    // If key does not exist, return the index for insertion\n    return firstTombstone == -1 ? index : firstTombstone;\n  }\n\n  /* Query operation */\n  String? get(int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, return corresponding val\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      return _buckets[index]!.val;\n    }\n    // If key-value pair does not exist, return null\n    return null;\n  }\n\n  /* Add operation */\n  void put(int key, String val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor() > _loadThres) {\n      extend();\n    }\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, overwrite val and return\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index]!.val = val;\n      return;\n    }\n    // If key-value pair does not exist, add the key-value pair\n    _buckets[index] = new Pair(key, val);\n    _size++;\n  }\n\n  /* Remove operation */\n  void remove(int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, overwrite it with removal marker\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index] = _TOMBSTONE;\n      _size--;\n    }\n  }\n\n  /* Expand hash table */\n  void extend() {\n    // Temporarily store the original hash table\n    List<Pair?> bucketsTmp = _buckets;\n    // Initialize expanded new hash table\n    _capacity *= _extendRatio;\n    _buckets = List.generate(_capacity, (index) => null);\n    _size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (Pair? pair in bucketsTmp) {\n      if (pair != null && pair != _TOMBSTONE) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Print hash table */\n  void printHashMap() {\n    for (Pair? pair in _buckets) {\n      if (pair == null) {\n        print(\"null\");\n      } else if (pair == _TOMBSTONE) {\n        print(\"TOMBSTONE\");\n      } else {\n        print(\"${pair.key} -> ${pair.val}\");\n      }\n    }\n  }\n}\n
hash_map_open_addressing.rs
/* Hash table with open addressing */\nstruct HashMapOpenAddressing {\n    size: usize,                // Number of key-value pairs\n    capacity: usize,            // Hash table capacity\n    load_thres: f64,            // Load factor threshold for triggering expansion\n    extend_ratio: usize,        // Expansion multiplier\n    buckets: Vec<Option<Pair>>, // Bucket array\n    TOMBSTONE: Option<Pair>,    // Removal marker\n}\n\nimpl HashMapOpenAddressing {\n    /* Constructor */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![None; 4],\n            TOMBSTONE: Some(Pair {\n                key: -1,\n                val: \"-1\".to_string(),\n            }),\n        }\n    }\n\n    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        (key % self.capacity as i32) as usize\n    }\n\n    /* Load factor */\n    fn load_factor(&self) -> f64 {\n        self.size as f64 / self.capacity as f64\n    }\n\n    /* Search for bucket index corresponding to key */\n    fn find_bucket(&mut self, key: i32) -> usize {\n        let mut index = self.hash_func(key);\n        let mut first_tombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while self.buckets[index].is_some() {\n            // If key is found, return corresponding bucket index\n            if self.buckets[index].as_ref().unwrap().key == key {\n                // If deletion marker was encountered before, move key-value pair to that index\n                if first_tombstone != -1 {\n                    self.buckets[first_tombstone as usize] = self.buckets[index].take();\n                    self.buckets[index] = self.TOMBSTONE.clone();\n                    return first_tombstone as usize; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE {\n                first_tombstone = index as i32;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % self.capacity;\n        }\n        // If key does not exist, return the index for insertion\n        if first_tombstone == -1 {\n            index\n        } else {\n            first_tombstone as usize\n        }\n    }\n\n    /* Query operation */\n    fn get(&mut self, key: i32) -> Option<&str> {\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, return corresponding val\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            return self.buckets[index].as_ref().map(|pair| &pair.val as &str);\n        }\n        // If key-value pair does not exist, return null\n        None\n    }\n\n    /* Add operation */\n    fn put(&mut self, key: i32, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, overwrite val and return\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index].as_mut().unwrap().val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        self.buckets[index] = Some(Pair { key, val });\n        self.size += 1;\n    }\n\n    /* Remove operation */\n    fn remove(&mut self, key: i32) {\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index] = self.TOMBSTONE.clone();\n            self.size -= 1;\n        }\n    }\n\n    /* Expand hash table */\n    fn extend(&mut self) {\n        // Temporarily store the original hash table\n        let buckets_tmp = self.buckets.clone();\n        // Initialize expanded new hash table\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![None; self.capacity];\n        self.size = 0;\n\n        // Move key-value pairs from original hash table to new hash table\n        for pair in buckets_tmp {\n            if pair.is_none() || pair == self.TOMBSTONE {\n                continue;\n            }\n            let pair = pair.unwrap();\n\n            self.put(pair.key, pair.val);\n        }\n    }\n    /* Print hash table */\n    fn print(&self) {\n        for pair in &self.buckets {\n            if pair.is_none() {\n                println!(\"null\");\n            } else if pair == &self.TOMBSTONE {\n                println!(\"TOMBSTONE\");\n            } else {\n                let pair = pair.as_ref().unwrap();\n                println!(\"{} -> {}\", pair.key, pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.c
/* Hash table with open addressing */\ntypedef struct {\n    int size;         // Number of key-value pairs\n    int capacity;     // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio;  // Expansion multiplier\n    Pair **buckets;   // Bucket array\n    Pair *TOMBSTONE;  // Removal marker\n} HashMapOpenAddressing;\n\n/* Constructor */\nHashMapOpenAddressing *newHashMapOpenAddressing() {\n    HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->TOMBSTONE = (Pair *)malloc(sizeof(Pair));\n    hashMap->TOMBSTONE->key = -1;\n    hashMap->TOMBSTONE->val = \"-1\";\n\n    return hashMap;\n}\n\n/* Destructor */\nvoid delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap->TOMBSTONE);\n    free(hashMap);\n}\n\n/* Hash function */\nint hashFunc(HashMapOpenAddressing *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Load factor */\ndouble loadFactor(HashMapOpenAddressing *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Search for bucket index corresponding to key */\nint findBucket(HashMapOpenAddressing *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    int firstTombstone = -1;\n    // Linear probing, break when encountering an empty bucket\n    while (hashMap->buckets[index] != NULL) {\n        // If key is encountered, return the corresponding bucket index\n        if (hashMap->buckets[index]->key == key) {\n            // If a removal marker was encountered before, move the key-value pair to that index\n            if (firstTombstone != -1) {\n                hashMap->buckets[firstTombstone] = hashMap->buckets[index];\n                hashMap->buckets[index] = hashMap->TOMBSTONE;\n                return firstTombstone; // Return the moved bucket index\n            }\n            return index; // Return bucket index\n        }\n        // Record the first removal marker encountered\n        if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) {\n            firstTombstone = index;\n        }\n        // Calculate bucket index, wrap around to the head if past the tail\n        index = (index + 1) % hashMap->capacity;\n    }\n    // If key does not exist, return the index for insertion\n    return firstTombstone == -1 ? index : firstTombstone;\n}\n\n/* Query operation */\nchar *get(HashMapOpenAddressing *hashMap, int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, return corresponding val\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        return hashMap->buckets[index]->val;\n    }\n    // Return empty string if key-value pair does not exist\n    return \"\";\n}\n\n/* Add operation */\nvoid put(HashMapOpenAddressing *hashMap, int key, char *val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, overwrite val and return\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        free(hashMap->buckets[index]->val);\n        hashMap->buckets[index]->val = (char *)malloc(sizeof(strlen(val) + 1));\n        strcpy(hashMap->buckets[index]->val, val);\n        hashMap->buckets[index]->val[strlen(val)] = '\\0';\n        return;\n    }\n    // If key-value pair does not exist, add the key-value pair\n    Pair *pair = (Pair *)malloc(sizeof(Pair));\n    pair->key = key;\n    pair->val = (char *)malloc(sizeof(strlen(val) + 1));\n    strcpy(pair->val, val);\n    pair->val[strlen(val)] = '\\0';\n\n    hashMap->buckets[index] = pair;\n    hashMap->size++;\n}\n\n/* Remove operation */\nvoid removeItem(HashMapOpenAddressing *hashMap, int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, overwrite it with removal marker\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        Pair *pair = hashMap->buckets[index];\n        free(pair->val);\n        free(pair);\n        hashMap->buckets[index] = hashMap->TOMBSTONE;\n        hashMap->size--;\n    }\n}\n\n/* Expand hash table */\nvoid extend(HashMapOpenAddressing *hashMap) {\n    // Temporarily store the original hash table\n    Pair **bucketsTmp = hashMap->buckets;\n    int oldCapacity = hashMap->capacity;\n    // Initialize expanded new hash table\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (int i = 0; i < oldCapacity; i++) {\n        Pair *pair = bucketsTmp[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            put(hashMap, pair->key, pair->val);\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(bucketsTmp);\n}\n\n/* Print hash table */\nvoid print(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair == NULL) {\n            printf(\"NULL\\n\");\n        } else if (pair == hashMap->TOMBSTONE) {\n            printf(\"TOMBSTONE\\n\");\n        } else {\n            printf(\"%d -> %s\\n\", pair->key, pair->val);\n        }\n    }\n}\n
hash_map_open_addressing.kt
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private var size: Int               // Number of key-value pairs\n    private var capacity: Int           // Hash table capacity\n    private val loadThres: Double       // Load factor threshold for triggering expansion\n    private val extendRatio: Int        // Expansion multiplier\n    private var buckets: Array<Pair?>   // Bucket array\n    private val TOMBSTONE: Pair         // Removal marker\n\n    /* Constructor */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = arrayOfNulls(capacity)\n        TOMBSTONE = Pair(-1, \"-1\")\n    }\n\n    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Load factor */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Search for bucket index corresponding to key */\n    fun findBucket(key: Int): Int {\n        var index = hashFunc(key)\n        var firstTombstone = -1\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index]?.key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Return the moved bucket index\n                }\n                return index // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity\n        }\n        // If key does not exist, return the index for insertion\n        return if (firstTombstone == -1) index else firstTombstone\n    }\n\n    /* Query operation */\n    fun get(key: Int): String? {\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index]?._val\n        }\n        // If key-value pair does not exist, return null\n        return null\n    }\n\n    /* Add operation */\n    fun put(key: Int, _val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index]!!._val = _val\n            return\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = Pair(key, _val)\n        size++\n    }\n\n    /* Remove operation */\n    fun remove(key: Int) {\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE\n            size--\n        }\n    }\n\n    /* Expand hash table */\n    fun extend() {\n        // Temporarily store the original hash table\n        val bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = arrayOfNulls(capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for (pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Print hash table */\n    fun print() {\n        for (pair in buckets) {\n            if (pair == null) {\n                println(\"null\")\n            } else if (pair == TOMBSTONE) {\n                println(\"TOMESTOME\")\n            } else {\n                println(\"${pair.key} -> ${pair._val}\")\n            }\n        }\n    }\n}\n
hash_map_open_addressing.rb
### Hash map with open addressing ###\nclass HashMapOpenAddressing\n  TOMBSTONE = Pair.new(-1, '-1') # Removal marker\n\n  ### Constructor ###\n  def initialize\n    @size = 0 # Number of key-value pairs\n    @capacity = 4 # Hash table capacity\n    @load_thres = 2.0 / 3.0 # Load factor threshold for triggering expansion\n    @extend_ratio = 2 # Expansion multiplier\n    @buckets = Array.new(@capacity) # Bucket array\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Load factor ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Search bucket index for key ###\n  def find_bucket(key)\n    index = hash_func(key)\n    first_tombstone = -1\n    # Linear probing, break when encountering an empty bucket\n    while !@buckets[index].nil?\n      # If key is encountered, return the corresponding bucket index\n      if @buckets[index].key == key\n        # If a removal marker was encountered before, move the key-value pair to that index\n        if first_tombstone != -1\n          @buckets[first_tombstone] = @buckets[index]\n          @buckets[index] = TOMBSTONE\n          return first_tombstone # Return the moved bucket index\n        end\n        return index # Return bucket index\n      end\n      # Record the first removal marker encountered\n      first_tombstone = index if first_tombstone == -1 && @buckets[index] == TOMBSTONE\n      # Calculate bucket index, wrap around to the head if past the tail\n      index = (index + 1) % @capacity\n    end\n    # If key does not exist, return the index for insertion\n    first_tombstone == -1 ? index : first_tombstone\n  end\n\n  ### Query operation ###\n  def get(key)\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair is found, return corresponding val\n    return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index])\n    # Return nil if key-value pair does not exist\n    nil\n  end\n\n  ### Add operation ###\n  def put(key, val)\n    # When load factor exceeds threshold, perform expansion\n    extend if load_factor > @load_thres\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair found, overwrite val and return\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index].val = val\n      return\n    end\n    # If key-value pair does not exist, add the key-value pair\n    @buckets[index] = Pair.new(key, val)\n    @size += 1\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair is found, overwrite it with removal marker\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index] = TOMBSTONE\n      @size -= 1\n    end\n  end\n\n  ### Expand hash table ###\n  def extend\n    # Temporarily store the original hash table\n    buckets_tmp = @buckets\n    # Initialize expanded new hash table\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity)\n    @size = 0\n    # Move key-value pairs from original hash table to new hash table\n    for pair in buckets_tmp\n      put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair)\n    end\n  end\n\n  ### Print hash table ###\n  def print\n    for pair in @buckets\n      if pair.nil?\n        puts \"Nil\"\n      elsif pair == TOMBSTONE\n        puts \"TOMBSTONE\"\n      else\n        puts \"#{pair.key} -> #{pair.val}\"\n      end\n    end\n  end\nend\n
","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#2-quadratic-probing","level":3,"title":"2.   Quadratic Probing","text":"

Quadratic probing is similar to linear probing and is one of the common strategies for open addressing. When a collision occurs, quadratic probing does not simply skip a fixed number of steps but skips a number of steps equal to the \"square of the number of probes\", i.e., \\(1, 4, 9, \\dots\\) steps.

Quadratic probing has the following advantages:

  • Quadratic probing attempts to alleviate the clustering effect of linear probing by skipping distances equal to the square of the probe count.
  • Quadratic probing skips larger distances to find empty positions, which helps to distribute data more evenly.

However, quadratic probing is not perfect:

  • Clustering still exists, i.e., some positions are more likely to be occupied than others.
  • Due to the growth of squares, quadratic probing may not probe the entire hash table, meaning that even if there are empty buckets in the hash table, quadratic probing may not be able to access them.
","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#3-double-hashing","level":3,"title":"3.   Double Hashing","text":"

As the name suggests, the double hashing method uses multiple hash functions \\(f_1(x)\\), \\(f_2(x)\\), \\(f_3(x)\\), \\(\\dots\\) for probing.

  • Inserting elements: If hash function \\(f_1(x)\\) encounters a conflict, try \\(f_2(x)\\), and so on, until an empty position is found and the element is inserted.
  • Searching for elements: Search in the same order of hash functions until the target element is found and return it; if an empty position is encountered or all hash functions have been tried, it indicates the element is not in the hash table, then return None.

Compared to linear probing, the double hashing method is less prone to clustering, but multiple hash functions introduce additional computational overhead.

Tip

Please note that open addressing (linear probing, quadratic probing, and double hashing) hash tables all have the problem of \"cannot directly delete elements\".

","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#623-choice-of-programming-languages","level":2,"title":"6.2.3   Choice of Programming Languages","text":"

Different programming languages adopt different hash table implementation strategies. Here are a few examples:

  • Python uses open addressing. The dict dictionary uses pseudo-random numbers for probing.
  • Java uses separate chaining. Since JDK 1.8, when the array length in HashMap reaches 64 and the length of a linked list reaches 8, the linked list is converted to a red-black tree to improve search performance.
  • Go uses separate chaining. Go stipulates that each bucket can store up to 8 key-value pairs, and if the capacity is exceeded, an overflow bucket is linked; when there are too many overflow buckets, a special equal-capacity expansion operation is performed to ensure performance.
","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   Hash Table","text":"

A hash table, also known as a hash map, establishes a mapping between keys key and values value, enabling efficient element retrieval. Specifically, when we input a key key into a hash table, we can retrieve the corresponding value value in \\(O(1)\\) time.

As shown in Figure 6-1, given \\(n\\) students, each with two pieces of data: \"name\" and \"student ID\". If we want to implement a query function that \"inputs a student ID and returns the corresponding name\", we can use the hash table shown below.

Figure 6-1   Abstract representation of a hash table

In addition to hash tables, arrays and linked lists can also implement query functionality. Their efficiency comparison is shown in the following table.

  • Adding elements: Simply add elements to the end of the array (linked list), using \\(O(1)\\) time.
  • Querying elements: Since the array (linked list) is unordered, all elements need to be traversed, using \\(O(n)\\) time.
  • Deleting elements: The element must first be located, then deleted from the array (linked list), using \\(O(n)\\) time.

Table 6-1   Comparison of element query efficiency

Array Linked List Hash Table Find element \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) Add element \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Delete element \\(O(n)\\) \\(O(n)\\) \\(O(1)\\)

As observed, the time complexity for insertion, deletion, search, and modification operations in a hash table is \\(O(1)\\), which is very efficient.

","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#611-common-hash-table-operations","level":2,"title":"6.1.1   Common Hash Table Operations","text":"

Common operations on hash tables include: initialization, query operations, adding key-value pairs, and deleting key-value pairs. Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
# Initialize hash table\nhmap: dict = {}\n\n# Add operation\n# Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n# Query operation\n# Input key into hash table to get value\nname: str = hmap[15937]\n\n# Delete operation\n# Delete key-value pair (key, value) from hash table\nhmap.pop(10583)\n
hash_map.cpp
/* Initialize hash table */\nunordered_map<int, string> map;\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\";\nmap[15937] = \"XiaoLuo\";\nmap[16750] = \"XiaoSuan\";\nmap[13276] = \"XiaoFa\";\nmap[10583] = \"XiaoYa\";\n\n/* Query operation */\n// Input key into hash table to get value\nstring name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.erase(10583);\n
hash_map.java
/* Initialize hash table */\nMap<Integer, String> map = new HashMap<>();\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.put(12836, \"XiaoHa\");\nmap.put(15937, \"XiaoLuo\");\nmap.put(16750, \"XiaoSuan\");\nmap.put(13276, \"XiaoFa\");\nmap.put(10583, \"XiaoYa\");\n\n/* Query operation */\n// Input key into hash table to get value\nString name = map.get(15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583);\n
hash_map.cs
/* Initialize hash table */\nDictionary<int, string> map = new() {\n    /* Add operation */\n    // Add key-value pair (key, value) to hash table\n    { 12836, \"XiaoHa\" },\n    { 15937, \"XiaoLuo\" },\n    { 16750, \"XiaoSuan\" },\n    { 13276, \"XiaoFa\" },\n    { 10583, \"XiaoYa\" }\n};\n\n/* Query operation */\n// Input key into hash table to get value\nstring name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.Remove(10583);\n
hash_map_test.go
/* Initialize hash table */\nhmap := make(map[int]string)\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nname := hmap[15937]\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\ndelete(hmap, 10583)\n
hash_map.swift
/* Initialize hash table */\nvar map: [Int: String] = [:]\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\"\nmap[15937] = \"XiaoLuo\"\nmap[16750] = \"XiaoSuan\"\nmap[13276] = \"XiaoFa\"\nmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map[15937]!\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.removeValue(forKey: 10583)\n
hash_map.js
/* Initialize hash table */\nconst map = new Map();\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.set(12836, 'XiaoHa');\nmap.set(15937, 'XiaoLuo');\nmap.set(16750, 'XiaoSuan');\nmap.set(13276, 'XiaoFa');\nmap.set(10583, 'XiaoYa');\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map.get(15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.delete(10583);\n
hash_map.ts
/* Initialize hash table */\nconst map = new Map<number, string>();\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.set(12836, 'XiaoHa');\nmap.set(15937, 'XiaoLuo');\nmap.set(16750, 'XiaoSuan');\nmap.set(13276, 'XiaoFa');\nmap.set(10583, 'XiaoYa');\nconsole.info('\\nAfter adding, hash table is\\nKey -> Value');\nconsole.info(map);\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map.get(15937);\nconsole.info('\\nInput student ID 15937, queried name ' + name);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.delete(10583);\nconsole.info('\\nAfter deleting 10583, hash table is\\nKey -> Value');\nconsole.info(map);\n
hash_map.dart
/* Initialize hash table */\nMap<int, String> map = {};\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\";\nmap[15937] = \"XiaoLuo\";\nmap[16750] = \"XiaoSuan\";\nmap[13276] = \"XiaoFa\";\nmap[10583] = \"XiaoYa\";\n\n/* Query operation */\n// Input key into hash table to get value\nString name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583);\n
hash_map.rs
use std::collections::HashMap;\n\n/* Initialize hash table */\nlet mut map: HashMap<i32, String> = HashMap::new();\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.insert(12836, \"XiaoHa\".to_string());\nmap.insert(15937, \"XiaoLuo\".to_string());\nmap.insert(16750, \"XiaoSuan\".to_string());\nmap.insert(13279, \"XiaoFa\".to_string());\nmap.insert(10583, \"XiaoYa\".to_string());\n\n/* Query operation */\n// Input key into hash table to get value\nlet _name: Option<&String> = map.get(&15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nlet _removed_value: Option<String> = map.remove(&10583);\n
hash_map.c
// C does not provide a built-in hash table\n
hash_map.kt
/* Initialize hash table */\nval map = HashMap<Int,String>()\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\"\nmap[15937] = \"XiaoLuo\"\nmap[16750] = \"XiaoSuan\"\nmap[13276] = \"XiaoFa\"\nmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nval name = map[15937]\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583)\n
hash_map.rb
# Initialize hash table\nhmap = {}\n\n# Add operation\n# Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n# Query operation\n# Input key into hash table to get value\nname = hmap[15937]\n\n# Delete operation\n# Delete key-value pair (key, value) from hash table\nhmap.delete(10583)\n
Visualized Execution

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

There are three common ways to traverse a hash table: traversing key-value pairs, traversing keys, and traversing values. Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
# Traverse hash table\n# Traverse key-value pairs key->value\nfor key, value in hmap.items():\n    print(key, \"->\", value)\n# Traverse keys only\nfor key in hmap.keys():\n    print(key)\n# Traverse values only\nfor value in hmap.values():\n    print(value)\n
hash_map.cpp
/* Traverse hash table */\n// Traverse key-value pairs key->value\nfor (auto kv: map) {\n    cout << kv.first << \" -> \" << kv.second << endl;\n}\n// Traverse using iterator key->value\nfor (auto iter = map.begin(); iter != map.end(); iter++) {\n    cout << iter->first << \"->\" << iter->second << endl;\n}\n
hash_map.java
/* Traverse hash table */\n// Traverse key-value pairs key->value\nfor (Map.Entry<Integer, String> kv: map.entrySet()) {\n    System.out.println(kv.getKey() + \" -> \" + kv.getValue());\n}\n// Traverse keys only\nfor (int key: map.keySet()) {\n    System.out.println(key);\n}\n// Traverse values only\nfor (String val: map.values()) {\n    System.out.println(val);\n}\n
hash_map.cs
/* Traverse hash table */\n// Traverse key-value pairs Key->Value\nforeach (var kv in map) {\n    Console.WriteLine(kv.Key + \" -> \" + kv.Value);\n}\n// Traverse keys only\nforeach (int key in map.Keys) {\n    Console.WriteLine(key);\n}\n// Traverse values only\nforeach (string val in map.Values) {\n    Console.WriteLine(val);\n}\n
hash_map_test.go
/* Traverse hash table */\n// Traverse key-value pairs key->value\nfor key, value := range hmap {\n    fmt.Println(key, \"->\", value)\n}\n// Traverse keys only\nfor key := range hmap {\n    fmt.Println(key)\n}\n// Traverse values only\nfor _, value := range hmap {\n    fmt.Println(value)\n}\n
hash_map.swift
/* Traverse hash table */\n// Traverse key-value pairs Key->Value\nfor (key, value) in map {\n    print(\"\\(key) -> \\(value)\")\n}\n// Traverse keys only\nfor key in map.keys {\n    print(key)\n}\n// Traverse values only\nfor value in map.values {\n    print(value)\n}\n
hash_map.js
/* Traverse hash table */\nconsole.info('\\nTraverse key-value pairs Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nTraverse keys only Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nTraverse values only Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
hash_map.ts
/* Traverse hash table */\nconsole.info('\\nTraverse key-value pairs Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nTraverse keys only Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nTraverse values only Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
hash_map.dart
/* Traverse hash table */\n// Traverse key-value pairs Key->Value\nmap.forEach((key, value) {\n  print('$key -> $value');\n});\n\n// Traverse keys only\nmap.keys.forEach((key) {\n  print(key);\n});\n\n// Traverse values only\nmap.values.forEach((value) {\n  print(value);\n});\n
hash_map.rs
/* Traverse hash table */\n// Traverse key-value pairs Key->Value\nfor (key, value) in &map {\n    println!(\"{key} -> {value}\");\n}\n\n// Traverse keys only\nfor key in map.keys() {\n    println!(\"{key}\");\n}\n\n// Traverse values only\nfor value in map.values() {\n    println!(\"{value}\");\n}\n
hash_map.c
// C does not provide a built-in hash table\n
hash_map.kt
/* Traverse hash table */\n// Traverse key-value pairs key->value\nfor ((key, value) in map) {\n    println(\"$key -> $value\")\n}\n// Traverse keys only\nfor (key in map.keys) {\n    println(key)\n}\n// Traverse values only\nfor (_val in map.values) {\n    println(_val)\n}\n
hash_map.rb
# Traverse hash table\n# Traverse key-value pairs key->value\nhmap.entries.each { |key, value| puts \"#{key} -> #{value}\" }\n\n# Traverse keys only\nhmap.keys.each { |key| puts key }\n\n# Traverse values only\nhmap.values.each { |val| puts val }\n
Visualized Execution

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#612-simple-hash-table-implementation","level":2,"title":"6.1.2   Simple Hash Table Implementation","text":"

Let's first consider the simplest case: implementing a hash table using only an array. In a hash table, each empty position in the array is called a bucket, and each bucket can store a key-value pair. Therefore, the query operation is to find the bucket corresponding to key and retrieve the value from the bucket.

So how do we locate the corresponding bucket based on key? This is achieved through a hash function. The role of the hash function is to map a larger input space to a smaller output space. In a hash table, the input space is all keys, and the output space is all buckets (array indices). In other words, given a key, we can use the hash function to obtain the storage location of the key-value pair corresponding to that key in the array.

When inputting a key, the hash function's calculation process consists of the following two steps:

  1. Calculate the hash value through a hash algorithm hash().
  2. Take the modulo of the hash value by the number of buckets (array length) capacity to obtain the bucket (array index) index corresponding to that key.
index = hash(key) % capacity\n

Subsequently, we can use index to access the corresponding bucket in the hash table and retrieve the value.

Assuming the array length is capacity = 100 and the hash algorithm is hash(key) = key, the hash function becomes key % 100. Figure 6-2 shows the working principle of the hash function using key as student ID and value as name.

Figure 6-2   Working principle of hash function

The following code implements a simple hash table. Here, we encapsulate key and value into a class Pair to represent a key-value pair.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_hash_map.py
class Pair:\n    \"\"\"Key-value pair\"\"\"\n\n    def __init__(self, key: int, val: str):\n        self.key = key\n        self.val = val\n\nclass ArrayHashMap:\n    \"\"\"Hash table based on array implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        # Initialize array with 100 buckets\n        self.buckets: list[Pair | None] = [None] * 100\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        index = key % 100\n        return index\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Query operation\"\"\"\n        index: int = self.hash_func(key)\n        pair: Pair = self.buckets[index]\n        if pair is None:\n            return None\n        return pair.val\n\n    def put(self, key: int, val: str):\n        \"\"\"Add and update operation\"\"\"\n        pair = Pair(key, val)\n        index: int = self.hash_func(key)\n        self.buckets[index] = pair\n\n    def remove(self, key: int):\n        \"\"\"Remove operation\"\"\"\n        index: int = self.hash_func(key)\n        # Set to None to represent removal\n        self.buckets[index] = None\n\n    def entry_set(self) -> list[Pair]:\n        \"\"\"Get all key-value pairs\"\"\"\n        result: list[Pair] = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair)\n        return result\n\n    def key_set(self) -> list[int]:\n        \"\"\"Get all keys\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.key)\n        return result\n\n    def value_set(self) -> list[str]:\n        \"\"\"Get all values\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.val)\n        return result\n\n    def print(self):\n        \"\"\"Print hash table\"\"\"\n        for pair in self.buckets:\n            if pair is not None:\n                print(pair.key, \"->\", pair.val)\n
array_hash_map.cpp
/* Key-value pair */\nstruct Pair {\n  public:\n    int key;\n    string val;\n    Pair(int key, string val) {\n        this->key = key;\n        this->val = val;\n    }\n};\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n  private:\n    vector<Pair *> buckets;\n\n  public:\n    ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = vector<Pair *>(100);\n    }\n\n    ~ArrayHashMap() {\n        // Free memory\n        for (const auto &bucket : buckets) {\n            delete bucket;\n        }\n        buckets.clear();\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\n    string get(int key) {\n        int index = hashFunc(key);\n        Pair *pair = buckets[index];\n        if (pair == nullptr)\n            return \"\";\n        return pair->val;\n    }\n\n    /* Add operation */\n    void put(int key, string val) {\n        Pair *pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        // Free memory and set to nullptr\n        delete buckets[index];\n        buckets[index] = nullptr;\n    }\n\n    /* Get all key-value pairs */\n    vector<Pair *> pairSet() {\n        vector<Pair *> pairSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                pairSet.push_back(pair);\n            }\n        }\n        return pairSet;\n    }\n\n    /* Get all keys */\n    vector<int> keySet() {\n        vector<int> keySet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                keySet.push_back(pair->key);\n            }\n        }\n        return keySet;\n    }\n\n    /* Get all values */\n    vector<string> valueSet() {\n        vector<string> valueSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                valueSet.push_back(pair->val);\n            }\n        }\n        return valueSet;\n    }\n\n    /* Print hash table */\n    void print() {\n        for (Pair *kv : pairSet()) {\n            cout << kv->key << \" -> \" << kv->val << endl;\n        }\n    }\n};\n
array_hash_map.java
/* Key-value pair */\nclass Pair {\n    public int key;\n    public String val;\n\n    public Pair(int key, String val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private List<Pair> buckets;\n\n    public ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = new ArrayList<>();\n        for (int i = 0; i < 100; i++) {\n            buckets.add(null);\n        }\n    }\n\n    /* Hash function */\n    private int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\n    public String get(int key) {\n        int index = hashFunc(key);\n        Pair pair = buckets.get(index);\n        if (pair == null)\n            return null;\n        return pair.val;\n    }\n\n    /* Add operation */\n    public void put(int key, String val) {\n        Pair pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets.set(index, pair);\n    }\n\n    /* Remove operation */\n    public void remove(int key) {\n        int index = hashFunc(key);\n        // Set to null to represent deletion\n        buckets.set(index, null);\n    }\n\n    /* Get all key-value pairs */\n    public List<Pair> pairSet() {\n        List<Pair> pairSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                pairSet.add(pair);\n        }\n        return pairSet;\n    }\n\n    /* Get all keys */\n    public List<Integer> keySet() {\n        List<Integer> keySet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                keySet.add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* Get all values */\n    public List<String> valueSet() {\n        List<String> valueSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                valueSet.add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* Print hash table */\n    public void print() {\n        for (Pair kv : pairSet()) {\n            System.out.println(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
array_hash_map.cs
/* Key-value pair int->string */\nclass Pair(int key, string val) {\n    public int key = key;\n    public string val = val;\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    List<Pair?> buckets;\n    public ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = [];\n        for (int i = 0; i < 100; i++) {\n            buckets.Add(null);\n        }\n    }\n\n    /* Hash function */\n    int HashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        Pair? pair = buckets[index];\n        if (pair == null) return null;\n        return pair.val;\n    }\n\n    /* Add operation */\n    public void Put(int key, string val) {\n        Pair pair = new(key, val);\n        int index = HashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* Remove operation */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Set to null to represent deletion\n        buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\n    public List<Pair> PairSet() {\n        List<Pair> pairSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                pairSet.Add(pair);\n        }\n        return pairSet;\n    }\n\n    /* Get all keys */\n    public List<int> KeySet() {\n        List<int> keySet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                keySet.Add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* Get all values */\n    public List<string> ValueSet() {\n        List<string> valueSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                valueSet.Add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* Print hash table */\n    public void Print() {\n        foreach (Pair kv in PairSet()) {\n            Console.WriteLine(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
array_hash_map.go
/* Key-value pair */\ntype pair struct {\n    key int\n    val string\n}\n\n/* Hash table based on array implementation */\ntype arrayHashMap struct {\n    buckets []*pair\n}\n\n/* Initialize hash table */\nfunc newArrayHashMap() *arrayHashMap {\n    // Initialize array with 100 buckets\n    buckets := make([]*pair, 100)\n    return &arrayHashMap{buckets: buckets}\n}\n\n/* Hash function */\nfunc (a *arrayHashMap) hashFunc(key int) int {\n    index := key % 100\n    return index\n}\n\n/* Query operation */\nfunc (a *arrayHashMap) get(key int) string {\n    index := a.hashFunc(key)\n    pair := a.buckets[index]\n    if pair == nil {\n        return \"Not Found\"\n    }\n    return pair.val\n}\n\n/* Add operation */\nfunc (a *arrayHashMap) put(key int, val string) {\n    pair := &pair{key: key, val: val}\n    index := a.hashFunc(key)\n    a.buckets[index] = pair\n}\n\n/* Remove operation */\nfunc (a *arrayHashMap) remove(key int) {\n    index := a.hashFunc(key)\n    // Set to nil to delete\n    a.buckets[index] = nil\n}\n\n/* Get all key pairs */\nfunc (a *arrayHashMap) pairSet() []*pair {\n    var pairs []*pair\n    for _, pair := range a.buckets {\n        if pair != nil {\n            pairs = append(pairs, pair)\n        }\n    }\n    return pairs\n}\n\n/* Get all keys */\nfunc (a *arrayHashMap) keySet() []int {\n    var keys []int\n    for _, pair := range a.buckets {\n        if pair != nil {\n            keys = append(keys, pair.key)\n        }\n    }\n    return keys\n}\n\n/* Get all values */\nfunc (a *arrayHashMap) valueSet() []string {\n    var values []string\n    for _, pair := range a.buckets {\n        if pair != nil {\n            values = append(values, pair.val)\n        }\n    }\n    return values\n}\n\n/* Print hash table */\nfunc (a *arrayHashMap) print() {\n    for _, pair := range a.buckets {\n        if pair != nil {\n            fmt.Println(pair.key, \"->\", pair.val)\n        }\n    }\n}\n
array_hash_map.swift
/* Key-value pair */\nclass Pair: Equatable {\n    public var key: Int\n    public var val: String\n\n    public init(key: Int, val: String) {\n        self.key = key\n        self.val = val\n    }\n\n    public static func == (lhs: Pair, rhs: Pair) -> Bool {\n        lhs.key == rhs.key && lhs.val == rhs.val\n    }\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private var buckets: [Pair?]\n\n    init() {\n        // Initialize array with 100 buckets\n        buckets = Array(repeating: nil, count: 100)\n    }\n\n    /* Hash function */\n    private func hashFunc(key: Int) -> Int {\n        let index = key % 100\n        return index\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let pair = buckets[index]\n        return pair?.val\n    }\n\n    /* Add operation */\n    func put(key: Int, val: String) {\n        let pair = Pair(key: key, val: val)\n        let index = hashFunc(key: key)\n        buckets[index] = pair\n    }\n\n    /* Remove operation */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        // Set to nil to delete\n        buckets[index] = nil\n    }\n\n    /* Get all key-value pairs */\n    func pairSet() -> [Pair] {\n        buckets.compactMap { $0 }\n    }\n\n    /* Get all keys */\n    func keySet() -> [Int] {\n        buckets.compactMap { $0?.key }\n    }\n\n    /* Get all values */\n    func valueSet() -> [String] {\n        buckets.compactMap { $0?.val }\n    }\n\n    /* Print hash table */\n    func print() {\n        for pair in pairSet() {\n            Swift.print(\"\\(pair.key) -> \\(pair.val)\")\n        }\n    }\n}\n
array_hash_map.js
/* Key-value pair Number -> String */\nclass Pair {\n    constructor(key, val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    #buckets;\n    constructor() {\n        // Initialize array with 100 buckets\n        this.#buckets = new Array(100).fill(null);\n    }\n\n    /* Hash function */\n    #hashFunc(key) {\n        return key % 100;\n    }\n\n    /* Query operation */\n    get(key) {\n        let index = this.#hashFunc(key);\n        let pair = this.#buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* Add operation */\n    set(key, val) {\n        let index = this.#hashFunc(key);\n        this.#buckets[index] = new Pair(key, val);\n    }\n\n    /* Remove operation */\n    delete(key) {\n        let index = this.#hashFunc(key);\n        // Set to null to represent deletion\n        this.#buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\n    entries() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* Get all keys */\n    keys() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* Get all values */\n    values() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* Print hash table */\n    print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
array_hash_map.ts
/* Key-value pair Number -> String */\nclass Pair {\n    public key: number;\n    public val: string;\n\n    constructor(key: number, val: string) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private readonly buckets: (Pair | null)[];\n\n    constructor() {\n        // Initialize array with 100 buckets\n        this.buckets = new Array(100).fill(null);\n    }\n\n    /* Hash function */\n    private hashFunc(key: number): number {\n        return key % 100;\n    }\n\n    /* Query operation */\n    public get(key: number): string | null {\n        let index = this.hashFunc(key);\n        let pair = this.buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* Add operation */\n    public set(key: number, val: string) {\n        let index = this.hashFunc(key);\n        this.buckets[index] = new Pair(key, val);\n    }\n\n    /* Remove operation */\n    public delete(key: number) {\n        let index = this.hashFunc(key);\n        // Set to null to represent deletion\n        this.buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\n    public entries(): (Pair | null)[] {\n        let arr: (Pair | null)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* Get all keys */\n    public keys(): (number | undefined)[] {\n        let arr: (number | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* Get all values */\n    public values(): (string | undefined)[] {\n        let arr: (string | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* Print hash table */\n    public print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
array_hash_map.dart
/* Key-value pair */\nclass Pair {\n  int key;\n  String val;\n  Pair(this.key, this.val);\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n  late List<Pair?> _buckets;\n\n  ArrayHashMap() {\n    // Initialize array with 100 buckets\n    _buckets = List.filled(100, null);\n  }\n\n  /* Hash function */\n  int _hashFunc(int key) {\n    final int index = key % 100;\n    return index;\n  }\n\n  /* Query operation */\n  String? get(int key) {\n    final int index = _hashFunc(key);\n    final Pair? pair = _buckets[index];\n    if (pair == null) {\n      return null;\n    }\n    return pair.val;\n  }\n\n  /* Add operation */\n  void put(int key, String val) {\n    final Pair pair = Pair(key, val);\n    final int index = _hashFunc(key);\n    _buckets[index] = pair;\n  }\n\n  /* Remove operation */\n  void remove(int key) {\n    final int index = _hashFunc(key);\n    _buckets[index] = null;\n  }\n\n  /* Get all key-value pairs */\n  List<Pair> pairSet() {\n    List<Pair> pairSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        pairSet.add(pair);\n      }\n    }\n    return pairSet;\n  }\n\n  /* Get all keys */\n  List<int> keySet() {\n    List<int> keySet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        keySet.add(pair.key);\n      }\n    }\n    return keySet;\n  }\n\n  /* Get all values */\n  List<String> values() {\n    List<String> valueSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        valueSet.add(pair.val);\n      }\n    }\n    return valueSet;\n  }\n\n  /* Print hash table */\n  void printHashMap() {\n    for (final Pair kv in pairSet()) {\n      print(\"${kv.key} -> ${kv.val}\");\n    }\n  }\n}\n
array_hash_map.rs
/* Key-value pair */\n#[derive(Debug, Clone, PartialEq)]\npub struct Pair {\n    pub key: i32,\n    pub val: String,\n}\n\n/* Hash table based on array implementation */\npub struct ArrayHashMap {\n    buckets: Vec<Option<Pair>>,\n}\n\nimpl ArrayHashMap {\n    pub fn new() -> ArrayHashMap {\n        // Initialize array with 100 buckets\n        Self {\n            buckets: vec![None; 100],\n        }\n    }\n\n    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % 100\n    }\n\n    /* Query operation */\n    pub fn get(&self, key: i32) -> Option<&String> {\n        let index = self.hash_func(key);\n        self.buckets[index].as_ref().map(|pair| &pair.val)\n    }\n\n    /* Add operation */\n    pub fn put(&mut self, key: i32, val: &str) {\n        let index = self.hash_func(key);\n        self.buckets[index] = Some(Pair {\n            key,\n            val: val.to_string(),\n        });\n    }\n\n    /* Remove operation */\n    pub fn remove(&mut self, key: i32) {\n        let index = self.hash_func(key);\n        // Set to None to represent removal\n        self.buckets[index] = None;\n    }\n\n    /* Get all key-value pairs */\n    pub fn entry_set(&self) -> Vec<&Pair> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref())\n            .collect()\n    }\n\n    /* Get all keys */\n    pub fn key_set(&self) -> Vec<&i32> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.key))\n            .collect()\n    }\n\n    /* Get all values */\n    pub fn value_set(&self) -> Vec<&String> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.val))\n            .collect()\n    }\n\n    /* Print hash table */\n    pub fn print(&self) {\n        for pair in self.entry_set() {\n            println!(\"{} -> {}\", pair.key, pair.val);\n        }\n    }\n}\n
array_hash_map.c
/* Key-value pair int->string */\ntypedef struct {\n    int key;\n    char *val;\n} Pair;\n\n/* Hash table based on array implementation */\ntypedef struct {\n    Pair *buckets[MAX_SIZE];\n} ArrayHashMap;\n\n/* Constructor */\nArrayHashMap *newArrayHashMap() {\n    ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap));\n    for (int i=0; i < MAX_SIZE; i++) {\n        hmap->buckets[i] = NULL;\n    }\n    return hmap;\n}\n\n/* Destructor */\nvoid delArrayHashMap(ArrayHashMap *hmap) {\n    for (int i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            free(hmap->buckets[i]->val);\n            free(hmap->buckets[i]);\n        }\n    }\n    free(hmap);\n}\n\n/* Add operation */\nvoid put(ArrayHashMap *hmap, const int key, const char *val) {\n    Pair *Pair = malloc(sizeof(Pair));\n    Pair->key = key;\n    Pair->val = malloc(strlen(val) + 1);\n    strcpy(Pair->val, val);\n\n    int index = hashFunc(key);\n    hmap->buckets[index] = Pair;\n}\n\n/* Remove operation */\nvoid removeItem(ArrayHashMap *hmap, const int key) {\n    int index = hashFunc(key);\n    free(hmap->buckets[index]->val);\n    free(hmap->buckets[index]);\n    hmap->buckets[index] = NULL;\n}\n\n/* Get all key-value pairs */\nvoid pairSet(ArrayHashMap *hmap, MapSet *set) {\n    Pair *entries;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    entries = malloc(sizeof(Pair) * total);\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            entries[index].key = hmap->buckets[i]->key;\n            entries[index].val = malloc(strlen(hmap->buckets[i]->val) + 1);\n            strcpy(entries[index].val, hmap->buckets[i]->val);\n            index++;\n        }\n    }\n    set->set = entries;\n    set->len = total;\n}\n\n/* Get all keys */\nvoid keySet(ArrayHashMap *hmap, MapSet *set) {\n    int *keys;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    keys = malloc(total * sizeof(int));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            keys[index] = hmap->buckets[i]->key;\n            index++;\n        }\n    }\n    set->set = keys;\n    set->len = total;\n}\n\n/* Get all values */\nvoid valueSet(ArrayHashMap *hmap, MapSet *set) {\n    char **vals;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    vals = malloc(total * sizeof(char *));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            vals[index] = hmap->buckets[i]->val;\n            index++;\n        }\n    }\n    set->set = vals;\n    set->len = total;\n}\n\n/* Print hash table */\nvoid print(ArrayHashMap *hmap) {\n    int i;\n    MapSet set;\n    pairSet(hmap, &set);\n    Pair *entries = (Pair *)set.set;\n    for (i = 0; i < set.len; i++) {\n        printf(\"%d -> %s\\n\", entries[i].key, entries[i].val);\n    }\n    free(set.set);\n}\n
array_hash_map.kt
/* Key-value pair */\nclass Pair(\n    var key: Int,\n    var _val: String\n)\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    // Initialize array with 100 buckets\n    private val buckets = arrayOfNulls<Pair>(100)\n\n    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        val index = key % 100\n        return index\n    }\n\n    /* Query operation */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val pair = buckets[index] ?: return null\n        return pair._val\n    }\n\n    /* Add operation */\n    fun put(key: Int, _val: String) {\n        val pair = Pair(key, _val)\n        val index = hashFunc(key)\n        buckets[index] = pair\n    }\n\n    /* Remove operation */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        // Set to null to represent deletion\n        buckets[index] = null\n    }\n\n    /* Get all key-value pairs */\n    fun pairSet(): MutableList<Pair> {\n        val pairSet = mutableListOf<Pair>()\n        for (pair in buckets) {\n            if (pair != null)\n                pairSet.add(pair)\n        }\n        return pairSet\n    }\n\n    /* Get all keys */\n    fun keySet(): MutableList<Int> {\n        val keySet = mutableListOf<Int>()\n        for (pair in buckets) {\n            if (pair != null)\n                keySet.add(pair.key)\n        }\n        return keySet\n    }\n\n    /* Get all values */\n    fun valueSet(): MutableList<String> {\n        val valueSet = mutableListOf<String>()\n        for (pair in buckets) {\n            if (pair != null)\n                valueSet.add(pair._val)\n        }\n        return valueSet\n    }\n\n    /* Print hash table */\n    fun print() {\n        for (kv in pairSet()) {\n            val key = kv.key\n            val _val = kv._val\n            println(\"$key -> $_val\")\n        }\n    }\n}\n
array_hash_map.rb
### Key-value pair ###\nclass Pair\n  attr_accessor :key, :val\n\n  def initialize(key, val)\n    @key = key\n    @val = val\n  end\nend\n\n### Hash map based on array ###\nclass ArrayHashMap\n  ### Constructor ###\n  def initialize\n    # Initialize array with 100 buckets\n    @buckets = Array.new(100)\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    index = key % 100\n  end\n\n  ### Query operation ###\n  def get(key)\n    index = hash_func(key)\n    pair = @buckets[index]\n\n    return if pair.nil?\n    pair.val\n  end\n\n  ### Add operation ###\n  def put(key, val)\n    pair = Pair.new(key, val)\n    index = hash_func(key)\n    @buckets[index] = pair\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    index = hash_func(key)\n    # Set to nil to delete\n    @buckets[index] = nil\n  end\n\n  ### Get all key-value pairs ###\n  def entry_set\n    result = []\n    @buckets.each { |pair| result << pair unless pair.nil? }\n    result\n  end\n\n  ### Get all keys ###\n  def key_set\n    result = []\n    @buckets.each { |pair| result << pair.key unless pair.nil? }\n    result\n  end\n\n  ### Get all values ###\n  def value_set\n    result = []\n    @buckets.each { |pair| result << pair.val unless pair.nil? }\n    result\n  end\n\n  ### Print hash table ###\n  def print\n    @buckets.each { |pair| puts \"#{pair.key} -> #{pair.val}\" unless pair.nil? }\n  end\nend\n
","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#613-hash-collision-and-resizing","level":2,"title":"6.1.3   Hash Collision and Resizing","text":"

Fundamentally, the role of a hash function is to map the input space consisting of all keys to the output space consisting of all array indices, and the input space is often much larger than the output space. Therefore, theoretically there must be cases where \"multiple inputs correspond to the same output\".

For the hash function in the above example, when the input keys have the same last two digits, the hash function produces the same output. For example, when querying two students with IDs 12836 and 20336, we get:

12836 % 100 = 36\n20336 % 100 = 36\n

As shown in Figure 6-3, two student IDs point to the same name, which is obviously incorrect. We call this situation where multiple inputs correspond to the same output a hash collision.

Figure 6-3   Hash collision example

It's easy to see that the larger the hash table capacity \\(n\\), the lower the probability that multiple keys will be assigned to the same bucket, and the fewer collisions. Therefore, we can reduce hash collisions by expanding the hash table.

As shown in Figure 6-4, before expansion, the key-value pairs (136, A) and (236, D) collided, but after expansion, the collision disappears.

Figure 6-4   Hash table resizing

Similar to array expansion, hash table expansion requires migrating all key-value pairs from the original hash table to the new hash table, which is very time-consuming. Moreover, since the hash table capacity capacity changes, we need to recalculate the storage locations of all key-value pairs through the hash function, further increasing the computational overhead of the expansion process. For this reason, programming languages typically reserve a sufficiently large hash table capacity to prevent frequent expansion.

The load factor is an important concept for hash tables. It is defined as the number of elements in the hash table divided by the number of buckets, and is used to measure the severity of hash collisions. It is also commonly used as a trigger condition for hash table expansion. For example, in Java, when the load factor exceeds \\(0.75\\), the system will expand the hash table to \\(2\\) times its original size.

","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/summary/","level":1,"title":"6.4   Summary","text":"","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_hashing/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Given an input key, a hash table can retrieve the corresponding value in \\(O(1)\\) time, which is highly efficient.
  • Common hash table operations include querying, adding key-value pairs, deleting key-value pairs, and traversing the hash table.
  • The hash function maps a key to an array index, allowing access to the corresponding bucket and retrieval of the value.
  • Two different keys may end up with the same array index after hashing, leading to erroneous query results. This phenomenon is known as hash collision.
  • The larger the capacity of the hash table, the lower the probability of hash collisions. Therefore, hash table expansion can mitigate hash collisions. Similar to array expansion, hash table expansion is costly.
  • The load factor, defined as the number of elements divided by the number of buckets, reflects the severity of hash collisions and is often used as a condition to trigger hash table expansion.
  • Separate chaining addresses hash collisions by converting each element into a linked list, storing all colliding elements in the same linked list. However, excessively long linked lists can reduce query efficiency, which can be improved by converting the linked lists into red-black trees.
  • Open addressing handles hash collisions through multiple probing. Linear probing uses a fixed step size but cannot delete elements and is prone to clustering. Double hashing uses multiple hash functions for probing, which reduces clustering compared to linear probing but increases computational overhead.
  • Different programming languages adopt various hash table implementations. For example, Java's HashMap uses separate chaining, while Python's dict employs open addressing.
  • In hash tables, we desire hash algorithms with determinism, high efficiency, and uniform distribution. In cryptography, hash algorithms should also possess collision resistance and the avalanche effect.
  • Hash algorithms typically use large prime numbers as moduli to maximize the uniform distribution of hash values and reduce hash collisions.
  • Common hash algorithms include MD5, SHA-1, SHA-2, and SHA-3. MD5 is often used for file integrity checks, while SHA-2 is commonly used in secure applications and protocols.
  • Programming languages usually provide built-in hash algorithms for data types to calculate bucket indices in hash tables. Generally, only immutable objects are hashable.
","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_hashing/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: When does the time complexity of a hash table degrade to \\(O(n)\\)?

The time complexity of a hash table can degrade to \\(O(n)\\) when hash collisions are severe. When the hash function is well-designed, the capacity is set appropriately, and collisions are evenly distributed, the time complexity is \\(O(1)\\). We usually consider the time complexity to be \\(O(1)\\) when using built-in hash tables in programming languages.

Q: Why not use the hash function \\(f(x) = x\\)? This would eliminate collisions.

Under the hash function \\(f(x) = x\\), each element corresponds to a unique bucket index, which is equivalent to an array. However, the input space is usually much larger than the output space (array length), so the last step of a hash function is often to take the modulo of the array length. In other words, the goal of a hash table is to map a larger state space to a smaller one while providing \\(O(1)\\) query efficiency.

Q: Why can hash tables be more efficient than arrays, linked lists, or binary trees, even though hash tables are implemented using these structures?

Firstly, hash tables have higher time efficiency but lower space efficiency. A significant portion of memory in hash tables remains unused.

Secondly, hash tables are only more time-efficient in specific use cases. If a feature can be implemented with the same time complexity using an array or a linked list, it's usually faster than using a hash table. This is because the computation of the hash function incurs overhead, making the constant factor in the time complexity larger.

Lastly, the time complexity of hash tables can degrade. For example, in separate chaining, we perform search operations in a linked list or red-black tree, which still risks degrading to \\(O(n)\\) time.

Q: Does double hashing also have the flaw of not being able to delete elements directly? Can space marked as deleted be reused?

Double hashing is a form of open addressing, and all open addressing methods have the drawback of not being able to delete elements directly; they require marking elements as deleted. Marked spaces can be reused. When inserting new elements into the hash table, and the hash function points to a position marked as deleted, that position can be used by the new element. This maintains the probing sequence of the hash table while ensuring efficient use of space.

Q: Why do hash collisions occur during the search process in linear probing?

During the search process, the hash function points to the corresponding bucket and key-value pair. If the key doesn't match, it indicates a hash collision. Therefore, linear probing will search downward at a predetermined step size until the correct key-value pair is found or the search fails.

Q: Why can expanding a hash table alleviate hash collisions?

The last step of a hash function often involves taking the modulo of the array length \\(n\\), to keep the output within the array index range. When expanding, the array length \\(n\\) changes, and the indices corresponding to the keys may also change. Keys that were previously mapped to the same bucket might be distributed across multiple buckets after expansion, thereby mitigating hash collisions.

","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_heap/","level":1,"title":"Chapter 8.   Heap","text":"

Abstract

Heaps are like mountain peaks, layered and undulating, each with its unique form.

The peaks rise and fall at varying heights, yet the tallest peak always catches the eye first.

","path":["Chapter 8. Heap","Chapter 8.   Heap"],"tags":[]},{"location":"chapter_heap/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 8.1   Heap
  • 8.2   Building a Heap
  • 8.3   Top-K Problem
  • 8.4   Summary
","path":["Chapter 8. Heap","Chapter 8.   Heap"],"tags":[]},{"location":"chapter_heap/build_heap/","level":1,"title":"8.2   Heap Construction Operation","text":"

In some cases, we want to build a heap using all elements of a list, and this process is called \"heap construction operation.\"

","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#821-implementing-with-element-insertion","level":2,"title":"8.2.1   Implementing with Element Insertion","text":"

We first create an empty heap, then iterate through the list, performing the \"element insertion operation\" on each element in sequence. This means adding the element to the bottom of the heap and then performing \"bottom-to-top\" heapify on that element.

Each time an element is inserted into the heap, the heap's length increases by one. Since nodes are added to the binary tree sequentially from top to bottom, the heap is constructed \"from top to bottom.\"

Given \\(n\\) elements, each element's insertion operation takes \\(O(\\log{n})\\) time, so the time complexity of this heap construction method is \\(O(n \\log n)\\).

","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#822-implementing-through-heapify-traversal","level":2,"title":"8.2.2   Implementing Through Heapify Traversal","text":"

In fact, we can implement a more efficient heap construction method in two steps.

  1. Add all elements of the list as-is to the heap, at which point the heap property is not yet satisfied.
  2. Traverse the heap in reverse order (reverse of level-order traversal), performing \"top-to-bottom heapify\" on each non-leaf node in sequence.

After heapifying a node, the subtree rooted at that node becomes a valid sub-heap. Since we traverse in reverse order, the heap is constructed \"from bottom to top.\"

The reason for choosing reverse order traversal is that it ensures the subtree below the current node is already a valid sub-heap, making the heapification of the current node effective.

It's worth noting that since leaf nodes have no children, they are naturally valid sub-heaps and do not require heapification. As shown in the code below, the last non-leaf node is the parent of the last node; we start from it and traverse in reverse order to perform heapification:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def __init__(self, nums: list[int]):\n    \"\"\"Constructor, build heap based on input list\"\"\"\n    # Add list elements to heap as is\n    self.max_heap = nums\n    # Heapify all nodes except leaf nodes\n    for i in range(self.parent(self.size() - 1), -1, -1):\n        self.sift_down(i)\n
my_heap.cpp
/* Constructor, build heap based on input list */\nMaxHeap(vector<int> nums) {\n    // Add list elements to heap as is\n    maxHeap = nums;\n    // Heapify all nodes except leaf nodes\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
my_heap.java
/* Constructor, build heap based on input list */\nMaxHeap(List<Integer> nums) {\n    // Add list elements to heap as is\n    maxHeap = new ArrayList<>(nums);\n    // Heapify all nodes except leaf nodes\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
my_heap.cs
/* Constructor, build heap from input list */\nMaxHeap(IEnumerable<int> nums) {\n    // Add list elements to heap as is\n    maxHeap = new List<int>(nums);\n    // Heapify all nodes except leaf nodes\n    var size = Parent(this.Size() - 1);\n    for (int i = size; i >= 0; i--) {\n        SiftDown(i);\n    }\n}\n
my_heap.go
/* Constructor, build heap from slice */\nfunc newMaxHeap(nums []any) *maxHeap {\n    // Add list elements to heap as is\n    h := &maxHeap{data: nums}\n    for i := h.parent(len(h.data) - 1); i >= 0; i-- {\n        // Heapify all nodes except leaf nodes\n        h.siftDown(i)\n    }\n    return h\n}\n
my_heap.swift
/* Constructor, build heap based on input list */\ninit(nums: [Int]) {\n    // Add list elements to heap as is\n    maxHeap = nums\n    // Heapify all nodes except leaf nodes\n    for i in (0 ... parent(i: size() - 1)).reversed() {\n        siftDown(i: i)\n    }\n}\n
my_heap.js
/* Constructor, build empty heap or build heap from input list */\nconstructor(nums) {\n    // Add list elements to heap as is\n    this.#maxHeap = nums === undefined ? [] : [...nums];\n    // Heapify all nodes except leaf nodes\n    for (let i = this.#parent(this.size() - 1); i >= 0; i--) {\n        this.#siftDown(i);\n    }\n}\n
my_heap.ts
/* Constructor, build empty heap or build heap from input list */\nconstructor(nums?: number[]) {\n    // Add list elements to heap as is\n    this.maxHeap = nums === undefined ? [] : [...nums];\n    // Heapify all nodes except leaf nodes\n    for (let i = this.parent(this.size() - 1); i >= 0; i--) {\n        this.siftDown(i);\n    }\n}\n
my_heap.dart
/* Constructor, build heap based on input list */\nMaxHeap(List<int> nums) {\n  // Add list elements to heap as is\n  _maxHeap = nums;\n  // Heapify all nodes except leaf nodes\n  for (int i = _parent(size() - 1); i >= 0; i--) {\n    siftDown(i);\n  }\n}\n
my_heap.rs
/* Constructor, build heap based on input list */\nfn new(nums: Vec<i32>) -> Self {\n    // Add list elements to heap as is\n    let mut heap = MaxHeap { max_heap: nums };\n    // Heapify all nodes except leaf nodes\n    for i in (0..=Self::parent(heap.size() - 1)).rev() {\n        heap.sift_down(i);\n    }\n    heap\n}\n
my_heap.c
/* Constructor, build heap from slice */\nMaxHeap *newMaxHeap(int nums[], int size) {\n    // Push all elements to heap\n    MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap));\n    maxHeap->size = size;\n    memcpy(maxHeap->data, nums, size * sizeof(int));\n    for (int i = parent(maxHeap, size - 1); i >= 0; i--) {\n        // Heapify all nodes except leaf nodes\n        siftDown(maxHeap, i);\n    }\n    return maxHeap;\n}\n
my_heap.kt
/* Max heap */\nclass MaxHeap(nums: MutableList<Int>?) {\n    // Use list instead of array, no need to consider capacity expansion\n    private val maxHeap = mutableListOf<Int>()\n\n    /* Constructor, build heap based on input list */\n    init {\n        // Add list elements to heap as is\n        maxHeap.addAll(nums!!)\n        // Heapify all nodes except leaf nodes\n        for (i in parent(size() - 1) downTo 0) {\n            siftDown(i)\n        }\n    }\n\n    /* Get index of left child node */\n    private fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Get index of right child node */\n    private fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Get index of parent node */\n    private fun parent(i: Int): Int {\n        return (i - 1) / 2 // Floor division\n    }\n\n    /* Swap elements */\n    private fun swap(i: Int, j: Int) {\n        val temp = maxHeap[i]\n        maxHeap[i] = maxHeap[j]\n        maxHeap[j] = temp\n    }\n\n    /* Get heap size */\n    fun size(): Int {\n        return maxHeap.size\n    }\n\n    /* Check if heap is empty */\n    fun isEmpty(): Boolean {\n        /* Check if heap is empty */\n        return size() == 0\n    }\n\n    /* Access top element */\n    fun peek(): Int {\n        return maxHeap[0]\n    }\n\n    /* Element enters heap */\n    fun push(_val: Int) {\n        // Add node\n        maxHeap.add(_val)\n        // Heapify from bottom to top\n        siftUp(size() - 1)\n    }\n\n    /* Starting from node i, heapify from bottom to top */\n    private fun siftUp(it: Int) {\n        // Kotlin function parameters are immutable, so create temporary variable\n        var i = it\n        while (true) {\n            // Get parent node of node i\n            val p = parent(i)\n            // When \"crossing root node\" or \"node needs no repair\", end heapify\n            if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n            // Swap two nodes\n            swap(i, p)\n            // Loop upward heapify\n            i = p\n        }\n    }\n\n    /* Element exits heap */\n    fun pop(): Int {\n        // Handle empty case\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Delete node\n        swap(0, size() - 1)\n        // Remove node\n        val _val = maxHeap.removeAt(size() - 1)\n        // Return top element\n        siftDown(0)\n        // Return heap top element\n        return _val\n    }\n\n    /* Starting from node i, heapify from top to bottom */\n    private fun siftDown(it: Int) {\n        // Kotlin function parameters are immutable, so create temporary variable\n        var i = it\n        while (true) {\n            // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n            val l = left(i)\n            val r = right(i)\n            var ma = i\n            if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n            if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n            // Swap two nodes\n            if (ma == i) break\n            // Swap two nodes\n            swap(i, ma)\n            // Loop downwards heapification\n            i = ma\n        }\n    }\n\n    /* Driver Code */\n    fun print() {\n        val queue = PriorityQueue { a: Int, b: Int -> b - a }\n        queue.addAll(maxHeap)\n        printHeap(queue)\n    }\n}\n
my_heap.rb
### Constructor, build heap from input list ###\ndef initialize(nums)\n  # Add list elements to heap as is\n  @max_heap = nums\n  # Heapify all nodes except leaf nodes\n  parent(size - 1).downto(0) do |i|\n    sift_down(i)\n  end\nend\n
","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#823-complexity-analysis","level":2,"title":"8.2.3   Complexity Analysis","text":"

Next, let's attempt to derive the time complexity of this second heap construction method.

  • Assuming the complete binary tree has \\(n\\) nodes, then the number of leaf nodes is \\((n + 1) / 2\\), where \\(/\\) is floor division. Therefore, the number of nodes that need heapification is \\((n - 1) / 2\\).
  • In the top-to-bottom heapify process, each node is heapified at most to the leaf nodes, so the maximum number of iterations is the binary tree height \\(\\log n\\).

Multiplying these two together, we get a time complexity of \\(O(n \\log n)\\) for the heap construction process. However, this estimate is not accurate because it doesn't account for the property that binary trees have far more nodes at lower levels than at upper levels.

Let's perform a more accurate calculation. To reduce calculation difficulty, assume a \"perfect binary tree\" with \\(n\\) nodes and height \\(h\\); this assumption does not affect the correctness of the result.

Figure 8-5   Node count at each level of a perfect binary tree

As shown in Figure 8-5, the maximum number of iterations for a node's \"top-to-bottom heapify\" equals the distance from that node to the leaf nodes, which is precisely the \"node height.\" Therefore, we can sum the \"number of nodes \\(\\times\\) node height\" at each level to obtain the total number of heapify iterations for all nodes.

\\[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{(h-1)}\\times1 \\]

To simplify the above expression, we need to use sequence knowledge from high school. First, multiply \\(T(h)\\) by \\(2\\) to get:

\\[ \\begin{aligned} T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{h-1}\\times1 \\newline 2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \\dots + 2^{h}\\times1 \\newline \\end{aligned} \\]

Using the method of differences, subtract the first equation \\(T(h)\\) from the second equation \\(2 T(h)\\) to get:

\\[ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \\dots + 2^{h-1} + 2^h \\]

Observing the above expression, we find that \\(T(h)\\) is a geometric series, which can be calculated directly using the sum formula, yielding a time complexity of:

\\[ \\begin{aligned} T(h) & = 2 \\frac{1 - 2^h}{1 - 2} - h \\newline & = 2^{h+1} - h - 2 \\newline & = O(2^h) \\end{aligned} \\]

Furthermore, a perfect binary tree with height \\(h\\) has \\(n = 2^{h+1} - 1\\) nodes, so the complexity is \\(O(2^h) = O(n)\\). This derivation shows that the time complexity of building a heap from an input list is \\(O(n)\\), which is highly efficient.

","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/heap/","level":1,"title":"8.1   Heap","text":"

A heap is a complete binary tree that satisfies specific conditions and can be mainly categorized into two types, as shown in Figure 8-1.

  • min heap: The value of any node \\(\\leq\\) the values of its child nodes.
  • max heap: The value of any node \\(\\geq\\) the values of its child nodes.

Figure 8-1   Min heap and max heap

As a special case of a complete binary tree, heaps have the following characteristics.

  • The bottom layer nodes are filled from left to right, and nodes in other layers are fully filled.
  • We call the root node of the binary tree the \"heap top\" and the bottom-rightmost node the \"heap bottom.\"
  • For max heaps (min heaps), the value of the heap top element (root node) is the largest (smallest).
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#811-common-heap-operations","level":2,"title":"8.1.1   Common Heap Operations","text":"

It should be noted that many programming languages provide a priority queue, which is an abstract data structure defined as a queue with priority sorting.

In fact, heaps are typically used to implement priority queues, with max heaps corresponding to priority queues where elements are dequeued in descending order. From a usage perspective, we can regard \"priority queue\" and \"heap\" as equivalent data structures. Therefore, this book does not make a special distinction between the two and uniformly refers to them as \"heap.\"

Common heap operations are shown in Table 8-1, and method names need to be determined based on the programming language.

Table 8-1   Efficiency of Heap Operations

Method name Description Time complexity push() Insert an element into the heap \\(O(\\log n)\\) pop() Remove the heap top element \\(O(\\log n)\\) peek() Access the heap top element (max/min value for max/min heap) \\(O(1)\\) size() Get the number of elements in the heap \\(O(1)\\) isEmpty() Check if the heap is empty \\(O(1)\\)

In practical applications, we can directly use the heap class (or priority queue class) provided by programming languages.

Similar to \"ascending order\" and \"descending order\" in sorting algorithms, we can implement conversion between \"min heap\" and \"max heap\" by setting a flag or modifying the Comparator. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap.py
# Initialize a min heap\nmin_heap, flag = [], 1\n# Initialize a max heap\nmax_heap, flag = [], -1\n\n# Python's heapq module implements a min heap by default\n# Consider negating elements before pushing them to the heap, which inverts the size relationship and thus implements a max heap\n# In this example, flag = 1 corresponds to a min heap, flag = -1 corresponds to a max heap\n\n# Push elements into the heap\nheapq.heappush(max_heap, flag * 1)\nheapq.heappush(max_heap, flag * 3)\nheapq.heappush(max_heap, flag * 2)\nheapq.heappush(max_heap, flag * 5)\nheapq.heappush(max_heap, flag * 4)\n\n# Get the heap top element\npeek: int = flag * max_heap[0] # 5\n\n# Remove the heap top element\n# The removed elements will form a descending sequence\nval = flag * heapq.heappop(max_heap) # 5\nval = flag * heapq.heappop(max_heap) # 4\nval = flag * heapq.heappop(max_heap) # 3\nval = flag * heapq.heappop(max_heap) # 2\nval = flag * heapq.heappop(max_heap) # 1\n\n# Get the heap size\nsize: int = len(max_heap)\n\n# Check if the heap is empty\nis_empty: bool = not max_heap\n\n# Build a heap from an input list\nmin_heap: list[int] = [1, 3, 2, 5, 4]\nheapq.heapify(min_heap)\n
heap.cpp
/* Initialize a heap */\n// Initialize a min heap\npriority_queue<int, vector<int>, greater<int>> minHeap;\n// Initialize a max heap\npriority_queue<int, vector<int>, less<int>> maxHeap;\n\n/* Push elements into the heap */\nmaxHeap.push(1);\nmaxHeap.push(3);\nmaxHeap.push(2);\nmaxHeap.push(5);\nmaxHeap.push(4);\n\n/* Get the heap top element */\nint peek = maxHeap.top(); // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\nmaxHeap.pop(); // 5\nmaxHeap.pop(); // 4\nmaxHeap.pop(); // 3\nmaxHeap.pop(); // 2\nmaxHeap.pop(); // 1\n\n/* Get the heap size */\nint size = maxHeap.size();\n\n/* Check if the heap is empty */\nbool isEmpty = maxHeap.empty();\n\n/* Build a heap from an input list */\nvector<int> input{1, 3, 2, 5, 4};\npriority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());\n
heap.java
/* Initialize a heap */\n// Initialize a min heap\nQueue<Integer> minHeap = new PriorityQueue<>();\n// Initialize a max heap (use lambda expression to modify Comparator)\nQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);\n\n/* Push elements into the heap */\nmaxHeap.offer(1);\nmaxHeap.offer(3);\nmaxHeap.offer(2);\nmaxHeap.offer(5);\nmaxHeap.offer(4);\n\n/* Get the heap top element */\nint peek = maxHeap.peek(); // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.poll(); // 5\npeek = maxHeap.poll(); // 4\npeek = maxHeap.poll(); // 3\npeek = maxHeap.poll(); // 2\npeek = maxHeap.poll(); // 1\n\n/* Get the heap size */\nint size = maxHeap.size();\n\n/* Check if the heap is empty */\nboolean isEmpty = maxHeap.isEmpty();\n\n/* Build a heap from an input list */\nminHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));\n
heap.cs
/* Initialize a heap */\n// Initialize a min heap\nPriorityQueue<int, int> minHeap = new();\n// Initialize a max heap (use lambda expression to modify Comparer)\nPriorityQueue<int, int> maxHeap = new(Comparer<int>.Create((x, y) => y.CompareTo(x)));\n\n/* Push elements into the heap */\nmaxHeap.Enqueue(1, 1);\nmaxHeap.Enqueue(3, 3);\nmaxHeap.Enqueue(2, 2);\nmaxHeap.Enqueue(5, 5);\nmaxHeap.Enqueue(4, 4);\n\n/* Get the heap top element */\nint peek = maxHeap.Peek();//5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.Dequeue();  // 5\npeek = maxHeap.Dequeue();  // 4\npeek = maxHeap.Dequeue();  // 3\npeek = maxHeap.Dequeue();  // 2\npeek = maxHeap.Dequeue();  // 1\n\n/* Get the heap size */\nint size = maxHeap.Count;\n\n/* Check if the heap is empty */\nbool isEmpty = maxHeap.Count == 0;\n\n/* Build a heap from an input list */\nminHeap = new PriorityQueue<int, int>([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);\n
heap.go
// In Go, we can construct a max heap of integers by implementing heap.Interface\n// Implementing heap.Interface also requires implementing sort.Interface\ntype intHeap []any\n\n// Push implements the heap.Interface method for pushing an element into the heap\nfunc (h *intHeap) Push(x any) {\n    // Push and Pop use pointer receiver as parameters\n    // because they not only adjust the slice contents but also modify the slice length\n    *h = append(*h, x.(int))\n}\n\n// Pop implements the heap.Interface method for popping the heap top element\nfunc (h *intHeap) Pop() any {\n    // The element to be removed is stored at the end\n    last := (*h)[len(*h)-1]\n    *h = (*h)[:len(*h)-1]\n    return last\n}\n\n// Len is a sort.Interface method\nfunc (h *intHeap) Len() int {\n    return len(*h)\n}\n\n// Less is a sort.Interface method\nfunc (h *intHeap) Less(i, j int) bool {\n    // To implement a min heap, change this to a less-than sign\n    return (*h)[i].(int) > (*h)[j].(int)\n}\n\n// Swap is a sort.Interface method\nfunc (h *intHeap) Swap(i, j int) {\n    (*h)[i], (*h)[j] = (*h)[j], (*h)[i]\n}\n\n// Top gets the heap top element\nfunc (h *intHeap) Top() any {\n    return (*h)[0]\n}\n\n/* Driver Code */\nfunc TestHeap(t *testing.T) {\n    /* Initialize a heap */\n    // Initialize a max heap\n    maxHeap := &intHeap{}\n    heap.Init(maxHeap)\n    /* Push elements into the heap */\n    // Call heap.Interface methods to add elements\n    heap.Push(maxHeap, 1)\n    heap.Push(maxHeap, 3)\n    heap.Push(maxHeap, 2)\n    heap.Push(maxHeap, 4)\n    heap.Push(maxHeap, 5)\n\n    /* Get the heap top element */\n    top := maxHeap.Top()\n    fmt.Printf(\"Heap top element is %d\\n\", top)\n\n    /* Remove the heap top element */\n    // Call heap.Interface methods to remove elements\n    heap.Pop(maxHeap) // 5\n    heap.Pop(maxHeap) // 4\n    heap.Pop(maxHeap) // 3\n    heap.Pop(maxHeap) // 2\n    heap.Pop(maxHeap) // 1\n\n    /* Get the heap size */\n    size := len(*maxHeap)\n    fmt.Printf(\"Number of heap elements is %d\\n\", size)\n\n    /* Check if the heap is empty */\n    isEmpty := len(*maxHeap) == 0\n    fmt.Printf(\"Is the heap empty? %t\\n\", isEmpty)\n}\n
heap.swift
/* Initialize a heap */\n// Swift's Heap type supports both max heaps and min heaps, and requires importing swift-collections\nvar heap = Heap<Int>()\n\n/* Push elements into the heap */\nheap.insert(1)\nheap.insert(3)\nheap.insert(2)\nheap.insert(5)\nheap.insert(4)\n\n/* Get the heap top element */\nvar peek = heap.max()!\n\n/* Remove the heap top element */\npeek = heap.removeMax() // 5\npeek = heap.removeMax() // 4\npeek = heap.removeMax() // 3\npeek = heap.removeMax() // 2\npeek = heap.removeMax() // 1\n\n/* Get the heap size */\nlet size = heap.count\n\n/* Check if the heap is empty */\nlet isEmpty = heap.isEmpty\n\n/* Build a heap from an input list */\nlet heap2 = Heap([1, 3, 2, 5, 4])\n
heap.js
// JavaScript does not provide a built-in Heap class\n
heap.ts
// TypeScript does not provide a built-in Heap class\n
heap.dart
// Dart does not provide a built-in Heap class\n
heap.rs
use std::collections::BinaryHeap;\nuse std::cmp::Reverse;\n\n/* Initialize a heap */\n// Initialize a min heap\nlet mut min_heap = BinaryHeap::<Reverse<i32>>::new();\n// Initialize a max heap\nlet mut max_heap = BinaryHeap::new();\n\n/* Push elements into the heap */\nmax_heap.push(1);\nmax_heap.push(3);\nmax_heap.push(2);\nmax_heap.push(5);\nmax_heap.push(4);\n\n/* Get the heap top element */\nlet peek = max_heap.peek().unwrap();  // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\nlet peek = max_heap.pop().unwrap();   // 5\nlet peek = max_heap.pop().unwrap();   // 4\nlet peek = max_heap.pop().unwrap();   // 3\nlet peek = max_heap.pop().unwrap();   // 2\nlet peek = max_heap.pop().unwrap();   // 1\n\n/* Get the heap size */\nlet size = max_heap.len();\n\n/* Check if the heap is empty */\nlet is_empty = max_heap.is_empty();\n\n/* Build a heap from an input list */\nlet min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);\n
heap.c
// C does not provide a built-in Heap class\n
heap.kt
/* Initialize a heap */\n// Initialize a min heap\nvar minHeap = PriorityQueue<Int>()\n// Initialize a max heap (use lambda expression to modify Comparator)\nval maxHeap = PriorityQueue { a: Int, b: Int -> b - a }\n\n/* Push elements into the heap */\nmaxHeap.offer(1)\nmaxHeap.offer(3)\nmaxHeap.offer(2)\nmaxHeap.offer(5)\nmaxHeap.offer(4)\n\n/* Get the heap top element */\nvar peek = maxHeap.peek() // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.poll() // 5\npeek = maxHeap.poll() // 4\npeek = maxHeap.poll() // 3\npeek = maxHeap.poll() // 2\npeek = maxHeap.poll() // 1\n\n/* Get the heap size */\nval size = maxHeap.size\n\n/* Check if the heap is empty */\nval isEmpty = maxHeap.isEmpty()\n\n/* Build a heap from an input list */\nminHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))\n
heap.rb
# Ruby does not provide a built-in Heap class\n
Code Visualization

Full Screen >

","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#812-implementation-of-the-heap","level":2,"title":"8.1.2   Implementation of the Heap","text":"

The following implementation is of a max heap. To convert it to a min heap, simply invert all size logic comparisons (for example, replace \\(\\geq\\) with \\(\\leq\\)). Interested readers are encouraged to implement this on their own.

","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#1-heap-storage-and-representation","level":3,"title":"1.   Heap Storage and Representation","text":"

As mentioned in the \"Binary Tree\" chapter, complete binary trees are well-suited for array representation. Since heaps are a type of complete binary tree, we will use arrays to store heaps.

When representing a binary tree with an array, elements represent node values, and indexes represent node positions in the binary tree. Node pointers are implemented through index mapping formulas.

As shown in Figure 8-2, given an index \\(i\\), the index of its left child is \\(2i + 1\\), the index of its right child is \\(2i + 2\\), and the index of its parent is \\((i - 1) / 2\\) (floor division). When an index is out of bounds, it indicates a null node or that the node does not exist.

Figure 8-2   Representation and storage of heaps

We can encapsulate the index mapping formula into functions for convenient subsequent use:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def left(self, i: int) -> int:\n    \"\"\"Get index of left child node\"\"\"\n    return 2 * i + 1\n\ndef right(self, i: int) -> int:\n    \"\"\"Get index of right child node\"\"\"\n    return 2 * i + 2\n\ndef parent(self, i: int) -> int:\n    \"\"\"Get index of parent node\"\"\"\n    return (i - 1) // 2  # Floor division\n
my_heap.cpp
/* Get index of left child node */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
my_heap.java
/* Get index of left child node */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
my_heap.cs
/* Get index of left child node */\nint Left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint Right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint Parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
my_heap.go
/* Get index of left child node */\nfunc (h *maxHeap) left(i int) int {\n    return 2*i + 1\n}\n\n/* Get index of right child node */\nfunc (h *maxHeap) right(i int) int {\n    return 2*i + 2\n}\n\n/* Get index of parent node */\nfunc (h *maxHeap) parent(i int) int {\n    // Floor division\n    return (i - 1) / 2\n}\n
my_heap.swift
/* Get index of left child node */\nfunc left(i: Int) -> Int {\n    2 * i + 1\n}\n\n/* Get index of right child node */\nfunc right(i: Int) -> Int {\n    2 * i + 2\n}\n\n/* Get index of parent node */\nfunc parent(i: Int) -> Int {\n    (i - 1) / 2 // Floor division\n}\n
my_heap.js
/* Get index of left child node */\n#left(i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\n#right(i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\n#parent(i) {\n    return Math.floor((i - 1) / 2); // Floor division\n}\n
my_heap.ts
/* Get index of left child node */\nleft(i: number): number {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nright(i: number): number {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nparent(i: number): number {\n    return Math.floor((i - 1) / 2); // Floor division\n}\n
my_heap.dart
/* Get index of left child node */\nint _left(int i) {\n  return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint _right(int i) {\n  return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint _parent(int i) {\n  return (i - 1) ~/ 2; // Floor division\n}\n
my_heap.rs
/* Get index of left child node */\nfn left(i: usize) -> usize {\n    2 * i + 1\n}\n\n/* Get index of right child node */\nfn right(i: usize) -> usize {\n    2 * i + 2\n}\n\n/* Get index of parent node */\nfn parent(i: usize) -> usize {\n    (i - 1) / 2 // Floor division\n}\n
my_heap.c
/* Get index of left child node */\nint left(MaxHeap *maxHeap, int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(MaxHeap *maxHeap, int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(MaxHeap *maxHeap, int i) {\n    return (i - 1) / 2; // Round down\n}\n
my_heap.kt
/* Get index of left child node */\nfun left(i: Int): Int {\n    return 2 * i + 1\n}\n\n/* Get index of right child node */\nfun right(i: Int): Int {\n    return 2 * i + 2\n}\n\n/* Get index of parent node */\nfun parent(i: Int): Int {\n    return (i - 1) / 2 // Floor division\n}\n
my_heap.rb
### Get left child index ###\ndef left(i)\n  2 * i + 1\nend\n\n### Get right child index ###\ndef right(i)\n  2 * i + 2\nend\n\n### Get parent node index ###\ndef parent(i)\n  (i - 1) / 2     # Floor division\nend\n
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#2-accessing-the-heap-top-element","level":3,"title":"2.   Accessing the Heap Top Element","text":"

The heap top element is the root node of the binary tree, which is also the first element of the list:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def peek(self) -> int:\n    \"\"\"Access top element\"\"\"\n    return self.max_heap[0]\n
my_heap.cpp
/* Access top element */\nint peek() {\n    return maxHeap[0];\n}\n
my_heap.java
/* Access top element */\nint peek() {\n    return maxHeap.get(0);\n}\n
my_heap.cs
/* Access top element */\nint Peek() {\n    return maxHeap[0];\n}\n
my_heap.go
/* Access top element */\nfunc (h *maxHeap) peek() any {\n    return h.data[0]\n}\n
my_heap.swift
/* Access top element */\nfunc peek() -> Int {\n    maxHeap[0]\n}\n
my_heap.js
/* Access top element */\npeek() {\n    return this.#maxHeap[0];\n}\n
my_heap.ts
/* Access top element */\npeek(): number {\n    return this.maxHeap[0];\n}\n
my_heap.dart
/* Access top element */\nint peek() {\n  return _maxHeap[0];\n}\n
my_heap.rs
/* Access top element */\nfn peek(&self) -> Option<i32> {\n    self.max_heap.first().copied()\n}\n
my_heap.c
/* Access top element */\nint peek(MaxHeap *maxHeap) {\n    return maxHeap->data[0];\n}\n
my_heap.kt
/* Access top element */\nfun peek(): Int {\n    return maxHeap[0]\n}\n
my_heap.rb
### Access heap top element ###\ndef peek\n  @max_heap[0]\nend\n
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#3-inserting-an-element-into-the-heap","level":3,"title":"3.   Inserting an Element Into the Heap","text":"

Given an element val, we first add it to the bottom of the heap. After addition, since val may be larger than other elements in the heap, the heap's property may be violated. Therefore, it's necessary to repair the path from the inserted node to the root node. This operation is called heapify.

Starting from the inserted node, perform heapify from bottom to top. As shown in Figure 8-3, we compare the inserted node with its parent node, and if the inserted node is larger, swap them. Then continue this operation, repairing nodes in the heap from bottom to top until we pass the root node or encounter a node that does not need swapping.

<1><2><3><4><5><6><7><8><9>

Figure 8-3   Steps of inserting an element into the heap

Given a total of \\(n\\) nodes, the tree height is \\(O(\\log n)\\). Thus, the number of loop iterations in the heapify operation is at most \\(O(\\log n)\\), making the time complexity of the element insertion operation \\(O(\\log n)\\). The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def push(self, val: int):\n    \"\"\"Element enters heap\"\"\"\n    # Add node\n    self.max_heap.append(val)\n    # Heapify from bottom to top\n    self.sift_up(self.size() - 1)\n\ndef sift_up(self, i: int):\n    \"\"\"Starting from node i, heapify from bottom to top\"\"\"\n    while True:\n        # Get parent node of node i\n        p = self.parent(i)\n        # When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n            break\n        # Swap two nodes\n        self.swap(i, p)\n        # Loop upward heapify\n        i = p\n
my_heap.cpp
/* Element enters heap */\nvoid push(int val) {\n    // Add node\n    maxHeap.push_back(val);\n    // Heapify from bottom to top\n    siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Swap two nodes\n        swap(maxHeap[i], maxHeap[p]);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.java
/* Element enters heap */\nvoid push(int val) {\n    // Add node\n    maxHeap.add(val);\n    // Heapify from bottom to top\n    siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n            break;\n        // Swap two nodes\n        swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.cs
/* Element enters heap */\nvoid Push(int val) {\n    // Add node\n    maxHeap.Add(val);\n    // Heapify from bottom to top\n    SiftUp(Size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid SiftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = Parent(i);\n        // If 'past root node' or 'node needs no repair', end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Swap two nodes\n        Swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.go
/* Element enters heap */\nfunc (h *maxHeap) push(val any) {\n    // Add node\n    h.data = append(h.data, val)\n    // Heapify from bottom to top\n    h.siftUp(len(h.data) - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfunc (h *maxHeap) siftUp(i int) {\n    for true {\n        // Get parent node of node i\n        p := h.parent(i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 || h.data[i].(int) <= h.data[p].(int) {\n            break\n        }\n        // Swap two nodes\n        h.swap(i, p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
my_heap.swift
/* Element enters heap */\nfunc push(val: Int) {\n    // Add node\n    maxHeap.append(val)\n    // Heapify from bottom to top\n    siftUp(i: size() - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfunc siftUp(i: Int) {\n    var i = i\n    while true {\n        // Get parent node of node i\n        let p = parent(i: i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 || maxHeap[i] <= maxHeap[p] {\n            break\n        }\n        // Swap two nodes\n        swap(i: i, j: p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
my_heap.js
/* Element enters heap */\npush(val) {\n    // Add node\n    this.#maxHeap.push(val);\n    // Heapify from bottom to top\n    this.#siftUp(this.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\n#siftUp(i) {\n    while (true) {\n        // Get parent node of node i\n        const p = this.#parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break;\n        // Swap two nodes\n        this.#swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.ts
/* Element enters heap */\npush(val: number): void {\n    // Add node\n    this.maxHeap.push(val);\n    // Heapify from bottom to top\n    this.siftUp(this.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nsiftUp(i: number): void {\n    while (true) {\n        // Get parent node of node i\n        const p = this.parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break;\n        // Swap two nodes\n        this.swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.dart
/* Element enters heap */\nvoid push(int val) {\n  // Add node\n  _maxHeap.add(val);\n  // Heapify from bottom to top\n  siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n  while (true) {\n    // Get parent node of node i\n    int p = _parent(i);\n    // When \"crossing root node\" or \"node needs no repair\", end heapify\n    if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {\n      break;\n    }\n    // Swap two nodes\n    _swap(i, p);\n    // Loop upward heapify\n    i = p;\n  }\n}\n
my_heap.rs
/* Element enters heap */\nfn push(&mut self, val: i32) {\n    // Add node\n    self.max_heap.push(val);\n    // Heapify from bottom to top\n    self.sift_up(self.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nfn sift_up(&mut self, mut i: usize) {\n    loop {\n        // Node i is already the heap root, end heapification\n        if i == 0 {\n            break;\n        }\n        // Get parent node of node i\n        let p = Self::parent(i);\n        // When \"node needs no repair\", end heapification\n        if self.max_heap[i] <= self.max_heap[p] {\n            break;\n        }\n        // Swap two nodes\n        self.swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.c
/* Element enters heap */\nvoid push(MaxHeap *maxHeap, int val) {\n    // By default, should not add this many nodes\n    if (maxHeap->size == MAX_SIZE) {\n        printf(\"heap is full!\");\n        return;\n    }\n    // Add node\n    maxHeap->data[maxHeap->size] = val;\n    maxHeap->size++;\n\n    // Heapify from bottom to top\n    siftUp(maxHeap, maxHeap->size - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(maxHeap, i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {\n            break;\n        }\n        // Swap two nodes\n        swap(maxHeap, i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.kt
/* Element enters heap */\nfun push(_val: Int) {\n    // Add node\n    maxHeap.add(_val)\n    // Heapify from bottom to top\n    siftUp(size() - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfun siftUp(it: Int) {\n    // Kotlin function parameters are immutable, so create temporary variable\n    var i = it\n    while (true) {\n        // Get parent node of node i\n        val p = parent(i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n        // Swap two nodes\n        swap(i, p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
my_heap.rb
### Push element to heap ###\ndef push(val)\n  # Add node\n  @max_heap << val\n  # Heapify from bottom to top\n  sift_up(size - 1)\nend\n\n### Heapify from node i, bottom to top ###\ndef sift_up(i)\n  loop do\n    # Get parent node of node i\n    p = parent(i)\n    # When \"crossing root node\" or \"node needs no repair\", end heapify\n    break if p < 0 || @max_heap[i] <= @max_heap[p]\n    # Swap two nodes\n    swap(i, p)\n    # Loop upward heapify\n    i = p\n  end\nend\n
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#4-removing-the-heap-top-element","level":3,"title":"4.   Removing the Heap Top Element","text":"

The heap top element is the root node of the binary tree, which is the first element of the list. If we directly remove the first element from the list, all node indexes in the binary tree would change, making subsequent repair with heapify difficult. To minimize changes in element indexes, we use the following steps.

  1. Swap the heap top element with the heap bottom element (swap the root node with the rightmost leaf node).
  2. After swapping, remove the heap bottom from the list (note that since we've swapped, we're actually removing the original heap top element).
  3. Starting from the root node, perform heapify from top to bottom.

As shown in Figure 8-4, the direction of \"top-to-bottom heapify\" is opposite to \"bottom-to-top heapify\". We compare the root node's value with its two children and swap it with the largest child. Then loop this operation until we pass a leaf node or encounter a node that doesn't need swapping.

<1><2><3><4><5><6><7><8><9><10>

Figure 8-4   Steps of removing the heap top element

Similar to the element insertion operation, the time complexity of the heap top element removal operation is also \\(O(\\log n)\\). The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def pop(self) -> int:\n    \"\"\"Element exits heap\"\"\"\n    # Handle empty case\n    if self.is_empty():\n        raise IndexError(\"Heap is empty\")\n    # Swap root node with rightmost leaf node (swap first element with last element)\n    self.swap(0, self.size() - 1)\n    # Delete node\n    val = self.max_heap.pop()\n    # Heapify from top to bottom\n    self.sift_down(0)\n    # Return top element\n    return val\n\ndef sift_down(self, i: int):\n    \"\"\"Starting from node i, heapify from top to bottom\"\"\"\n    while True:\n        # Find node with largest value among i, l, r, denoted as ma\n        l, r, ma = self.left(i), self.right(i), i\n        if l < self.size() and self.max_heap[l] > self.max_heap[ma]:\n            ma = l\n        if r < self.size() and self.max_heap[r] > self.max_heap[ma]:\n            ma = r\n        # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        if ma == i:\n            break\n        # Swap two nodes\n        self.swap(i, ma)\n        # Loop downward heapify\n        i = ma\n
my_heap.cpp
/* Element exits heap */\nvoid pop() {\n    // Handle empty case\n    if (isEmpty()) {\n        throw out_of_range(\"Heap is empty\");\n    }\n    // Delete node\n    swap(maxHeap[0], maxHeap[size() - 1]);\n    // Remove node\n    maxHeap.pop_back();\n    // Return top element\n    siftDown(0);\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // Swap two nodes\n        if (ma == i)\n            break;\n        swap(maxHeap[i], maxHeap[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.java
/* Element exits heap */\nint pop() {\n    // Handle empty case\n    if (isEmpty())\n        throw new IndexOutOfBoundsException();\n    // Delete node\n    swap(0, size() - 1);\n    // Remove node\n    int val = maxHeap.remove(size() - 1);\n    // Return top element\n    siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap.get(l) > maxHeap.get(ma))\n            ma = l;\n        if (r < size() && maxHeap.get(r) > maxHeap.get(ma))\n            ma = r;\n        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.cs
/* Element exits heap */\nint Pop() {\n    // Handle empty case\n    if (IsEmpty())\n        throw new IndexOutOfRangeException();\n    // Delete node\n    Swap(0, Size() - 1);\n    // Remove node\n    int val = maxHeap.Last();\n    maxHeap.RemoveAt(Size() - 1);\n    // Return top element\n    SiftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid SiftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = Left(i), r = Right(i), ma = i;\n        if (l < Size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < Size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // If 'node i is largest' or 'past leaf node', end heapify\n        if (ma == i) break;\n        // Swap two nodes\n        Swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.go
/* Element exits heap */\nfunc (h *maxHeap) pop() any {\n    // Handle empty case\n    if h.isEmpty() {\n        fmt.Println(\"error\")\n        return nil\n    }\n    // Delete node\n    h.swap(0, h.size()-1)\n    // Remove node\n    val := h.data[len(h.data)-1]\n    h.data = h.data[:len(h.data)-1]\n    // Return top element\n    h.siftDown(0)\n\n    // Return heap top element\n    return val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfunc (h *maxHeap) siftDown(i int) {\n    for true {\n        // Find node with maximum value among nodes i, l, r, denoted as max\n        l, r, max := h.left(i), h.right(i), i\n        if l < h.size() && h.data[l].(int) > h.data[max].(int) {\n            max = l\n        }\n        if r < h.size() && h.data[r].(int) > h.data[max].(int) {\n            max = r\n        }\n        // Swap two nodes\n        if max == i {\n            break\n        }\n        // Swap two nodes\n        h.swap(i, max)\n        // Loop downwards heapification\n        i = max\n    }\n}\n
my_heap.swift
/* Element exits heap */\nfunc pop() -> Int {\n    // Handle empty case\n    if isEmpty() {\n        fatalError(\"Heap is empty\")\n    }\n    // Delete node\n    swap(i: 0, j: size() - 1)\n    // Remove node\n    let val = maxHeap.remove(at: size() - 1)\n    // Return top element\n    siftDown(i: 0)\n    // Return heap top element\n    return val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfunc siftDown(i: Int) {\n    var i = i\n    while true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let l = left(i: i)\n        let r = right(i: i)\n        var ma = i\n        if l < size(), maxHeap[l] > maxHeap[ma] {\n            ma = l\n        }\n        if r < size(), maxHeap[r] > maxHeap[ma] {\n            ma = r\n        }\n        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        swap(i: i, j: ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n
my_heap.js
/* Element exits heap */\npop() {\n    // Handle empty case\n    if (this.isEmpty()) throw new Error('Heap is empty');\n    // Delete node\n    this.#swap(0, this.size() - 1);\n    // Remove node\n    const val = this.#maxHeap.pop();\n    // Return top element\n    this.#siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\n#siftDown(i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        const l = this.#left(i),\n            r = this.#right(i);\n        let ma = i;\n        if (l < this.size() && this.#maxHeap[l] > this.#maxHeap[ma]) ma = l;\n        if (r < this.size() && this.#maxHeap[r] > this.#maxHeap[ma]) ma = r;\n        // Swap two nodes\n        if (ma === i) break;\n        // Swap two nodes\n        this.#swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.ts
/* Element exits heap */\npop(): number {\n    // Handle empty case\n    if (this.isEmpty()) throw new RangeError('Heap is empty.');\n    // Delete node\n    this.swap(0, this.size() - 1);\n    // Remove node\n    const val = this.maxHeap.pop();\n    // Return top element\n    this.siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nsiftDown(i: number): void {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        const l = this.left(i),\n            r = this.right(i);\n        let ma = i;\n        if (l < this.size() && this.maxHeap[l] > this.maxHeap[ma]) ma = l;\n        if (r < this.size() && this.maxHeap[r] > this.maxHeap[ma]) ma = r;\n        // Swap two nodes\n        if (ma === i) break;\n        // Swap two nodes\n        this.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.dart
/* Element exits heap */\nint pop() {\n  // Handle empty case\n  if (isEmpty()) throw Exception('Heap is empty');\n  // Delete node\n  _swap(0, size() - 1);\n  // Remove node\n  int val = _maxHeap.removeLast();\n  // Return top element\n  siftDown(0);\n  // Return heap top element\n  return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n  while (true) {\n    // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n    int l = _left(i);\n    int r = _right(i);\n    int ma = i;\n    if (l < size() && _maxHeap[l] > _maxHeap[ma]) ma = l;\n    if (r < size() && _maxHeap[r] > _maxHeap[ma]) ma = r;\n    // Swap two nodes\n    if (ma == i) break;\n    // Swap two nodes\n    _swap(i, ma);\n    // Loop downwards heapification\n    i = ma;\n  }\n}\n
my_heap.rs
/* Element exits heap */\nfn pop(&mut self) -> i32 {\n    // Handle empty case\n    if self.is_empty() {\n        panic!(\"index out of bounds\");\n    }\n    // Delete node\n    self.swap(0, self.size() - 1);\n    // Remove node\n    let val = self.max_heap.pop().unwrap();\n    // Return top element\n    self.sift_down(0);\n    // Return heap top element\n    val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfn sift_down(&mut self, mut i: usize) {\n    loop {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let (l, r, mut ma) = (Self::left(i), Self::right(i), i);\n        if l < self.size() && self.max_heap[l] > self.max_heap[ma] {\n            ma = l;\n        }\n        if r < self.size() && self.max_heap[r] > self.max_heap[ma] {\n            ma = r;\n        }\n        // Swap two nodes\n        if ma == i {\n            break;\n        }\n        // Swap two nodes\n        self.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.c
/* Element exits heap */\nint pop(MaxHeap *maxHeap) {\n    // Handle empty case\n    if (isEmpty(maxHeap)) {\n        printf(\"heap is empty!\");\n        return INT_MAX;\n    }\n    // Delete node\n    swap(maxHeap, 0, size(maxHeap) - 1);\n    // Remove node\n    int val = maxHeap->data[maxHeap->size - 1];\n    maxHeap->size--;\n    // Return top element\n    siftDown(maxHeap, 0);\n\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Find node with maximum value among nodes i, l, r, denoted as max\n        int l = left(maxHeap, i);\n        int r = right(maxHeap, i);\n        int max = i;\n        if (l < size(maxHeap) && maxHeap->data[l] > maxHeap->data[max]) {\n            max = l;\n        }\n        if (r < size(maxHeap) && maxHeap->data[r] > maxHeap->data[max]) {\n            max = r;\n        }\n        // Swap two nodes\n        if (max == i) {\n            break;\n        }\n        // Swap two nodes\n        swap(maxHeap, i, max);\n        // Loop downwards heapification\n        i = max;\n    }\n}\n
my_heap.kt
/* Element exits heap */\nfun pop(): Int {\n    // Handle empty case\n    if (isEmpty()) throw IndexOutOfBoundsException()\n    // Delete node\n    swap(0, size() - 1)\n    // Remove node\n    val _val = maxHeap.removeAt(size() - 1)\n    // Return top element\n    siftDown(0)\n    // Return heap top element\n    return _val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfun siftDown(it: Int) {\n    // Kotlin function parameters are immutable, so create temporary variable\n    var i = it\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        val l = left(i)\n        val r = right(i)\n        var ma = i\n        if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n        if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n        // Swap two nodes\n        if (ma == i) break\n        // Swap two nodes\n        swap(i, ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n
my_heap.rb
### Pop element from heap ###\ndef pop\n  # Handle empty case\n  raise IndexError, \"Heap is empty\" if is_empty?\n  # Delete node\n  swap(0, size - 1)\n  # Remove node\n  val = @max_heap.pop\n  # Return top element\n  sift_down(0)\n  # Return heap top element\n  val\nend\n\n### Heapify from node i, top to bottom ###\ndef sift_down(i)\n  loop do\n    # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n    l, r, ma = left(i), right(i), i\n    ma = l if l < size && @max_heap[l] > @max_heap[ma]\n    ma = r if r < size && @max_heap[r] > @max_heap[ma]\n\n    # Swap two nodes\n    break if ma == i\n\n    # Swap two nodes\n    swap(i, ma)\n    # Loop downwards heapification\n    i = ma\n  end\nend\n
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#813-common-applications-of-heaps","level":2,"title":"8.1.3   Common Applications of Heaps","text":"
  • Priority queue: Heaps are typically the preferred data structure for implementing priority queues, with both enqueue and dequeue operations having a time complexity of \\(O(\\log n)\\), and the heap construction operation having \\(O(n)\\), all of which are highly efficient.
  • Heap sort: Given a set of data, we can build a heap with them and then continuously perform element removal operations to obtain sorted data. However, we usually use a more elegant approach to implement heap sort, as detailed in the \"Heap Sort\" chapter.
  • Getting the largest \\(k\\) elements: This is a classic algorithm problem and also a typical application, such as selecting the top 10 trending news for Weibo hot search, selecting the top 10 best-selling products, etc.
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/summary/","level":1,"title":"8.4   Summary","text":"","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • A heap is a complete binary tree that can be categorized as a max heap or min heap based on its property. The heap top element of a max heap (min heap) is the largest (smallest).
  • A priority queue is defined as a queue with priority sorting, typically implemented using heaps.
  • Common heap operations and their corresponding time complexities include: element insertion \\(O(\\log n)\\), heap top element removal \\(O(\\log n)\\), and accessing the heap top element \\(O(1)\\).
  • Complete binary trees are well-suited for array representation, so we typically use arrays to store heaps.
  • Heapify operations are used to maintain the heap property and are employed in both element insertion and removal operations.
  • The time complexity of building a heap with \\(n\\) input elements can be optimized to \\(O(n)\\), which is highly efficient.
  • Top-k is a classic algorithm problem that can be efficiently solved using the heap data structure, with a time complexity of \\(O(n \\log k)\\).
","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Are the \"heap\" in data structures and the \"heap\" in memory management the same concept?

The two are not the same concept; they just happen to share the name \"heap.\" The heap in computer system memory is part of dynamic memory allocation, where programs can use it to store data during runtime. Programs can request a certain amount of heap memory to store complex structures such as objects and arrays. When this data is no longer needed, the program needs to release this memory to prevent memory leaks. Compared to stack memory, heap memory management and usage require more caution, as improper use can lead to issues such as memory leaks and dangling pointers.

","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/top_k/","level":1,"title":"8.3   Top-K Problem","text":"

Question

Given an unordered array nums of length \\(n\\), return the largest \\(k\\) elements in the array.

For this problem, we'll first introduce two solutions with relatively straightforward approaches, then introduce a more efficient heap-based solution.

","path":["Chapter 8. Heap","8.3   Top-K Problem"],"tags":[]},{"location":"chapter_heap/top_k/#831-method-1-iterative-selection","level":2,"title":"8.3.1   Method 1: Iterative Selection","text":"

We can perform \\(k\\) rounds of traversal as shown in Figure 8-6, extracting the \\(1^{st}\\), \\(2^{nd}\\), \\(\\dots\\), \\(k^{th}\\) largest elements in each round, with a time complexity of \\(O(nk)\\).

This method is only suitable when \\(k \\ll n\\), because when \\(k\\) is close to \\(n\\), the time complexity approaches \\(O(n^2)\\), which is very time-consuming.

Figure 8-6   Traversing to find the largest k elements

Tip

When \\(k = n\\), we can obtain a complete sorted sequence, which is equivalent to the \"selection sort\" algorithm.

","path":["Chapter 8. Heap","8.3   Top-K Problem"],"tags":[]},{"location":"chapter_heap/top_k/#832-method-2-sorting","level":2,"title":"8.3.2   Method 2: Sorting","text":"

As shown in Figure 8-7, we can first sort the array nums, then return the rightmost \\(k\\) elements, with a time complexity of \\(O(n \\log n)\\).

Clearly, this method \"overachieves\" the task, as we only need to find the largest \\(k\\) elements, without needing to sort the other elements.

Figure 8-7   Sorting to find the largest k elements

","path":["Chapter 8. Heap","8.3   Top-K Problem"],"tags":[]},{"location":"chapter_heap/top_k/#833-method-3-heap","level":2,"title":"8.3.3   Method 3: Heap","text":"

We can solve the Top-k problem more efficiently using heaps, with the process shown in Figure 8-8.

  1. Initialize a min heap, where the heap top element is the smallest.
  2. First, insert the first \\(k\\) elements of the array into the heap in sequence.
  3. Starting from the \\((k + 1)^{th}\\) element, if the current element is greater than the heap top element, remove the heap top element and insert the current element into the heap.
  4. After traversal is complete, the heap contains the largest \\(k\\) elements.
<1><2><3><4><5><6><7><8><9>

Figure 8-8   Finding the largest k elements using a heap

Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby top_k.py
def top_k_heap(nums: list[int], k: int) -> list[int]:\n    \"\"\"Find the largest k elements in array based on heap\"\"\"\n    # Initialize min heap\n    heap = []\n    # Enter the first k elements of array into heap\n    for i in range(k):\n        heapq.heappush(heap, nums[i])\n    # Starting from the (k+1)th element, maintain heap length as k\n    for i in range(k, len(nums)):\n        # If current element is greater than top element, top element exits heap, current element enters heap\n        if nums[i] > heap[0]:\n            heapq.heappop(heap)\n            heapq.heappush(heap, nums[i])\n    return heap\n
top_k.cpp
/* Find the largest k elements in array based on heap */\npriority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {\n    // Python's heapq module implements min heap by default\n    priority_queue<int, vector<int>, greater<int>> heap;\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.push(nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.size(); i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.top()) {\n            heap.pop();\n            heap.push(nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.java
/* Find the largest k elements in array based on heap */\nQueue<Integer> topKHeap(int[] nums, int k) {\n    // Python's heapq module implements min heap by default\n    Queue<Integer> heap = new PriorityQueue<Integer>();\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.offer(nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.peek()) {\n            heap.poll();\n            heap.offer(nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.cs
/* Find the largest k elements in array based on heap */\nPriorityQueue<int, int> TopKHeap(int[] nums, int k) {\n    // Python's heapq module implements min heap by default\n    PriorityQueue<int, int> heap = new();\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.Enqueue(nums[i], nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.Length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.Peek()) {\n            heap.Dequeue();\n            heap.Enqueue(nums[i], nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.go
/* Find the largest k elements in array based on heap */\nfunc topKHeap(nums []int, k int) *minHeap {\n    // Python's heapq module implements min heap by default\n    h := &minHeap{}\n    heap.Init(h)\n    // Enter the first k elements of array into heap\n    for i := 0; i < k; i++ {\n        heap.Push(h, nums[i])\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for i := k; i < len(nums); i++ {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if nums[i] > h.Top().(int) {\n            heap.Pop(h)\n            heap.Push(h, nums[i])\n        }\n    }\n    return h\n}\n
top_k.swift
/* Find the largest k elements in array based on heap */\nfunc topKHeap(nums: [Int], k: Int) -> [Int] {\n    // Initialize min heap and build heap with first k elements\n    var heap = Heap(nums.prefix(k))\n    // Starting from the (k+1)th element, maintain heap length as k\n    for i in nums.indices.dropFirst(k) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if nums[i] > heap.min()! {\n            _ = heap.removeMin()\n            heap.insert(nums[i])\n        }\n    }\n    return heap.unordered\n}\n
top_k.js
/* Element enters heap */\nfunction pushMinHeap(maxHeap, val) {\n    // Negate element\n    maxHeap.push(-val);\n}\n\n/* Element exits heap */\nfunction popMinHeap(maxHeap) {\n    // Negate element\n    return -maxHeap.pop();\n}\n\n/* Access top element */\nfunction peekMinHeap(maxHeap) {\n    // Negate element\n    return -maxHeap.peek();\n}\n\n/* Extract elements from heap */\nfunction getMinHeap(maxHeap) {\n    // Negate element\n    return maxHeap.getMaxHeap().map((num) => -num);\n}\n\n/* Find the largest k elements in array based on heap */\nfunction topKHeap(nums, k) {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    const maxHeap = new MaxHeap([]);\n    // Enter the first k elements of array into heap\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (let i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Return elements in heap\n    return getMinHeap(maxHeap);\n}\n
top_k.ts
/* Element enters heap */\nfunction pushMinHeap(maxHeap: MaxHeap, val: number): void {\n    // Negate element\n    maxHeap.push(-val);\n}\n\n/* Element exits heap */\nfunction popMinHeap(maxHeap: MaxHeap): number {\n    // Negate element\n    return -maxHeap.pop();\n}\n\n/* Access top element */\nfunction peekMinHeap(maxHeap: MaxHeap): number {\n    // Negate element\n    return -maxHeap.peek();\n}\n\n/* Extract elements from heap */\nfunction getMinHeap(maxHeap: MaxHeap): number[] {\n    // Negate element\n    return maxHeap.getMaxHeap().map((num: number) => -num);\n}\n\n/* Find the largest k elements in array based on heap */\nfunction topKHeap(nums: number[], k: number): number[] {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    const maxHeap = new MaxHeap([]);\n    // Enter the first k elements of array into heap\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (let i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Return elements in heap\n    return getMinHeap(maxHeap);\n}\n
top_k.dart
/* Find the largest k elements in array based on heap */\nMinHeap topKHeap(List<int> nums, int k) {\n  // Initialize min heap, push first k elements of array to heap\n  MinHeap heap = MinHeap(nums.sublist(0, k));\n  // Starting from the (k+1)th element, maintain heap length as k\n  for (int i = k; i < nums.length; i++) {\n    // If current element is greater than top element, top element exits heap, current element enters heap\n    if (nums[i] > heap.peek()) {\n      heap.pop();\n      heap.push(nums[i]);\n    }\n  }\n  return heap;\n}\n
top_k.rs
/* Find the largest k elements in array based on heap */\nfn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {\n    // BinaryHeap is a max heap, use Reverse to negate elements to implement min heap\n    let mut heap = BinaryHeap::<Reverse<i32>>::new();\n    // Enter the first k elements of array into heap\n    for &num in nums.iter().take(k) {\n        heap.push(Reverse(num));\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for &num in nums.iter().skip(k) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if num > heap.peek().unwrap().0 {\n            heap.pop();\n            heap.push(Reverse(num));\n        }\n    }\n    heap\n}\n
top_k.c
/* Element enters heap */\nvoid pushMinHeap(MaxHeap *maxHeap, int val) {\n    // Negate element\n    push(maxHeap, -val);\n}\n\n/* Element exits heap */\nint popMinHeap(MaxHeap *maxHeap) {\n    // Negate element\n    return -pop(maxHeap);\n}\n\n/* Access top element */\nint peekMinHeap(MaxHeap *maxHeap) {\n    // Negate element\n    return -peek(maxHeap);\n}\n\n/* Extract elements from heap */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Negate all heap elements and store in res array\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n/* Extract elements from heap */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Negate all heap elements and store in res array\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n// Function to find k largest elements in array using heap\nint *topKHeap(int *nums, int sizeNums, int k) {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    int *empty = (int *)malloc(0);\n    MaxHeap *maxHeap = newMaxHeap(empty, 0);\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < sizeNums; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    int *res = getMinHeap(maxHeap);\n    // Free memory\n    delMaxHeap(maxHeap);\n    return res;\n}\n
top_k.kt
/* Find the largest k elements in array based on heap */\nfun topKHeap(nums: IntArray, k: Int): Queue<Int> {\n    // Python's heapq module implements min heap by default\n    val heap = PriorityQueue<Int>()\n    // Enter the first k elements of array into heap\n    for (i in 0..<k) {\n        heap.offer(nums[i])\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (i in k..<nums.size) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.peek()) {\n            heap.poll()\n            heap.offer(nums[i])\n        }\n    }\n    return heap\n}\n
top_k.rb
### Find largest k elements in array using heap ###\ndef top_k_heap(nums, k)\n  # Python's heapq module implements min heap by default\n  # Note: We negate all heap elements to simulate min heap using max heap\n  max_heap = MaxHeap.new([])\n\n  # Enter the first k elements of array into heap\n  for i in 0...k\n    push_min_heap(max_heap, nums[i])\n  end\n\n  # Starting from the (k+1)th element, maintain heap length as k\n  for i in k...nums.length\n    # If current element is greater than top element, top element exits heap, current element enters heap\n    if nums[i] > peek_min_heap(max_heap)\n      pop_min_heap(max_heap)\n      push_min_heap(max_heap, nums[i])\n    end\n  end\n\n  get_min_heap(max_heap)\nend\n

A total of \\(n\\) rounds of heap insertions and removals are performed, with the heap's maximum length being \\(k\\), so the time complexity is \\(O(n \\log k)\\). This method is very efficient; when \\(k\\) is small, the time complexity approaches \\(O(n)\\); when \\(k\\) is large, the time complexity does not exceed \\(O(n \\log n)\\).

Additionally, this method is suitable for dynamic data stream scenarios. By continuously adding data, we can maintain the elements in the heap, thus achieving dynamic updates of the largest \\(k\\) elements.

","path":["Chapter 8. Heap","8.3   Top-K Problem"],"tags":[]},{"location":"chapter_hello_algo/","level":1,"title":"Before Starting","text":"

A few years ago, I shared the \"Sword for Offer\" problem solutions on LeetCode, receiving encouragement and support from many readers. During interactions with readers, the most frequently asked question I encountered was \"how to get started with algorithms.\" Gradually, I developed a keen interest in this question.

Diving straight into problem-solving seems to be the most popular approach—it's simple, direct, and effective. However, problem-solving is like playing Minesweeper: those with strong self-learning abilities can successfully defuse the mines one by one, while those with insufficient foundations may end up bruised and battered, retreating step by step in frustration. Reading through textbooks is also a common practice, but for job seekers, graduation theses, resume submissions, and preparations for written tests and interviews have already consumed most of their energy, making working through thick books an arduous challenge.

If you're facing similar struggles, then it's fortunate that this book has \"found\" you. This book is my answer to this question—even if it may not be the optimal solution, it is at least a positive attempt. While this book alone won't directly land you a job offer, it will guide you to explore the \"knowledge map\" of data structures and algorithms, help you understand the shapes, sizes, and distributions of different \"mines,\" and enable you to master various \"mine-clearing methods.\" With these skills, I believe you can tackle problems and read technical literature more confidently, gradually building a complete knowledge system.

I deeply agree with Professor Feynman's words: \"Knowledge isn't free. You have to pay attention.\" In this sense, this book is not entirely \"free.\" In order to live up to the precious \"attention\" you invest in this book, I will do my utmost and devote my greatest \"attention\" to completing this work.

I'm acutely aware of my limited knowledge and shallow expertise. Although the content of this book has been refined over a period of time, there are certainly still many errors, and I sincerely welcome critiques and corrections from teachers and fellow students.

Hello, Algorithms!

The advent of computers has brought tremendous changes to the world. With their high-speed computing capabilities and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics in video games, the intelligent decision-making in autonomous driving, AlphaGo's brilliant Go matches, or ChatGPT's natural interactions, these applications are all exquisite interpretations of algorithms on computers.

In fact, before the advent of computers, algorithms and data structures already existed in every corner of the world. Early algorithms were relatively simple, such as ancient counting methods and tool-making procedures. As civilization progressed, algorithms gradually became more refined and complex. From the ingenious craftsmanship of master artisans, to industrial products that liberate productive forces, to the scientific laws governing the operation of the universe, behind almost every ordinary or astonishing thing lies ingenious algorithmic thinking.

Similarly, data structures are everywhere: from large-scale social networks to small subway systems, many systems can be modeled as \"graphs\"; from a nation to a family, the primary organizational forms of society exhibit characteristics of \"trees\"; winter clothing is like a \"stack,\" where the first item put on is the last to be taken off; a badminton tube is like a \"queue,\" with items inserted at one end and retrieved from the other; a dictionary is like a \"hash table,\" enabling quick lookup of target entries.

This book aims to help readers understand the core concepts of algorithms and data structures through clear and accessible animated illustrations and runnable code examples, and to implement them through programming. Building on this foundation, the book endeavors to reveal the vivid manifestations of algorithms in the complex world and showcase the beauty of algorithms. I hope this book can be of help to you!

","path":["Before Starting"],"tags":[]},{"location":"chapter_introduction/","level":1,"title":"Chapter 1.   Encounter with Algorithms","text":"

Abstract

A young girl dances gracefully, intertwined with data, her skirt flowing with the melody of algorithms.

She invites you to dance with her. Follow her steps closely and enter the world of algorithms, full of logic and beauty.

","path":["Chapter 1. Encounter With Algorithms","Chapter 1.   Encounter with Algorithms"],"tags":[]},{"location":"chapter_introduction/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 1.1   Algorithms Are Everywhere
  • 1.2   What Is an Algorithm
  • 1.3   Summary
","path":["Chapter 1. Encounter With Algorithms","Chapter 1.   Encounter with Algorithms"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   Algorithms Are Everywhere","text":"

When we hear the term \"algorithm,\" we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives.

Before we start discussing about algorithms officially, there's an interesting fact worth sharing: you've learned many algorithms unconsciously and are used to applying them in your daily life. Here, I will give a few specific examples to prove this point.

Example 1: Looking Up a Dictionary. In an English dictionary, words are listed alphabetically. Assuming we're searching for a word that starts with the letter \\(r\\), this is typically done in the following way:

  1. Open the dictionary to about halfway and check the first vocabulary of the page, let's say the letter starts with \\(m\\).
  2. Since \\(r\\) comes after \\(m\\) in the alphabet, the first half can be ignored and the search space is narrowed down to the second half.
  3. Repeat steps 1. and 2. until you find the page where the word starts with \\(r\\).
<1><2><3><4><5>

Figure 1-1   Process of looking up a dictionary

Looking up a dictionary, an essential skill for elementary school students is actually the famous \"Binary Search\" algorithm. From a data structure perspective, we can consider the dictionary as a sorted \"array\"; from an algorithmic perspective, the series of actions taken to look up a word in the dictionary can be viewed as the algorithm \"Binary Search.\"

Example 2: Organizing Card Deck. When playing cards, we need to arrange the cards in our hands in ascending order, as shown in the following process.

  1. Divide the playing cards into \"ordered\" and \"unordered\" sections, assuming initially the leftmost card is already in order.
  2. Take out a card from the unordered section and insert it into the correct position in the ordered section; after this, the leftmost two cards are in order.
  3. Repeat step 2 until all cards are in order.

Figure 1-2   Process of sorting a deck of cards

The above method of organizing playing cards is practically the \"Insertion Sort\" algorithm, which is very efficient for small datasets. Many programming languages' sorting functions include the insertion sort.

Example 3: Making Change. Assume making a purchase of \\(69\\) at a supermarket. If you give the cashier \\(100\\), they will need to provide you with \\(31\\) in change. This process can be clearly understood as illustrated in Figure 1-3.

  1. The options are currencies valued below \\(31\\), including \\(1\\), \\(5\\), \\(10\\), and \\(20\\).
  2. Take out the largest \\(20\\) from the options, leaving \\(31 - 20 = 11\\).
  3. Take out the largest \\(10\\) from the remaining options, leaving \\(11 - 10 = 1\\).
  4. Take out the largest \\(1\\) from the remaining options, leaving \\(1 - 1 = 0\\).
  5. Complete change-making, the solution is \\(20 + 10 + 1 = 31\\).

Figure 1-3   Process of making change

In the steps described, we choose the best option at each stage by utilizing the largest denomination available, which leads to an effective change-making strategy. From a data structures and algorithms perspective, this approach is known as a \"Greedy\" algorithm.

From cooking a meal to interstellar travel, almost all problem-solving involves algorithms. The advent of computers allows us to store data structures in memory and write code to call the CPU and GPU to execute algorithms. In this way, we can transfer real-life problems to computers and solve various complex issues in a more efficient way.

Tip

If you are still confused about concepts like data structures, algorithms, arrays, and binary searches, I encourage you to keep reading. This book will gently guide you into the realm of understanding data structures and algorithms.

","path":["Chapter 1. Encounter With Algorithms","1.1   Algorithms Are Everywhere"],"tags":[]},{"location":"chapter_introduction/summary/","level":1,"title":"1.3   Summary","text":"","path":["Chapter 1. Encounter With Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Algorithms are ubiquitous in daily life and are not distant, esoteric knowledge. In fact, we have already learned many algorithms unconsciously and use them to solve problems big and small in life.
  • The principle of looking up a dictionary is consistent with the binary search algorithm. Binary search embodies the important algorithmic idea of divide and conquer.
  • The process of organizing playing cards is very similar to the insertion sort algorithm. Insertion sort is suitable for sorting small datasets.
  • The steps of making change are essentially a greedy algorithm, where the best choice is made at each step based on the current situation.
  • An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time, while a data structure is the way computers organize and store data.
  • Data structures and algorithms are closely connected. Data structures are the foundation of algorithms, and algorithms breathe life into data structures.
  • We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the shape and connection method of the blocks represent the data structure, and the steps to assemble the blocks correspond to the algorithm.
","path":["Chapter 1. Encounter With Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: As a programmer, I have never used algorithms to solve problems in my daily work. Common algorithms are already encapsulated by programming languages and can be used directly. Does this mean that the problems in our work have not yet reached the level where algorithms are needed?

If we compare specific work skills to \"techniques\" in martial arts, then fundamental subjects should be more like \"internal skills\".

I believe the significance of learning algorithms (and other fundamental subjects) is not to implement them from scratch at work, but rather to be able to make professional reactions and judgments when solving problems based on the knowledge learned, thereby improving the overall quality of work. Here is a simple example. Every programming language has a built-in sorting function:

  • If we have not studied data structures and algorithms, we might simply feed any given data to this sorting function. It runs smoothly with good performance, and there doesn't seem to be any problem.
  • But if we have studied algorithms, we would know that the time complexity of the built-in sorting function is \\(O(n \\log n)\\). However, if the given data consists of integers with a fixed number of digits (such as student IDs), we can use the more efficient \"radix sort\", reducing the time complexity to \\(O(nk)\\), where \\(k\\) is the number of digits. When the data volume is very large, the saved running time can create significant value (reduced costs, improved experience, etc.).

In the field of engineering, a large number of problems are difficult to reach optimal solutions, and many problems are only solved \"approximately\". The difficulty of a problem depends on one hand on the nature of the problem itself, and on the other hand on the knowledge reserve of the person observing the problem. The more complete a person's knowledge and the more experience they have, the deeper their analysis of the problem will be, and the more elegantly the problem can be solved.

","path":["Chapter 1. Encounter With Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/","level":1,"title":"1.2   What Is an Algorithm","text":"","path":["Chapter 1. Encounter With Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#121-algorithm-definition","level":2,"title":"1.2.1   Algorithm Definition","text":"

An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time. It has the following characteristics.

  • The problem is well-defined, with clear input and output definitions.
  • It is feasible and can be completed within a finite number of steps, time, and memory space.
  • Each step has a definite meaning, and under the same input and operating conditions, the output is always the same.
","path":["Chapter 1. Encounter With Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#122-data-structure-definition","level":2,"title":"1.2.2   Data Structure Definition","text":"

A data structure is a way of organizing and storing data, covering the data content, relationships between data, and methods for data operations. It has the following design objectives.

  • Occupy as little space as possible to save computer memory.
  • Data operations should be as fast as possible, covering data access, addition, deletion, update, etc.
  • Provide a concise data representation and logical information so that algorithms can run efficiently.

Data structure design is a process full of trade-offs. If we want to achieve improvements in one aspect, we often need to make compromises in another aspect. Here are two examples.

  • Compared to arrays, linked lists are more convenient for data addition and deletion operations but sacrifice data access speed.
  • Compared to linked lists, graphs provide richer logical information but require larger memory space.
","path":["Chapter 1. Encounter With Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123-the-relationship-between-data-structures-and-algorithms","level":2,"title":"1.2.3   The Relationship Between Data Structures and Algorithms","text":"

As shown in Figure 1-4, data structures and algorithms are highly related and tightly coupled, specifically manifested in the following three aspects.

  • Data structures are the foundation of algorithms. Data structures provide algorithms with structured storage of data and methods for operating on data.
  • Algorithms breathe life into data structures. Data structures themselves only store data information; combined with algorithms, they can solve specific problems.
  • Algorithms can usually be implemented based on different data structures, but execution efficiency may vary greatly. Choosing the appropriate data structure is key.

Figure 1-4   The relationship between data structures and algorithms

Data structures and algorithms are like assembling building blocks as shown in Figure 1-5. A set of building blocks, in addition to containing many parts, also comes with detailed assembly instructions. By following the instructions step by step, we can assemble an exquisite building block model.

Figure 1-5   Assembling blocks

The detailed correspondence between the two is shown in Table 1-1.

Table 1-1   Comparing data structures and algorithms to assembling building blocks

Data structures and algorithms Assembling building blocks Input data Unassembled building blocks Data structure Organization form of building blocks, including shape, size, connection method, etc. Algorithm A series of operational steps to assemble the blocks into the target form Output data Building block model

It is worth noting that data structures and algorithms are independent of programming languages. For this reason, this book is able to provide implementations based on multiple programming languages.

Conventional abbreviation

In actual discussions, we usually abbreviate \"data structures and algorithms\" as \"algorithms\". For example, the well-known LeetCode algorithm problems actually examine knowledge of both data structures and algorithms.

","path":["Chapter 1. Encounter With Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"Chapter 0.   Preface","text":"

Abstract

Algorithms are like a beautiful symphony, each line of code flows like a melody.

May this book gently resonate in your mind, leaving a unique and profound melody.

","path":["Chapter 0. Preface","Chapter 0.   Preface"],"tags":[]},{"location":"chapter_preface/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 0.1   About This Book
  • 0.2   How to Use This Book
  • 0.3   Summary
","path":["Chapter 0. Preface","Chapter 0.   Preface"],"tags":[]},{"location":"chapter_preface/about_the_book/","level":1,"title":"0.1   About This Book","text":"

This project aims to create an open-source, free, beginner-friendly introductory tutorial on data structures and algorithms.

  • The entire book uses animated illustrations, with clear and easy-to-understand content and a smooth learning curve, guiding beginners to explore the knowledge map of data structures and algorithms.
  • The source code can be run with one click, helping readers improve their programming skills through practice and understand how algorithms work and the underlying implementation of data structures.
  • We encourage readers to learn from each other, and everyone is welcome to ask questions and share insights in the comments section, making progress together through discussion and exchange.
","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#011-target-audience","level":2,"title":"0.1.1   Target Audience","text":"

If you are an algorithm beginner who has never been exposed to algorithms, or if you already have some problem-solving experience and have a vague understanding of data structures and algorithms, oscillating between knowing and not knowing, then this book is tailor-made for you!

If you have already accumulated a certain amount of problem-solving experience and are familiar with most question types, this book can help you review and organize your algorithm knowledge system, and the repository's source code can be used as a \"problem-solving toolkit\" or \"algorithm dictionary.\"

If you are an algorithm \"expert,\" we look forward to receiving your valuable suggestions, or participating in creation together.

Prerequisites

You need to have at least a programming foundation in any language, and be able to read and write simple code.

","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#012-content-structure","level":2,"title":"0.1.2   Content Structure","text":"

The main content of this book is shown in Figure 0-1.

  • Complexity analysis: Evaluation dimensions and methods for data structures and algorithms. Methods for calculating time complexity and space complexity, common types, examples, etc.
  • Data structures: Classification methods for basic data types and data structures. The definition, advantages and disadvantages, common operations, common types, typical applications, implementation methods, etc. of data structures such as arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs.
  • Algorithms: The definition, advantages and disadvantages, efficiency, application scenarios, problem-solving steps, and example problems of algorithms such as searching, sorting, divide and conquer, backtracking, dynamic programming, and greedy algorithms.

Figure 0-1   Main content of this book

","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#013-acknowledgements","level":2,"title":"0.1.3   Acknowledgements","text":"

This book has been continuously improved through the joint efforts of many contributors in the open-source community. Thanks to every contributor who invested time and effort, they are (in the order automatically generated by GitHub): krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, IsChristina, xBLACKICEx, guowei-gong, Cathay-Chen, pengchzn, QiLOL, magentaqin, hello-ikun, JoseHung, qualifier1024, thomasq0, sunshinesDL, L-Super, Guanngxu, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, Shyam-Chen, theNefelibatas, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, MolDuM, Nigh, Dr-XYZ, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, beatrix-chan, DullSword, xjr7670, jiaxianhua, qq909244296, iStig, boloboloda, hts0000, gledfish, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, llql1211, fbigm, echo1937, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, GaochaoZhu, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Allen-Scai, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, zhongfq, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Keynman, KeiichiKasai, and KawaiiAsh.

The code review work for this book was completed by coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (in alphabetical order). Thanks to them for the time and effort they put in, it is they who ensure the standardization and unity of code in various languages.

The Traditional Chinese version of this book was reviewed by Shyam-Chen and Dr-XYZ, the English version was reviewed by yuelinxin, K3v123, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn, thomasq0 and magentaqin, and the Japanese edition was reviewed by eltociear. It is because of their continuous contributions that this book can serve a wider readership, and we thank them.

The ePub ebook generation tool for this book was developed by zhongfq. We thank him for his contribution, which provides readers with a more flexible way to read.

During the creation of this book, I received help from many people.

  • Thanks to my mentor at the company, Dr. Li Xi, who encouraged me to \"take action quickly\" during a conversation, strengthening my determination to write this book;
  • Thanks to my girlfriend Bubble as the first reader of this book, who provided many valuable suggestions from the perspective of an algorithm beginner, making this book more suitable for novices to read;
  • Thanks to Tengbao, Qibao, and Feibao for coming up with a creative name for this book, evoking everyone's fond memories of writing their first line of code \"Hello World!\";
  • Thanks to Xiaoquan for providing professional help in intellectual property rights, which played an important role in the improvement of this open-source book;
  • Thanks to Sutong for designing the beautiful cover and logo for this book, and for patiently making revisions multiple times driven by my obsessive-compulsive disorder;
  • Thanks to @squidfunk for the typesetting suggestions, as well as for developing the open-source documentation theme Material-for-MkDocs.

During the writing process, I read many textbooks and articles on data structures and algorithms. These works provided excellent examples for this book and ensured the accuracy and quality of the book's content. I would like to thank all the teachers and predecessors for their outstanding contributions!

This book advocates a learning method that combines hands and brain, and in this regard I was deeply inspired by Dive into Deep Learning. I highly recommend this excellent work to all readers.

Heartfelt thanks to my parents, it is your support and encouragement that has given me the opportunity to do this interesting thing.

","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/suggestions/","level":1,"title":"0.2   How to Use This Book","text":"

Tip

For the best reading experience, it is recommended that you read through this section.

","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#021-writing-style-conventions","level":2,"title":"0.2.1   Writing Style Conventions","text":"
  • Titles marked with * are optional sections with relatively difficult content. If you have limited time, you can skip them first.
  • Technical terms will be in bold (in paper and PDF versions) or underlined (in web versions), such as array. It is recommended to memorize them for reading literature.
  • Key content and summary statements will be bolded, and such text deserves special attention.
  • Words and phrases with specific meanings will be marked with \"quotation marks\" to avoid ambiguity.
  • When it comes to nouns that are inconsistent between programming languages, this book uses Python as the standard, for example, using None to represent \"null\".
  • This book partially abandons the comment conventions of programming languages in favor of more compact content layout. Comments are mainly divided into three types: title comments, content comments, and multi-line comments.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
\"\"\"Title comment, used to label functions, classes, test cases, etc.\"\"\"\n\n# Content comment, used to explain code in detail\n\n\"\"\"\nMulti-line\ncomment\n\"\"\"\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n// Multi-line\n// comment\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
### Title comment, used to label functions, classes, test cases, etc. ###\n\n# Content comment, used to explain code in detail\n\n# Multi-line\n# comment\n
","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#022-learning-efficiently-with-animated-illustrations","level":2,"title":"0.2.2   Learning Efficiently with Animated Illustrations","text":"

Compared to text, videos and images have higher information density and structural organization, making them easier to understand. In this book, key and difficult knowledge will mainly be presented in the form of animated illustrations, with text serving as explanation and supplement.

If you find that a section of content provides animated illustrations as shown in Figure 0-2 while reading this book, please focus on the illustrations first, with text as a supplement, and combine the two to understand the content.

Figure 0-2   Example of animated illustrations

","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#023-deepening-understanding-through-code-practice","level":2,"title":"0.2.3   Deepening Understanding Through Code Practice","text":"

The accompanying code for this book is hosted in the GitHub repository. As shown in Figure 0-3, the source code comes with test cases and can be run with one click.

If time permits, it is recommended that you type out the code yourself. If you have limited study time, please at least read through and run all the code.

Compared to reading code, the process of writing code often brings more rewards. Learning by doing is the real learning.

Figure 0-3   Example of running code

The preliminary work for running code is mainly divided into three steps.

Step 1: Install the local programming environment. Please follow the tutorial shown in the appendix for installation. If already installed, you can skip this step.

Step 2: Clone or download the code repository. Visit the GitHub repository. If you have already installed Git, you can clone this repository with the following command:

git clone https://github.com/krahets/hello-algo.git\n

Of course, you can also click the \"Download ZIP\" button at the location shown in Figure 0-4 to directly download the code compressed package, and then extract it locally.

Figure 0-4   Clone repository and download code

Step 3: Run the source code. As shown in Figure 0-5, for code blocks with file names at the top, we can find the corresponding source code files in the codes folder of the repository. The source code files can be run with one click, which will help you save unnecessary debugging time and allow you to focus on learning content.

Figure 0-5   Code blocks and corresponding source code files

In addition to running code locally, the web version also supports visual running of Python code (implemented based on pythontutor). As shown in Figure 0-6, you can click \"Visual Run\" below the code block to expand the view and observe the execution process of the algorithm code; you can also click \"Full Screen View\" for a better viewing experience.

Figure 0-6   Visual running of Python code

","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#024-growing-together-through-questions-and-discussions","level":2,"title":"0.2.4   Growing Together Through Questions and Discussions","text":"

When reading this book, please do not easily skip knowledge points that you have not learned well. Feel free to ask your questions in the comments section, and my friends and I will do our best to answer you, and generally reply within two days.

As shown in Figure 0-7, the web version has a comments section at the bottom of each chapter. I hope you will pay more attention to the content of the comments section. On the one hand, you can learn about the problems that everyone encounters, thus checking for omissions and stimulating deeper thinking. On the other hand, I hope you can generously answer other friends' questions, share your insights, and help others progress.

Figure 0-7   Example of comments section

","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#025-algorithm-learning-roadmap","level":2,"title":"0.2.5   Algorithm Learning Roadmap","text":"

From an overall perspective, we can divide the process of learning data structures and algorithms into three stages.

  1. Stage 1: Algorithm introduction. We need to familiarize ourselves with the characteristics and usage of various data structures, and learn the principles, processes, uses, and efficiency of different algorithms.
  2. Stage 2: Practice algorithm problems. It is recommended to start with popular problems, and accumulate at least 100 problems first, to familiarize yourself with mainstream algorithm problems. When first practicing problems, \"knowledge forgetting\" may be a challenge, but rest assured, this is very normal. We can review problems according to the \"Ebbinghaus forgetting curve\", and usually after 3-5 rounds of repetition, we can firmly remember them. For recommended problem lists and practice plans, please see this GitHub repository.
  3. Stage 3: Building a knowledge system. In terms of learning, we can read algorithm column articles, problem-solving frameworks, and algorithm textbooks to continuously enrich our knowledge system. In terms of practicing problems, we can try advanced problem-solving strategies, such as categorization by topic, one problem multiple solutions, one solution multiple problems, etc. Related problem-solving insights can be found in various communities.

As shown in Figure 0-8, the content of this book mainly covers \"Stage 1\", aiming to help you more efficiently carry out Stage 2 and Stage 3 learning.

Figure 0-8   Algorithm learning roadmap

","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/summary/","level":1,"title":"0.3   Summary","text":"","path":["Chapter 0. Preface","0.3   Summary"],"tags":[]},{"location":"chapter_preface/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • The main audience of this book is algorithm beginners. If you already have a certain foundation, this book can help you systematically review algorithm knowledge, and the source code in the book can also be used as a \"problem-solving toolkit.\"
  • The content of the book mainly includes three parts: complexity analysis, data structures, and algorithms, covering most topics in this field.
  • For algorithm novices, reading an introductory book during the initial learning stage is crucial, as it can help you avoid many detours.
  • The animated illustrations in the book are usually used to introduce key and difficult knowledge. When reading this book, you should pay more attention to these contents.
  • Practice is the best way to learn programming. It is strongly recommended to run the source code and type the code yourself.
  • The web version of this book has a comments section for each chapter, where you are welcome to share your questions and insights at any time.
","path":["Chapter 0. Preface","0.3   Summary"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"References","text":"

[1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition).

[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

[3] Robert Sedgewick, et al. Algorithms (4th Edition).

[4] Yan Weimin. Data Structures (C Language Version).

[5] Deng Junhui. Data Structures (C++ Language Version, Third Edition).

[6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition).

[7] Cheng Jie. Conversational Data Structures.

[8] Wang Zheng. The Beauty of Data Structures and Algorithms.

[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

[10] Aston Zhang, et al. Dive into Deep Learning.

","path":["References"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"Chapter 10.   Searching","text":"

Abstract

Searching is an adventure into the unknown, where we may need to traverse every corner of the mysterious space, or we may be able to quickly lock onto the target.

In this journey of discovery, each exploration may yield an unexpected answer.

","path":["Chapter 10. Searching","Chapter 10.   Searching"],"tags":[]},{"location":"chapter_searching/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 10.1   Binary Search
  • 10.2   Binary Search Insertion
  • 10.3   Binary Search Edge Cases
  • 10.4   Hash Optimization Strategy
  • 10.5   Search Algorithms Revisited
  • 10.6   Summary
","path":["Chapter 10. Searching","Chapter 10.   Searching"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   Binary Search","text":"

Binary search is an efficient searching algorithm based on the divide-and-conquer strategy. It leverages the orderliness of data to reduce the search range by half in each round until the target element is found or the search interval becomes empty.

Question

Given an array nums of length \\(n\\) with elements arranged in ascending order and no duplicates, search for and return the index of element target in the array. If the array does not contain the element, return \\(-1\\). An example is shown in Figure 10-1.

Figure 10-1   Binary search example data

As shown in Figure 10-2, we first initialize pointers \\(i = 0\\) and \\(j = n - 1\\), pointing to the first and last elements of the array respectively, representing the search interval \\([0, n - 1]\\). Note that square brackets denote a closed interval, which includes the boundary values themselves.

Next, perform the following two steps in a loop:

  1. Calculate the midpoint index \\(m = \\lfloor {(i + j) / 2} \\rfloor\\), where \\(\\lfloor \\: \\rfloor\\) denotes the floor operation.
  2. Compare nums[m] and target, which results in three cases:
    1. When nums[m] < target, it indicates that target is in the interval \\([m + 1, j]\\), so execute \\(i = m + 1\\).
    2. When nums[m] > target, it indicates that target is in the interval \\([i, m - 1]\\), so execute \\(j = m - 1\\).
    3. When nums[m] = target, it indicates that target has been found, so return index \\(m\\).

If the array does not contain the target element, the search interval will eventually shrink to empty. In this case, return \\(-1\\).

<1><2><3><4><5><6><7>

Figure 10-2   Binary search process

It's worth noting that since both \\(i\\) and \\(j\\) are of int type, \\(i + j\\) may exceed the range of the int type. To avoid large number overflow, we typically use the formula \\(m = \\lfloor {i + (j - i) / 2} \\rfloor\\) to calculate the midpoint.

The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
def binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Binary search (closed interval)\"\"\"\n    # Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    i, j = 0, len(nums) - 1\n    # Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j:\n        # In theory, Python numbers can be infinitely large (depending on memory size), no need to consider large number overflow\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # This means target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # This means target is in the interval [i, m-1]\n        else:\n            return m  # Found the target element, return its index\n    return -1  # Target element not found, return -1\n
binary_search.cpp
/* Binary search (closed interval on both sides) */\nint binarySearch(vector<int> &nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.size() - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.java
/* Binary search (closed interval on both sides) */\nint binarySearch(int[] nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.cs
/* Binary search (closed interval on both sides) */\nint BinarySearch(int[] nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.Length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2;   // Calculate the midpoint index m\n        if (nums[m] < target)      // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else                       // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.go
/* Binary search (closed interval on both sides) */\nfunc binarySearch(nums []int, target int) int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    i, j := 0, len(nums)-1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    for i <= j {\n        m := i + (j-i)/2      // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m-1]\n            j = m - 1\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.swift
/* Binary search (closed interval on both sides) */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m-1]\n            j = m - 1\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.js
/* Binary search (closed interval on both sides) */\nfunction binarySearch(nums, target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let i = 0,\n        j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        // Calculate midpoint index m, use parseInt() to round down\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target)\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else return m; // Found the target element, return its index\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.ts
/* Binary search (closed interval on both sides) */\nfunction binarySearch(nums: number[], target: number): number {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let i = 0,\n        j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        // Calculate the midpoint index m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    return -1; // Target element not found, return -1\n}\n
binary_search.dart
/* Binary search (closed interval on both sides) */\nint binarySearch(List<int> nums, int target) {\n  // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n  int i = 0, j = nums.length - 1;\n  // Loop, exit when the search interval is empty (empty when i > j)\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      // This means target is in the interval [m+1, j]\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // This means target is in the interval [i, m-1]\n      j = m - 1;\n    } else {\n      // Found the target element, return its index\n      return m;\n    }\n  }\n  // Target element not found, return -1\n  return -1;\n}\n
binary_search.rs
/* Binary search (closed interval on both sides) */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let mut i = 0;\n    let mut j = nums.len() as i32 - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.c
/* Binary search (closed interval on both sides) */\nint binarySearch(int *nums, int len, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = len - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.kt
/* Binary search (closed interval on both sides) */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    var i = 0\n    var j = nums.size - 1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j]\n            i = m + 1\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1\n        else  // Found the target element, return its index\n            return m\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.rb
### Binary search (closed interval) ###\ndef binary_search(nums, target)\n  # Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n  i, j = 0, nums.length - 1\n\n  # Loop, exit when the search interval is empty (empty when i > j)\n  while i <= j\n    # In theory, Ruby numbers can be infinitely large (limited by memory), no need to consider overflow\n    m = (i + j) / 2   # Calculate the midpoint index m\n\n    if nums[m] < target\n      i = m + 1 # This means target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # This means target is in the interval [i, m-1]\n    else\n      return m  # Found the target element, return its index\n    end\n  end\n\n  -1  # Target element not found, return -1\nend\n

Time complexity is \\(O(\\log n)\\): In the binary loop, the interval is reduced by half each round, so the number of loops is \\(\\log_2 n\\).

Space complexity is \\(O(1)\\): Pointers \\(i\\) and \\(j\\) use constant-size space.

","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search/#1011-interval-representation-methods","level":2,"title":"10.1.1   Interval Representation Methods","text":"

In addition to the closed interval mentioned above, another common interval representation is the \"left-closed right-open\" interval, defined as \\([0, n)\\), meaning the left boundary includes itself while the right boundary does not. Under this representation, the interval \\([i, j)\\) is empty when \\(i = j\\).

We can implement a binary search algorithm with the same functionality based on this representation:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
def binary_search_lcro(nums: list[int], target: int) -> int:\n    \"\"\"Binary search (left-closed right-open interval)\"\"\"\n    # Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    i, j = 0, len(nums)\n    # Loop, exit when the search interval is empty (empty when i = j)\n    while i < j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # This means target is in the interval [m+1, j)\n        elif nums[m] > target:\n            j = m  # This means target is in the interval [i, m)\n        else:\n            return m  # Found the target element, return its index\n    return -1  # Target element not found, return -1\n
binary_search.cpp
/* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(vector<int> &nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.size();\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.java
/* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(int[] nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.cs
/* Binary search (left-closed right-open interval) */\nint BinarySearchLCRO(int[] nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.Length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2;   // Calculate the midpoint index m\n        if (nums[m] < target)      // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else                       // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.go
/* Binary search (left-closed right-open interval) */\nfunc binarySearchLCRO(nums []int, target int) int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    i, j := 0, len(nums)\n    // Loop, exit when the search interval is empty (empty when i = j)\n    for i < j {\n        m := i + (j-i)/2      // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m)\n            j = m\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.swift
/* Binary search (left-closed right-open interval) */\nfunc binarySearchLCRO(nums: [Int], target: Int) -> Int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    var i = nums.startIndex\n    var j = nums.endIndex\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while i < j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m)\n            j = m\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.js
/* Binary search (left-closed right-open interval) */\nfunction binarySearchLCRO(nums, target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let i = 0,\n        j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        // Calculate midpoint index m, use parseInt() to round down\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target)\n            // This means target is in the interval [i, m)\n            j = m;\n        // Found the target element, return its index\n        else return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.ts
/* Binary search (left-closed right-open interval) */\nfunction binarySearchLCRO(nums: number[], target: number): number {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let i = 0,\n        j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        // Calculate the midpoint index m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // This means target is in the interval [i, m)\n            j = m;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    return -1; // Target element not found, return -1\n}\n
binary_search.dart
/* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(List<int> nums, int target) {\n  // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n  int i = 0, j = nums.length;\n  // Loop, exit when the search interval is empty (empty when i = j)\n  while (i < j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      // This means target is in the interval [m+1, j)\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // This means target is in the interval [i, m)\n      j = m;\n    } else {\n      // Found the target element, return its index\n      return m;\n    }\n  }\n  // Target element not found, return -1\n  return -1;\n}\n
binary_search.rs
/* Binary search (left-closed right-open interval) */\nfn binary_search_lcro(nums: &[i32], target: i32) -> i32 {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let mut i = 0;\n    let mut j = nums.len() as i32;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while i < j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // This means target is in the interval [i, m)\n            j = m;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.c
/* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(int *nums, int len, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = len;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.kt
/* Binary search (left-closed right-open interval) */\nfun binarySearchLCRO(nums: IntArray, target: Int): Int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    var i = 0\n    var j = nums.size\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j)\n            i = m + 1\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m\n        else  // Found the target element, return its index\n            return m\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.rb
### Binary search (left-closed right-open interval) ###\ndef binary_search_lcro(nums, target)\n  # Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n  i, j = 0, nums.length\n\n  # Loop, exit when the search interval is empty (empty when i = j)\n  while i < j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # This means target is in the interval [m+1, j)\n    elsif nums[m] > target\n      j = m - 1 # This means target is in the interval [i, m)\n    else\n      return m  # Found the target element, return its index\n    end\n  end\n\n  -1  # Target element not found, return -1\nend\n

As shown in Figure 10-3, under the two interval representations, the initialization, loop condition, and interval narrowing operations of the binary search algorithm are all different.

Since both the left and right boundaries in the \"closed interval\" representation are defined as closed, the operations to narrow the interval through pointers \\(i\\) and \\(j\\) are also symmetric. This makes it less error-prone, so the \"closed interval\" approach is generally recommended.

Figure 10-3   Two interval definitions

","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search/#1012-advantages-and-limitations","level":2,"title":"10.1.2   Advantages and Limitations","text":"

Binary search performs well in both time and space aspects.

  • Binary search has high time efficiency. With large data volumes, the logarithmic time complexity has significant advantages. For example, when the data size \\(n = 2^{20}\\), linear search requires \\(2^{20} = 1048576\\) loop rounds, while binary search only needs \\(\\log_2 2^{20} = 20\\) rounds.
  • Binary search requires no extra space. Compared to searching algorithms that require additional space (such as hash-based search), binary search is more space-efficient.

However, binary search is not suitable for all situations, mainly for the following reasons:

  • Binary search is only applicable to sorted data. If the input data is unsorted, sorting specifically to use binary search would be counterproductive, as sorting algorithms typically have a time complexity of \\(O(n \\log n)\\), which is higher than both linear search and binary search. For scenarios with frequent element insertions, maintaining array orderliness requires inserting elements at specific positions with a time complexity of \\(O(n)\\), which is also very expensive.
  • Binary search is only applicable to arrays. Binary search requires jump-style (non-contiguous) element access, and jump-style access has low efficiency in linked lists, making it unsuitable for linked lists or data structures based on linked list implementations.
  • For small data volumes, linear search performs better. In linear search, each round requires only 1 comparison operation; while in binary search, it requires 1 addition, 1 division, 1-3 comparison operations, and 1 addition (subtraction), totaling 4-6 unit operations. Therefore, when the data volume \\(n\\) is small, linear search is actually faster than binary search.
","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search_edge/","level":1,"title":"10.3   Binary Search Edge Cases","text":"","path":["Chapter 10. Searching","10.3   Binary Search Edge Cases"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1031-finding-the-left-boundary","level":2,"title":"10.3.1   Finding the Left Boundary","text":"

Question

Given a sorted array nums of length \\(n\\) that may contain duplicate elements, return the index of the leftmost element target in the array. If the array does not contain the element, return \\(-1\\).

Recall the method for finding the insertion point with binary search. After the search completes, \\(i\\) points to the leftmost target, so finding the insertion point is essentially finding the index of the leftmost target.

Consider implementing the left boundary search using the insertion point finding function. Note that the array may not contain target, which could result in the following two cases:

  • The insertion point index \\(i\\) is out of bounds.
  • The element nums[i] is not equal to target.

When either of these situations occurs, simply return \\(-1\\). The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
def binary_search_left_edge(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for the leftmost target\"\"\"\n    # Equivalent to finding the insertion point of target\n    i = binary_search_insertion(nums, target)\n    # Target not found, return -1\n    if i == len(nums) or nums[i] != target:\n        return -1\n    # Found target, return index i\n    return i\n
binary_search_edge.cpp
/* Binary search for the leftmost target */\nint binarySearchLeftEdge(vector<int> &nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.size() || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.java
/* Binary search for the leftmost target */\nint binarySearchLeftEdge(int[] nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binary_search_insertion.binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.length || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.cs
/* Binary search for the leftmost target */\nint BinarySearchLeftEdge(int[] nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.Length || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.go
/* Binary search for the leftmost target */\nfunc binarySearchLeftEdge(nums []int, target int) int {\n    // Equivalent to finding the insertion point of target\n    i := binarySearchInsertion(nums, target)\n    // Target not found, return -1\n    if i == len(nums) || nums[i] != target {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
binary_search_edge.swift
/* Binary search for the leftmost target */\nfunc binarySearchLeftEdge(nums: [Int], target: Int) -> Int {\n    // Equivalent to finding the insertion point of target\n    let i = binarySearchInsertion(nums: nums, target: target)\n    // Target not found, return -1\n    if i == nums.endIndex || nums[i] != target {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
binary_search_edge.js
/* Binary search for the leftmost target */\nfunction binarySearchLeftEdge(nums, target) {\n    // Equivalent to finding the insertion point of target\n    const i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.ts
/* Binary search for the leftmost target */\nfunction binarySearchLeftEdge(nums: Array<number>, target: number): number {\n    // Equivalent to finding the insertion point of target\n    const i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.dart
/* Binary search for the leftmost target */\nint binarySearchLeftEdge(List<int> nums, int target) {\n  // Equivalent to finding the insertion point of target\n  int i = binarySearchInsertion(nums, target);\n  // Target not found, return -1\n  if (i == nums.length || nums[i] != target) {\n    return -1;\n  }\n  // Found target, return index i\n  return i;\n}\n
binary_search_edge.rs
/* Binary search for the leftmost target */\nfn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {\n    // Equivalent to finding the insertion point of target\n    let i = binary_search_insertion(nums, target);\n    // Target not found, return -1\n    if i == nums.len() as i32 || nums[i as usize] != target {\n        return -1;\n    }\n    // Found target, return index i\n    i\n}\n
binary_search_edge.c
/* Binary search for the leftmost target */\nint binarySearchLeftEdge(int *nums, int numSize, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binarySearchInsertion(nums, numSize, target);\n    // Target not found, return -1\n    if (i == numSize || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.kt
/* Binary search for the leftmost target */\nfun binarySearchLeftEdge(nums: IntArray, target: Int): Int {\n    // Equivalent to finding the insertion point of target\n    val i = binarySearchInsertion(nums, target)\n    // Target not found, return -1\n    if (i == nums.size || nums[i] != target) {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
binary_search_edge.rb
### Binary search leftmost target ###\ndef binary_search_left_edge(nums, target)\n  # Equivalent to finding the insertion point of target\n  i = binary_search_insertion(nums, target)\n\n  # Target not found, return -1\n  return -1 if i == nums.length || nums[i] != target\n\n  i # Found target, return index i\nend\n
","path":["Chapter 10. Searching","10.3   Binary Search Edge Cases"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1032-finding-the-right-boundary","level":2,"title":"10.3.2   Finding the Right Boundary","text":"

So how do we find the rightmost target? The most direct approach is to modify the code and replace the pointer shrinking operation in the nums[m] == target case. The code is omitted here; interested readers can implement it themselves.

Below we introduce two more clever methods.

","path":["Chapter 10. Searching","10.3   Binary Search Edge Cases"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1-reusing-left-boundary-search","level":3,"title":"1.   Reusing Left Boundary Search","text":"

In fact, we can use the function for finding the leftmost element to find the rightmost element. The specific method is: Convert finding the rightmost target into finding the leftmost target + 1.

As shown in Figure 10-7, after the search completes, pointer \\(i\\) points to the leftmost target + 1 (if it exists), while \\(j\\) points to the rightmost target, so we can simply return \\(j\\).

Figure 10-7   Converting right boundary search to left boundary search

Note that the returned insertion point is \\(i\\), so we need to subtract \\(1\\) from it to obtain \\(j\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
def binary_search_right_edge(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for the rightmost target\"\"\"\n    # Convert to finding the leftmost target + 1\n    i = binary_search_insertion(nums, target + 1)\n    # j points to the rightmost target, i points to the first element greater than target\n    j = i - 1\n    # Target not found, return -1\n    if j == -1 or nums[j] != target:\n        return -1\n    # Found target, return index j\n    return j\n
binary_search_edge.cpp
/* Binary search for the rightmost target */\nint binarySearchRightEdge(vector<int> &nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.java
/* Binary search for the rightmost target */\nint binarySearchRightEdge(int[] nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.cs
/* Binary search for the rightmost target */\nint BinarySearchRightEdge(int[] nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.go
/* Binary search for the rightmost target */\nfunc binarySearchRightEdge(nums []int, target int) int {\n    // Convert to finding the leftmost target + 1\n    i := binarySearchInsertion(nums, target+1)\n    // j points to the rightmost target, i points to the first element greater than target\n    j := i - 1\n    // Target not found, return -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
binary_search_edge.swift
/* Binary search for the rightmost target */\nfunc binarySearchRightEdge(nums: [Int], target: Int) -> Int {\n    // Convert to finding the leftmost target + 1\n    let i = binarySearchInsertion(nums: nums, target: target + 1)\n    // j points to the rightmost target, i points to the first element greater than target\n    let j = i - 1\n    // Target not found, return -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
binary_search_edge.js
/* Binary search for the rightmost target */\nfunction binarySearchRightEdge(nums, target) {\n    // Convert to finding the leftmost target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    const j = i - 1;\n    // Target not found, return -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.ts
/* Binary search for the rightmost target */\nfunction binarySearchRightEdge(nums: Array<number>, target: number): number {\n    // Convert to finding the leftmost target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    const j = i - 1;\n    // Target not found, return -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.dart
/* Binary search for the rightmost target */\nint binarySearchRightEdge(List<int> nums, int target) {\n  // Convert to finding the leftmost target + 1\n  int i = binarySearchInsertion(nums, target + 1);\n  // j points to the rightmost target, i points to the first element greater than target\n  int j = i - 1;\n  // Target not found, return -1\n  if (j == -1 || nums[j] != target) {\n    return -1;\n  }\n  // Found target, return index j\n  return j;\n}\n
binary_search_edge.rs
/* Binary search for the rightmost target */\nfn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {\n    // Convert to finding the leftmost target + 1\n    let i = binary_search_insertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    let j = i - 1;\n    // Target not found, return -1\n    if j == -1 || nums[j as usize] != target {\n        return -1;\n    }\n    // Found target, return index j\n    j\n}\n
binary_search_edge.c
/* Binary search for the rightmost target */\nint binarySearchRightEdge(int *nums, int numSize, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binarySearchInsertion(nums, numSize, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.kt
/* Binary search for the rightmost target */\nfun binarySearchRightEdge(nums: IntArray, target: Int): Int {\n    // Convert to finding the leftmost target + 1\n    val i = binarySearchInsertion(nums, target + 1)\n    // j points to the rightmost target, i points to the first element greater than target\n    val j = i - 1\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
binary_search_edge.rb
### Binary search rightmost target ###\ndef binary_search_right_edge(nums, target)\n  # Convert to finding the leftmost target + 1\n  i = binary_search_insertion(nums, target + 1)\n\n  # j points to the rightmost target, i points to the first element greater than target\n  j = i - 1\n\n  # Target not found, return -1\n  return -1 if j == -1 || nums[j] != target\n\n  j # Found target, return index j\nend\n
","path":["Chapter 10. Searching","10.3   Binary Search Edge Cases"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#2-converting-to-element-search","level":3,"title":"2.   Converting to Element Search","text":"

We know that when the array does not contain target, \\(i\\) and \\(j\\) will eventually point to the first elements greater than and less than target, respectively.

Therefore, as shown in Figure 10-8, we can construct an element that does not exist in the array to find the left and right boundaries.

  • Finding the leftmost target: Can be converted to finding target - 0.5 and returning pointer \\(i\\).
  • Finding the rightmost target: Can be converted to finding target + 0.5 and returning pointer \\(j\\).

Figure 10-8   Converting boundary search to element search

The code is omitted here, but the following two points are worth noting:

  • Since the given array does not contain decimals, we don't need to worry about how to handle equal cases.
  • Because this method introduces decimals, the variable target in the function needs to be changed to a floating-point type (Python does not require this change).
","path":["Chapter 10. Searching","10.3   Binary Search Edge Cases"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/","level":1,"title":"10.2   Binary Search Insertion Point","text":"

Binary search can not only be used to search for target elements but also to solve many variant problems, such as searching for the insertion position of a target element.

","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1021-case-without-duplicate-elements","level":2,"title":"10.2.1   Case Without Duplicate Elements","text":"

Question

Given a sorted array nums of length \\(n\\) and an element target, where the array contains no duplicate elements. Insert target into the array nums while maintaining its sorted order. If the array already contains the element target, insert it to its left. Return the index of target in the array after insertion. An example is shown in Figure 10-4.

Figure 10-4   Binary search insertion point example data

If we want to reuse the binary search code from the previous section, we need to answer the following two questions.

Question 1: When the array contains target, is the insertion point index the same as that element's index?

The problem requires inserting target to the left of equal elements, which means the newly inserted target replaces the position of the original target. In other words, when the array contains target, the insertion point index is the index of that target.

Question 2: When the array does not contain target, what is the insertion point index?

Further consider the binary search process: When nums[m] < target, \\(i\\) moves, which means pointer \\(i\\) is approaching elements greater than or equal to target. Similarly, pointer \\(j\\) is always approaching elements less than or equal to target.

Therefore, when the binary search ends, we must have: \\(i\\) points to the first element greater than target, and \\(j\\) points to the first element less than target. It's easy to see that when the array does not contain target, the insertion index is \\(i\\). The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
def binary_search_insertion_simple(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for insertion point (no duplicate elements)\"\"\"\n    i, j = 0, len(nums) - 1  # Initialize closed interval [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target is in the interval [i, m-1]\n        else:\n            return m  # Found target, return insertion point m\n    # Target not found, return insertion point i\n    return i\n
binary_search_insertion.cpp
/* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.java
/* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.cs
/* Binary search for insertion point (no duplicate elements) */\nint BinarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.go
/* Binary search for insertion point (no duplicate elements) */\nfunc binarySearchInsertionSimple(nums []int, target int) int {\n    // Initialize closed interval [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Calculate the midpoint index m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target is in the interval [i, m-1]\n            j = m - 1\n        } else {\n            // Found target, return insertion point m\n            return m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
binary_search_insertion.swift
/* Binary search for insertion point (no duplicate elements) */\nfunc binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [0, n-1]\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            return m // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
binary_search_insertion.js
/* Binary search for insertion point (no duplicate elements) */\nfunction binarySearchInsertionSimple(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.ts
/* Binary search for insertion point (no duplicate elements) */\nfunction binarySearchInsertionSimple(\n    nums: Array<number>,\n    target: number\n): number {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.dart
/* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      i = m + 1; // target is in the interval [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target is in the interval [i, m-1]\n    } else {\n      return m; // Found target, return insertion point m\n    }\n  }\n  // Target not found, return insertion point i\n  return i;\n}\n
binary_search_insertion.rs
/* Binary search for insertion point (no duplicate elements) */\nfn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m;\n        }\n    }\n    // Target not found, return insertion point i\n    i\n}\n
binary_search_insertion.c
/* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.kt
/* Binary search for insertion point (no duplicate elements) */\nfun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            return m // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
binary_search_insertion.rb
### Binary search insertion point (no duplicates) ###\ndef binary_search_insertion_simple(nums, target)\n  # Initialize closed interval [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target is in the interval [i, m-1]\n    else\n      return m  # Found target, return insertion point m\n    end\n  end\n\n  i # Target not found, return insertion point i\nend\n
","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1022-case-with-duplicate-elements","level":2,"title":"10.2.2   Case with Duplicate Elements","text":"

Question

Based on the previous problem, assume the array may contain duplicate elements, with everything else remaining the same.

Suppose there are multiple target elements in the array. Ordinary binary search can only return the index of one target, and cannot determine how many target elements are to the left and right of that element.

The problem requires inserting the target element at the leftmost position, so we need to find the index of the leftmost target in the array. Initially, consider implementing this through the steps shown in Figure 10-5:

  1. Perform binary search to obtain the index of any target, denoted as \\(k\\).
  2. Starting from index \\(k\\), perform linear traversal to the left, and return when the leftmost target is found.

Figure 10-5   Linear search for insertion point of duplicate elements

Although this method works, it includes linear search, resulting in a time complexity of \\(O(n)\\). When the array contains many duplicate target elements, this method is very inefficient.

Now consider extending the binary search code. As shown in Figure 10-6, the overall process remains unchanged: calculate the midpoint index \\(m\\) in each round, then compare target with nums[m], divided into the following cases:

  • When nums[m] < target or nums[m] > target, it means target has not been found yet, so use the ordinary binary search interval narrowing operation to make pointers \\(i\\) and \\(j\\) approach target.
  • When nums[m] == target, it means elements less than target are in the interval \\([i, m - 1]\\), so use \\(j = m - 1\\) to narrow the interval, thereby making pointer \\(j\\) approach elements less than target.

After the loop completes, \\(i\\) points to the leftmost target, and \\(j\\) points to the first element less than target, so index \\(i\\) is the insertion point.

<1><2><3><4><5><6><7><8>

Figure 10-6   Steps for binary search insertion point of duplicate elements

Observe the following code: the operations for branches nums[m] > target and nums[m] == target are the same, so the two can be merged.

Even so, we can still keep the conditional branches expanded, as the logic is clearer and more readable.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
def binary_search_insertion(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for insertion point (with duplicate elements)\"\"\"\n    i, j = 0, len(nums) - 1  # Initialize closed interval [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target is in the interval [i, m-1]\n        else:\n            j = m - 1  # The first element less than target is in the interval [i, m-1]\n    # Return insertion point i\n    return i\n
binary_search_insertion.cpp
/* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.java
/* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.cs
/* Binary search for insertion point (with duplicate elements) */\nint BinarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.go
/* Binary search for insertion point (with duplicate elements) */\nfunc binarySearchInsertion(nums []int, target int) int {\n    // Initialize closed interval [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Calculate the midpoint index m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target is in the interval [i, m-1]\n            j = m - 1\n        } else {\n            // The first element less than target is in the interval [i, m-1]\n            j = m - 1\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
binary_search_insertion.swift
/* Binary search for insertion point (with duplicate elements) */\nfunc binarySearchInsertion(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [0, n-1]\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            j = m - 1 // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
binary_search_insertion.js
/* Binary search for insertion point (with duplicate elements) */\nfunction binarySearchInsertion(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.ts
/* Binary search for insertion point (with duplicate elements) */\nfunction binarySearchInsertion(nums: Array<number>, target: number): number {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.dart
/* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      i = m + 1; // target is in the interval [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target is in the interval [i, m-1]\n    } else {\n      j = m - 1; // The first element less than target is in the interval [i, m-1]\n    }\n  }\n  // Return insertion point i\n  return i;\n}\n
binary_search_insertion.rs
/* Binary search for insertion point (with duplicate elements) */\npub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    i\n}\n
binary_search_insertion.c
/* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.kt
/* Binary search for insertion point (with duplicate elements) */\nfun binarySearchInsertion(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            j = m - 1 // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
binary_search_insertion.rb
### Binary search insertion point (with duplicates) ###\ndef binary_search_insertion(nums, target)\n  # Initialize closed interval [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target is in the interval [i, m-1]\n    else\n      j = m - 1 # The first element less than target is in the interval [i, m-1]\n    end\n  end\n\n  i # Return insertion point i\nend\n

Tip

The code in this section all uses the \"closed interval\" approach. Interested readers can implement the \"left-closed right-open\" approach themselves.

Overall, binary search is simply about setting search targets for pointers \\(i\\) and \\(j\\) separately. The target could be a specific element (such as target) or a range of elements (such as elements less than target).

Through continuous binary iterations, both pointers \\(i\\) and \\(j\\) gradually approach their preset targets. Ultimately, they either successfully find the answer or stop after crossing the boundaries.

","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/","level":1,"title":"10.4   Hash Optimization Strategy","text":"

In algorithm problems, we often reduce the time complexity of algorithms by replacing linear search with hash-based search. Let's use an algorithm problem to deepen our understanding.

Question

Given an integer array nums and a target element target, search for two elements in the array whose \"sum\" equals target, and return their array indices. Any solution will do.

","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041-linear-search-trading-time-for-space","level":2,"title":"10.4.1   Linear Search: Trading Time for Space","text":"

Consider directly traversing all possible combinations. As shown in Figure 10-9, we open a two-layer loop and judge in each round whether the sum of two integers equals target. If so, return their indices.

Figure 10-9   Linear search solution for two sum

The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n    \"\"\"Method 1: Brute force enumeration\"\"\"\n    # Two nested loops, time complexity is O(n^2)\n    for i in range(len(nums) - 1):\n        for j in range(i + 1, len(nums)):\n            if nums[i] + nums[j] == target:\n                return [i, j]\n    return []\n
two_sum.cpp
/* Method 1: Brute force enumeration */\nvector<int> twoSumBruteForce(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Two nested loops, time complexity is O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return {i, j};\n        }\n    }\n    return {};\n}\n
two_sum.java
/* Method 1: Brute force enumeration */\nint[] twoSumBruteForce(int[] nums, int target) {\n    int size = nums.length;\n    // Two nested loops, time complexity is O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return new int[] { i, j };\n        }\n    }\n    return new int[0];\n}\n
two_sum.cs
/* Method 1: Brute force enumeration */\nint[] TwoSumBruteForce(int[] nums, int target) {\n    int size = nums.Length;\n    // Two nested loops, time complexity is O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return [i, j];\n        }\n    }\n    return [];\n}\n
two_sum.go
/* Method 1: Brute force enumeration */\nfunc twoSumBruteForce(nums []int, target int) []int {\n    size := len(nums)\n    // Two nested loops, time complexity is O(n^2)\n    for i := 0; i < size-1; i++ {\n        for j := i + 1; j < size; j++ {\n            if nums[i]+nums[j] == target {\n                return []int{i, j}\n            }\n        }\n    }\n    return nil\n}\n
two_sum.swift
/* Method 1: Brute force enumeration */\nfunc twoSumBruteForce(nums: [Int], target: Int) -> [Int] {\n    // Two nested loops, time complexity is O(n^2)\n    for i in nums.indices.dropLast() {\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[i] + nums[j] == target {\n                return [i, j]\n            }\n        }\n    }\n    return [0]\n}\n
two_sum.js
/* Method 1: Brute force enumeration */\nfunction twoSumBruteForce(nums, target) {\n    const n = nums.length;\n    // Two nested loops, time complexity is O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
two_sum.ts
/* Method 1: Brute force enumeration */\nfunction twoSumBruteForce(nums: number[], target: number): number[] {\n    const n = nums.length;\n    // Two nested loops, time complexity is O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
two_sum.dart
/* Method 1: Brute force enumeration */\nList<int> twoSumBruteForce(List<int> nums, int target) {\n  int size = nums.length;\n  // Two nested loops, time complexity is O(n^2)\n  for (var i = 0; i < size - 1; i++) {\n    for (var j = i + 1; j < size; j++) {\n      if (nums[i] + nums[j] == target) return [i, j];\n    }\n  }\n  return [0];\n}\n
two_sum.rs
/* Method 1: Brute force enumeration */\npub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    let size = nums.len();\n    // Two nested loops, time complexity is O(n^2)\n    for i in 0..size - 1 {\n        for j in i + 1..size {\n            if nums[i] + nums[j] == target {\n                return Some(vec![i as i32, j as i32]);\n            }\n        }\n    }\n    None\n}\n
two_sum.c
/* Method 1: Brute force enumeration */\nint *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {\n    for (int i = 0; i < numsSize; ++i) {\n        for (int j = i + 1; j < numsSize; ++j) {\n            if (nums[i] + nums[j] == target) {\n                int *res = malloc(sizeof(int) * 2);\n                res[0] = i, res[1] = j;\n                *returnSize = 2;\n                return res;\n            }\n        }\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
two_sum.kt
/* Method 1: Brute force enumeration */\nfun twoSumBruteForce(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Two nested loops, time complexity is O(n^2)\n    for (i in 0..<size - 1) {\n        for (j in i + 1..<size) {\n            if (nums[i] + nums[j] == target) return intArrayOf(i, j)\n        }\n    }\n    return IntArray(0)\n}\n
two_sum.rb
### Method 1: Brute force enumeration ###\ndef two_sum_brute_force(nums, target)\n  # Two nested loops, time complexity is O(n^2)\n  for i in 0...(nums.length - 1)\n    for j in (i + 1)...nums.length\n      return [i, j] if nums[i] + nums[j] == target\n    end\n  end\n\n  []\nend\n

This method has a time complexity of \\(O(n^2)\\) and a space complexity of \\(O(1)\\), which is very time-consuming with large data volumes.

","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1042-hash-based-search-trading-space-for-time","level":2,"title":"10.4.2   Hash-Based Search: Trading Space for Time","text":"

Consider using a hash table where key-value pairs are array elements and element indices respectively. Loop through the array, performing the steps shown in Figure 10-10 in each round:

  1. Check if the number target - nums[i] is in the hash table. If so, directly return the indices of these two elements.
  2. Add the key-value pair nums[i] and index i to the hash table.
<1><2><3>

Figure 10-10   Hash table solution for two sum

The implementation code is shown below, requiring only a single loop:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
def two_sum_hash_table(nums: list[int], target: int) -> list[int]:\n    \"\"\"Method 2: Auxiliary hash table\"\"\"\n    # Auxiliary hash table, space complexity is O(n)\n    dic = {}\n    # Single loop, time complexity is O(n)\n    for i in range(len(nums)):\n        if target - nums[i] in dic:\n            return [dic[target - nums[i]], i]\n        dic[nums[i]] = i\n    return []\n
two_sum.cpp
/* Method 2: Auxiliary hash table */\nvector<int> twoSumHashTable(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Auxiliary hash table, space complexity is O(n)\n    unordered_map<int, int> dic;\n    // Single loop, time complexity is O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.find(target - nums[i]) != dic.end()) {\n            return {dic[target - nums[i]], i};\n        }\n        dic.emplace(nums[i], i);\n    }\n    return {};\n}\n
two_sum.java
/* Method 2: Auxiliary hash table */\nint[] twoSumHashTable(int[] nums, int target) {\n    int size = nums.length;\n    // Auxiliary hash table, space complexity is O(n)\n    Map<Integer, Integer> dic = new HashMap<>();\n    // Single loop, time complexity is O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.containsKey(target - nums[i])) {\n            return new int[] { dic.get(target - nums[i]), i };\n        }\n        dic.put(nums[i], i);\n    }\n    return new int[0];\n}\n
two_sum.cs
/* Method 2: Auxiliary hash table */\nint[] TwoSumHashTable(int[] nums, int target) {\n    int size = nums.Length;\n    // Auxiliary hash table, space complexity is O(n)\n    Dictionary<int, int> dic = [];\n    // Single loop, time complexity is O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.ContainsKey(target - nums[i])) {\n            return [dic[target - nums[i]], i];\n        }\n        dic.Add(nums[i], i);\n    }\n    return [];\n}\n
two_sum.go
/* Method 2: Auxiliary hash table */\nfunc twoSumHashTable(nums []int, target int) []int {\n    // Auxiliary hash table, space complexity is O(n)\n    hashTable := map[int]int{}\n    // Single loop, time complexity is O(n)\n    for idx, val := range nums {\n        if preIdx, ok := hashTable[target-val]; ok {\n            return []int{preIdx, idx}\n        }\n        hashTable[val] = idx\n    }\n    return nil\n}\n
two_sum.swift
/* Method 2: Auxiliary hash table */\nfunc twoSumHashTable(nums: [Int], target: Int) -> [Int] {\n    // Auxiliary hash table, space complexity is O(n)\n    var dic: [Int: Int] = [:]\n    // Single loop, time complexity is O(n)\n    for i in nums.indices {\n        if let j = dic[target - nums[i]] {\n            return [j, i]\n        }\n        dic[nums[i]] = i\n    }\n    return [0]\n}\n
two_sum.js
/* Method 2: Auxiliary hash table */\nfunction twoSumHashTable(nums, target) {\n    // Auxiliary hash table, space complexity is O(n)\n    let m = {};\n    // Single loop, time complexity is O(n)\n    for (let i = 0; i < nums.length; i++) {\n        if (m[target - nums[i]] !== undefined) {\n            return [m[target - nums[i]], i];\n        } else {\n            m[nums[i]] = i;\n        }\n    }\n    return [];\n}\n
two_sum.ts
/* Method 2: Auxiliary hash table */\nfunction twoSumHashTable(nums: number[], target: number): number[] {\n    // Auxiliary hash table, space complexity is O(n)\n    let m: Map<number, number> = new Map();\n    // Single loop, time complexity is O(n)\n    for (let i = 0; i < nums.length; i++) {\n        let index = m.get(target - nums[i]);\n        if (index !== undefined) {\n            return [index, i];\n        } else {\n            m.set(nums[i], i);\n        }\n    }\n    return [];\n}\n
two_sum.dart
/* Method 2: Auxiliary hash table */\nList<int> twoSumHashTable(List<int> nums, int target) {\n  int size = nums.length;\n  // Auxiliary hash table, space complexity is O(n)\n  Map<int, int> dic = HashMap();\n  // Single loop, time complexity is O(n)\n  for (var i = 0; i < size; i++) {\n    if (dic.containsKey(target - nums[i])) {\n      return [dic[target - nums[i]]!, i];\n    }\n    dic.putIfAbsent(nums[i], () => i);\n  }\n  return [0];\n}\n
two_sum.rs
/* Method 2: Auxiliary hash table */\npub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    // Auxiliary hash table, space complexity is O(n)\n    let mut dic = HashMap::new();\n    // Single loop, time complexity is O(n)\n    for (i, num) in nums.iter().enumerate() {\n        match dic.get(&(target - num)) {\n            Some(v) => return Some(vec![*v as i32, i as i32]),\n            None => dic.insert(num, i as i32),\n        };\n    }\n    None\n}\n
two_sum.c
/* Hash table */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Implemented using uthash.h\n} HashTable;\n\n/* Hash table lookup */\nHashTable *find(HashTable *h, int key) {\n    HashTable *tmp;\n    HASH_FIND_INT(h, &key, tmp);\n    return tmp;\n}\n\n/* Hash table element insertion */\nvoid insert(HashTable **h, int key, int val) {\n    HashTable *t = find(*h, key);\n    if (t == NULL) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = key, tmp->val = val;\n        HASH_ADD_INT(*h, key, tmp);\n    } else {\n        t->val = val;\n    }\n}\n\n/* Method 2: Auxiliary hash table */\nint *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {\n    HashTable *hashtable = NULL;\n    for (int i = 0; i < numsSize; i++) {\n        HashTable *t = find(hashtable, target - nums[i]);\n        if (t != NULL) {\n            int *res = malloc(sizeof(int) * 2);\n            res[0] = t->val, res[1] = i;\n            *returnSize = 2;\n            return res;\n        }\n        insert(&hashtable, nums[i], i);\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
two_sum.kt
/* Method 2: Auxiliary hash table */\nfun twoSumHashTable(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Auxiliary hash table, space complexity is O(n)\n    val dic = HashMap<Int, Int>()\n    // Single loop, time complexity is O(n)\n    for (i in 0..<size) {\n        if (dic.containsKey(target - nums[i])) {\n            return intArrayOf(dic[target - nums[i]]!!, i)\n        }\n        dic[nums[i]] = i\n    }\n    return IntArray(0)\n}\n
two_sum.rb
### Method 2: Auxiliary hash table ###\ndef two_sum_hash_table(nums, target)\n  # Auxiliary hash table, space complexity is O(n)\n  dic = {}\n  # Single loop, time complexity is O(n)\n  for i in 0...nums.length\n    return [dic[target - nums[i]], i] if dic.has_key?(target - nums[i])\n\n    dic[nums[i]] = i\n  end\n\n  []\nend\n

This method reduces the time complexity from \\(O(n^2)\\) to \\(O(n)\\) through hash-based search, greatly improving runtime efficiency.

Since an additional hash table needs to be maintained, the space complexity is \\(O(n)\\). Nevertheless, this method achieves a more balanced overall time-space efficiency, making it the optimal solution for this problem.

","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/","level":1,"title":"10.5   Searching Algorithms Revisited","text":"

Searching algorithms are used to search for one or a group of elements that meet specific conditions in data structures (such as arrays, linked lists, trees, or graphs).

Searching algorithms can be divided into the following two categories based on their implementation approach:

  • Locating target elements by traversing the data structure, such as traversing arrays, linked lists, trees, and graphs.
  • Achieving efficient element search by utilizing data organization structure or prior information contained in the data, such as binary search, hash-based search, and binary search tree search.

It's not hard to see that these topics have all been covered in previous chapters, so searching algorithms are not unfamiliar to us. In this section, we will approach from a more systematic perspective and re-examine searching algorithms.

","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051-brute-force-search","level":2,"title":"10.5.1   Brute-Force Search","text":"

Brute-force search locates target elements by traversing each element of the data structure.

  • \"Linear search\" is applicable to linear data structures such as arrays and linked lists. It starts from one end of the data structure and accesses elements one by one until the target element is found or the other end is reached without finding the target element.
  • \"Breadth-first search\" and \"depth-first search\" are two traversal strategies for graphs and trees. Breadth-first search starts from the initial node and searches layer by layer, visiting nodes from near to far. Depth-first search starts from the initial node, follows a path to the end, then backtracks and tries other paths until the entire data structure is traversed.

The advantage of brute-force search is that it is simple and has good generality, requiring no data preprocessing or additional data structures.

However, the time complexity of such algorithms is \\(O(n)\\), where \\(n\\) is the number of elements, so performance is poor when dealing with large amounts of data.

","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1052-adaptive-search","level":2,"title":"10.5.2   Adaptive Search","text":"

Adaptive search utilizes the unique properties of data (such as orderliness) to optimize the search process, thereby locating target elements more efficiently.

  • \"Binary search\" uses the orderliness of data to achieve efficient searching, applicable only to arrays.
  • \"Hash-based search\" uses hash tables to establish key-value pair mappings between search data and target data, thereby achieving query operations.
  • \"Tree search\" in specific tree structures (such as binary search trees), quickly eliminates nodes based on comparing node values to locate target elements.

The advantage of such algorithms is high efficiency, with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\).

However, using these algorithms often requires data preprocessing. For example, binary search requires pre-sorting the array, while hash-based search and tree search both require additional data structures, and maintaining these data structures also requires extra time and space overhead.

Tip

Adaptive search algorithms are often called lookup algorithms, mainly used to quickly retrieve target elements in specific data structures.

","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1053-search-method-selection","level":2,"title":"10.5.3   Search Method Selection","text":"

Given a dataset of size \\(n\\), we can use linear search, binary search, tree search, hash-based search, and other methods to search for the target element. The working principles of each method are shown in Figure 10-11.

Figure 10-11   Multiple search strategies

The operational efficiency and characteristics of the above methods are as follows:

Table 10-1   Comparison of search algorithm efficiency

Linear search Binary search Tree search Hash-based search Search element \\(O(n)\\) \\(O(\\log n)\\) \\(O(\\log n)\\) \\(O(1)\\) Insert element \\(O(1)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Delete element \\(O(n)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Extra space \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\) Data preprocessing / Sorting \\(O(n \\log n)\\) Tree building \\(O(n \\log n)\\) Hash table building \\(O(n)\\) Data ordered Unordered Ordered Ordered Unordered

The choice of search algorithm also depends on data volume, search performance requirements, data query and update frequency, etc.

Linear search

  • Good generality, requiring no data preprocessing operations. If we only need to query the data once, the data preprocessing time for the other three methods would be longer than linear search.
  • Suitable for small data volumes, where time complexity has less impact on efficiency.
  • Suitable for scenarios with high data update frequency, as this method does not require any additional data maintenance.

Binary search

  • Suitable for large data volumes with stable efficiency performance, worst-case time complexity of \\(O(\\log n)\\).
  • Data volume cannot be too large, as storing arrays requires contiguous memory space.
  • Not suitable for scenarios with frequent data insertion and deletion, as maintaining a sorted array has high overhead.

Hash-based search

  • Suitable for scenarios with high query performance requirements, with an average time complexity of \\(O(1)\\).
  • Not suitable for scenarios requiring ordered data or range searches, as hash tables cannot maintain data orderliness.
  • High dependence on hash functions and hash collision handling strategies, with significant risk of performance degradation.
  • Not suitable for excessively large data volumes, as hash tables require extra space to minimize collisions and thus provide good query performance.

Tree search

  • Suitable for massive data, as tree nodes are stored dispersedly in memory.
  • Suitable for scenarios requiring maintained ordered data or range searches.
  • During continuous node insertion and deletion, binary search trees may become skewed, degrading time complexity to \\(O(n)\\).
  • If using AVL trees or red-black trees, all operations can run stably at \\(O(\\log n)\\) efficiency, but operations to maintain tree balance add extra overhead.
","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/summary/","level":1,"title":"10.6   Summary","text":"","path":["Chapter 10. Searching","10.6   Summary"],"tags":[]},{"location":"chapter_searching/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Binary search relies on data orderliness and progressively reduces the search interval by half through loops. It requires input data to be sorted and is only applicable to arrays or data structures based on array implementations.
  • Brute-force search locates data by traversing the data structure. Linear search is applicable to arrays and linked lists, while breadth-first search and depth-first search are applicable to graphs and trees. Such algorithms have good generality and require no data preprocessing, but have a relatively high time complexity of \\(O(n)\\).
  • Hash-based search, tree search, and binary search are efficient search methods that can quickly locate target elements in specific data structures. Such algorithms are highly efficient with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\), but typically require additional data structures.
  • In practice, we need to analyze factors such as data scale, search performance requirements, and data query and update frequency to choose the appropriate search method.
  • Linear search is suitable for small-scale or frequently updated data; binary search is suitable for large-scale, sorted data; hash-based search is suitable for data with high query efficiency requirements and no need for range queries; tree search is suitable for large-scale dynamic data that needs to maintain order and support range queries.
  • Replacing linear search with hash-based search is a commonly used strategy to optimize runtime, reducing time complexity from \\(O(n)\\) to \\(O(1)\\).
","path":["Chapter 10. Searching","10.6   Summary"],"tags":[]},{"location":"chapter_sorting/","level":1,"title":"Chapter 11.   Sorting","text":"

Abstract

Sorting is like a magic key that transforms chaos into order, enabling us to understand and process data more efficiently.

Whether it's simple ascending order or complex categorized arrangements, sorting demonstrates the harmonious beauty of data.

","path":["Chapter 11. Sorting","Chapter 11.   Sorting"],"tags":[]},{"location":"chapter_sorting/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 11.1   Sorting Algorithms
  • 11.2   Selection Sort
  • 11.3   Bubble Sort
  • 11.4   Insertion Sort
  • 11.5   Quick Sort
  • 11.6   Merge Sort
  • 11.7   Heap Sort
  • 11.8   Bucket Sort
  • 11.9   Counting Sort
  • 11.10   Radix Sort
  • 11.11   Summary
","path":["Chapter 11. Sorting","Chapter 11.   Sorting"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   Bubble Sort","text":"

Bubble sort (bubble sort) achieves sorting by continuously comparing and swapping adjacent elements. This process is like bubbles rising from the bottom to the top, hence the name bubble sort.

As shown in Figure 11-4, the bubbling process can be simulated using element swap operations: starting from the leftmost end of the array and traversing to the right, compare the size of adjacent elements, and if \"left element > right element\", swap them. After completing the traversal, the largest element will be moved to the rightmost end of the array.

<1><2><3><4><5><6><7>

Figure 11-4   Simulating bubble using element swap operation

","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1131-algorithm-flow","level":2,"title":"11.3.1   Algorithm Flow","text":"

Assume the array has length \\(n\\). The steps of bubble sort are shown in Figure 11-5.

  1. First, perform \"bubbling\" on \\(n\\) elements, swapping the largest element of the array to its correct position.
  2. Next, perform \"bubbling\" on the remaining \\(n - 1\\) elements, swapping the second largest element to its correct position.
  3. And so on. After \\(n - 1\\) rounds of \"bubbling\", the largest \\(n - 1\\) elements have all been swapped to their correct positions.
  4. The only remaining element must be the smallest element, requiring no sorting, so the array sorting is complete.

Figure 11-5   Bubble sort flow

Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
def bubble_sort(nums: list[int]):\n    \"\"\"Bubble sort\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [0, i]\n    for i in range(n - 1, 0, -1):\n        # Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n
bubble_sort.cpp
/* Bubble sort */\nvoid bubbleSort(vector<int> &nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                // Using std::swap() function here\n                swap(nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
bubble_sort.java
/* Bubble sort */\nvoid bubbleSort(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.cs
/* Bubble sort */\nvoid BubbleSort(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
bubble_sort.go
/* Bubble sort */\nfunc bubbleSort(nums []int) {\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n            }\n        }\n    }\n}\n
bubble_sort.swift
/* Bubble sort */\nfunc bubbleSort(nums: inout [Int]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swapAt(j, j + 1)\n            }\n        }\n    }\n}\n
bubble_sort.js
/* Bubble sort */\nfunction bubbleSort(nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.ts
/* Bubble sort */\nfunction bubbleSort(nums: number[]): void {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.dart
/* Bubble sort */\nvoid bubbleSort(List<int> nums) {\n  // Outer loop: unsorted range is [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n      }\n    }\n  }\n}\n
bubble_sort.rs
/* Bubble sort */\nfn bubble_sort(nums: &mut [i32]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swap(j, j + 1);\n            }\n        }\n    }\n}\n
bubble_sort.c
/* Bubble sort */\nvoid bubbleSort(int nums[], int size) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n            }\n        }\n    }\n}\n
bubble_sort.kt
/* Bubble sort */\nfun bubbleSort(nums: IntArray) {\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n            }\n        }\n    }\n}\n
bubble_sort.rb
### Bubble sort ###\ndef bubble_sort(nums)\n  n = nums.length\n  # Outer loop: unsorted range is [0, i]\n  for i in (n - 1).downto(1)\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n
","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1132-efficiency-optimization","level":2,"title":"11.3.2   Efficiency Optimization","text":"

We notice that if no swap operations are performed during a certain round of \"bubbling\", it means the array has already completed sorting and can directly return the result. Therefore, we can add a flag flag to monitor this situation and return immediately once it occurs.

After optimization, the worst-case time complexity and average time complexity of bubble sort remain \\(O(n^2)\\); but when the input array is completely ordered, the best-case time complexity can reach \\(O(n)\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
def bubble_sort_with_flag(nums: list[int]):\n    \"\"\"Bubble sort (flag optimization)\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [0, i]\n    for i in range(n - 1, 0, -1):\n        flag = False  # Initialize flag\n        # Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n                flag = True  # Record element swap\n        if not flag:\n            break  # No elements were swapped in this round of \"bubbling\", exit directly\n
bubble_sort.cpp
/* Bubble sort (flag optimization)*/\nvoid bubbleSortWithFlag(vector<int> &nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        bool flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                // Using std::swap() function here\n                swap(nums[j], nums[j + 1]);\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag)\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.java
/* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        boolean flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag)\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.cs
/* Bubble sort (flag optimization) */\nvoid BubbleSortWithFlag(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        bool flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                flag = true;  // Record element swap\n            }\n        }\n        if (!flag) break;     // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.go
/* Bubble sort (flag optimization) */\nfunc bubbleSortWithFlag(nums []int) {\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        flag := false // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n                flag = true // Record element swap\n            }\n        }\n        if flag == false { // No elements were swapped in this round of \"bubbling\", exit directly\n            break\n        }\n    }\n}\n
bubble_sort.swift
/* Bubble sort (flag optimization) */\nfunc bubbleSortWithFlag(nums: inout [Int]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        var flag = false // Initialize flag\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swapAt(j, j + 1)\n                flag = true // Record element swap\n            }\n        }\n        if !flag { // No elements were swapped in this round of \"bubbling\", exit directly\n            break\n        }\n    }\n}\n
bubble_sort.js
/* Bubble sort (flag optimization) */\nfunction bubbleSortWithFlag(nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.ts
/* Bubble sort (flag optimization) */\nfunction bubbleSortWithFlag(nums: number[]): void {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.dart
/* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(List<int> nums) {\n  // Outer loop: unsorted range is [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    bool flag = false; // Initialize flag\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        flag = true; // Record element swap\n      }\n    }\n    if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n  }\n}\n
bubble_sort.rs
/* Bubble sort (flag optimization) */\nfn bubble_sort_with_flag(nums: &mut [i32]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        let mut flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swap(j, j + 1);\n                flag = true; // Record element swap\n            }\n        }\n        if !flag {\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n        };\n    }\n}\n
bubble_sort.c
/* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(int nums[], int size) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        bool flag = false;\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n                flag = true;\n            }\n        }\n        if (!flag)\n            break;\n    }\n}\n
bubble_sort.kt
/* Bubble sort (flag optimization) */\nfun bubbleSortWithFlag(nums: IntArray) {\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        var flag = false // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                flag = true // Record element swap\n            }\n        }\n        if (!flag) break // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.rb
### Bubble sort (flag optimization) ###\ndef bubble_sort_with_flag(nums)\n  n = nums.length\n  # Outer loop: unsorted range is [0, i]\n  for i in (n - 1).downto(1)\n    flag = false # Initialize flag\n\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n        flag = true # Record element swap\n      end\n    end\n\n    break unless flag # No elements were swapped in this round of \"bubbling\", exit directly\n  end\nend\n
","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1133-algorithm-characteristics","level":2,"title":"11.3.3   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n^2)\\), adaptive sorting: The array lengths traversed in each round of \"bubbling\" are \\(n - 1\\), \\(n - 2\\), \\(\\dots\\), \\(2\\), \\(1\\), totaling \\((n - 1) n / 2\\). After introducing the flag optimization, the best-case time complexity can reach \\(O(n)\\).
  • Space complexity of \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
  • Stable sorting: Since equal elements are not swapped during \"bubbling\".
","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/","level":1,"title":"11.8   Bucket Sort","text":"

The several sorting algorithms mentioned earlier all belong to \"comparison-based sorting algorithms\", which achieve sorting by comparing the size of elements. The time complexity of such sorting algorithms cannot exceed \\(O(n \\log n)\\). Next, we will explore several \"non-comparison sorting algorithms\", whose time complexity can reach linear order.

Bucket sort (bucket sort) is a typical application of the divide-and-conquer strategy. It works by setting up buckets with size order, each bucket corresponding to a data range, evenly distributing data to each bucket; then, sorting within each bucket separately; finally, merging all data in the order of the buckets.

","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1181-algorithm-flow","level":2,"title":"11.8.1   Algorithm Flow","text":"

Consider an array of length \\(n\\), whose elements are floating-point numbers in the range \\([0, 1)\\). The flow of bucket sort is shown in Figure 11-13.

  1. Initialize \\(k\\) buckets and distribute the \\(n\\) elements into the \\(k\\) buckets.
  2. Sort each bucket separately (here we use the built-in sorting function of the programming language).
  3. Merge the results in order from smallest to largest bucket.

Figure 11-13   Bucket sort algorithm flow

The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bucket_sort.py
def bucket_sort(nums: list[float]):\n    \"\"\"Bucket sort\"\"\"\n    # Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    k = len(nums) // 2\n    buckets = [[] for _ in range(k)]\n    # 1. Distribute array elements into various buckets\n    for num in nums:\n        # Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        i = int(num * k)\n        # Add num to bucket i\n        buckets[i].append(num)\n    # 2. Sort each bucket\n    for bucket in buckets:\n        # Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort()\n    # 3. Traverse buckets to merge results\n    i = 0\n    for bucket in buckets:\n        for num in bucket:\n            nums[i] = num\n            i += 1\n
bucket_sort.cpp
/* Bucket sort */\nvoid bucketSort(vector<float> &nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    int k = nums.size() / 2;\n    vector<vector<float>> buckets(k);\n    // 1. Distribute array elements into various buckets\n    for (float num : nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = num * k;\n        // Add num to bucket bucket_idx\n        buckets[i].push_back(num);\n    }\n    // 2. Sort each bucket\n    for (vector<float> &bucket : buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        sort(bucket.begin(), bucket.end());\n    }\n    // 3. Traverse buckets to merge results\n    int i = 0;\n    for (vector<float> &bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.java
/* Bucket sort */\nvoid bucketSort(float[] nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    int k = nums.length / 2;\n    List<List<Float>> buckets = new ArrayList<>();\n    for (int i = 0; i < k; i++) {\n        buckets.add(new ArrayList<>());\n    }\n    // 1. Distribute array elements into various buckets\n    for (float num : nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = (int) (num * k);\n        // Add num to bucket i\n        buckets.get(i).add(num);\n    }\n    // 2. Sort each bucket\n    for (List<Float> bucket : buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        Collections.sort(bucket);\n    }\n    // 3. Traverse buckets to merge results\n    int i = 0;\n    for (List<Float> bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.cs
/* Bucket sort */\nvoid BucketSort(float[] nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    int k = nums.Length / 2;\n    List<List<float>> buckets = [];\n    for (int i = 0; i < k; i++) {\n        buckets.Add([]);\n    }\n    // 1. Distribute array elements into various buckets\n    foreach (float num in nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = (int)(num * k);\n        // Add num to bucket i\n        buckets[i].Add(num);\n    }\n    // 2. Sort each bucket\n    foreach (List<float> bucket in buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.Sort();\n    }\n    // 3. Traverse buckets to merge results\n    int j = 0;\n    foreach (List<float> bucket in buckets) {\n        foreach (float num in bucket) {\n            nums[j++] = num;\n        }\n    }\n}\n
bucket_sort.go
/* Bucket sort */\nfunc bucketSort(nums []float64) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    k := len(nums) / 2\n    buckets := make([][]float64, k)\n    for i := 0; i < k; i++ {\n        buckets[i] = make([]float64, 0)\n    }\n    // 1. Distribute array elements into various buckets\n    for _, num := range nums {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        i := int(num * float64(k))\n        // Add num to bucket i\n        buckets[i] = append(buckets[i], num)\n    }\n    // 2. Sort each bucket\n    for i := 0; i < k; i++ {\n        // Use built-in slice sorting function, can also be replaced with other sorting algorithms\n        sort.Float64s(buckets[i])\n    }\n    // 3. Traverse buckets to merge results\n    i := 0\n    for _, bucket := range buckets {\n        for _, num := range bucket {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
bucket_sort.swift
/* Bucket sort */\nfunc bucketSort(nums: inout [Double]) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    let k = nums.count / 2\n    var buckets = (0 ..< k).map { _ in [Double]() }\n    // 1. Distribute array elements into various buckets\n    for num in nums {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        let i = Int(num * Double(k))\n        // Add num to bucket i\n        buckets[i].append(num)\n    }\n    // 2. Sort each bucket\n    for i in buckets.indices {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        buckets[i].sort()\n    }\n    // 3. Traverse buckets to merge results\n    var i = nums.startIndex\n    for bucket in buckets {\n        for num in bucket {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
bucket_sort.js
/* Bucket sort */\nfunction bucketSort(nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    const k = nums.length / 2;\n    const buckets = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Distribute array elements into various buckets\n    for (const num of nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        const i = Math.floor(num * k);\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for (const bucket of buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Traverse buckets to merge results\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.ts
/* Bucket sort */\nfunction bucketSort(nums: number[]): void {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    const k = nums.length / 2;\n    const buckets: number[][] = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Distribute array elements into various buckets\n    for (const num of nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        const i = Math.floor(num * k);\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for (const bucket of buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Traverse buckets to merge results\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.dart
/* Bucket sort */\nvoid bucketSort(List<double> nums) {\n  // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n  int k = nums.length ~/ 2;\n  List<List<double>> buckets = List.generate(k, (index) => []);\n\n  // 1. Distribute array elements into various buckets\n  for (double _num in nums) {\n    // Input data range is [0, 1), use _num * k to map to index range [0, k-1]\n    int i = (_num * k).toInt();\n    // Add _num to bucket bucket_idx\n    buckets[i].add(_num);\n  }\n  // 2. Sort each bucket\n  for (List<double> bucket in buckets) {\n    bucket.sort();\n  }\n  // 3. Traverse buckets to merge results\n  int i = 0;\n  for (List<double> bucket in buckets) {\n    for (double _num in bucket) {\n      nums[i++] = _num;\n    }\n  }\n}\n
bucket_sort.rs
/* Bucket sort */\nfn bucket_sort(nums: &mut [f64]) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    let k = nums.len() / 2;\n    let mut buckets = vec![vec![]; k];\n    // 1. Distribute array elements into various buckets\n    for &num in nums.iter() {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        let i = (num * k as f64) as usize;\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for bucket in &mut buckets {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());\n    }\n    // 3. Traverse buckets to merge results\n    let mut i = 0;\n    for bucket in buckets.iter() {\n        for &num in bucket.iter() {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
bucket_sort.c
/* Bucket sort */\nvoid bucketSort(float nums[], int n) {\n    int k = n / 2;                                 // Initialize k = n/2 buckets\n    int *sizes = malloc(k * sizeof(int));          // Record each bucket's size\n    float **buckets = malloc(k * sizeof(float *)); // Array of dynamic arrays (buckets)\n    // Pre-allocate sufficient space for each bucket\n    for (int i = 0; i < k; ++i) {\n        buckets[i] = (float *)malloc(n * sizeof(float));\n        sizes[i] = 0;\n    }\n    // 1. Distribute array elements into various buckets\n    for (int i = 0; i < n; ++i) {\n        int idx = (int)(nums[i] * k);\n        buckets[idx][sizes[idx]++] = nums[i];\n    }\n    // 2. Sort each bucket\n    for (int i = 0; i < k; ++i) {\n        qsort(buckets[i], sizes[i], sizeof(float), compare);\n    }\n    // 3. Merge sorted buckets\n    int idx = 0;\n    for (int i = 0; i < k; ++i) {\n        for (int j = 0; j < sizes[i]; ++j) {\n            nums[idx++] = buckets[i][j];\n        }\n        // Free memory\n        free(buckets[i]);\n    }\n}\n
bucket_sort.kt
/* Bucket sort */\nfun bucketSort(nums: FloatArray) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    val k = nums.size / 2\n    val buckets = mutableListOf<MutableList<Float>>()\n    for (i in 0..<k) {\n        buckets.add(mutableListOf())\n    }\n    // 1. Distribute array elements into various buckets\n    for (num in nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        val i = (num * k).toInt()\n        // Add num to bucket i\n        buckets[i].add(num)\n    }\n    // 2. Sort each bucket\n    for (bucket in buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort()\n    }\n    // 3. Traverse buckets to merge results\n    var i = 0\n    for (bucket in buckets) {\n        for (num in bucket) {\n            nums[i++] = num\n        }\n    }\n}\n
bucket_sort.rb
### Bucket sort ###\ndef bucket_sort(nums)\n  # Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n  k = nums.length / 2\n  buckets = Array.new(k) { [] }\n\n  # 1. Distribute array elements into various buckets\n  nums.each do |num|\n    # Input data range is [0, 1), use num * k to map to index range [0, k-1]\n    i = (num * k).to_i\n    # Add num to bucket i\n    buckets[i] << num\n  end\n\n  # 2. Sort each bucket\n  buckets.each do |bucket|\n    # Use built-in sorting function, can also replace with other sorting algorithms\n    bucket.sort!\n  end\n\n  # 3. Traverse buckets to merge results\n  i = 0\n  buckets.each do |bucket|\n    bucket.each do |num|\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1182-algorithm-characteristics","level":2,"title":"11.8.2   Algorithm Characteristics","text":"

Bucket sort is suitable for processing very large data volumes. For example, if the input data contains 1 million elements and system memory cannot load all the data at once, the data can be divided into 1000 buckets, each bucket sorted separately, and then the results merged.

  • Time complexity of \\(O(n + k)\\): Assuming the elements are evenly distributed among the buckets, then the number of elements in each bucket is \\(\\frac{n}{k}\\). Assuming sorting a single bucket uses \\(O(\\frac{n}{k} \\log\\frac{n}{k})\\) time, then sorting all buckets uses \\(O(n \\log\\frac{n}{k})\\) time. When the number of buckets \\(k\\) is relatively large, the time complexity approaches \\(O(n)\\). Merging results requires traversing all buckets and elements, taking \\(O(n + k)\\) time. In the worst case, all data is distributed into one bucket, and sorting that bucket uses \\(O(n^2)\\) time.
  • Space complexity of \\(O(n + k)\\), non-in-place sorting: Additional space is required for \\(k\\) buckets and a total of \\(n\\) elements.
  • Whether bucket sort is stable depends on whether the algorithm for sorting elements within buckets is stable.
","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1183-how-to-achieve-even-distribution","level":2,"title":"11.8.3   How to Achieve Even Distribution","text":"

Theoretically, bucket sort can achieve \\(O(n)\\) time complexity. The key is to evenly distribute elements to each bucket, because real data is often not evenly distributed. For example, if we want to evenly distribute all products on Taobao into 10 buckets by price range, there may be very many products below 100 yuan and very few above 1000 yuan. If the price intervals are evenly divided into 10, the difference in the number of products in each bucket will be very large.

To achieve even distribution, we can first set an approximate dividing line to roughly divide the data into 3 buckets. After distribution is complete, continue dividing buckets with more products into 3 buckets until the number of elements in all buckets is roughly equal.

As shown in Figure 11-14, this method essentially creates a recursion tree, with the goal of making the values of leaf nodes as even as possible. Of course, it is not necessary to divide the data into 3 buckets every round; the specific division method can be flexibly chosen according to data characteristics.

Figure 11-14   Recursively dividing buckets

If we know the probability distribution of product prices in advance, we can set the price dividing line for each bucket based on the data probability distribution. It is worth noting that the data distribution does not necessarily need to be specifically calculated, but can also be approximated using a certain probability model based on data characteristics.

As shown in Figure 11-15, we assume that product prices follow a normal distribution, which allows us to reasonably set price intervals to evenly distribute products to each bucket.

Figure 11-15   Dividing buckets based on probability distribution

","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/","level":1,"title":"11.9   Counting Sort","text":"

Counting sort (counting sort) achieves sorting by counting the number of elements, typically applied to integer arrays.

","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1191-simple-implementation","level":2,"title":"11.9.1   Simple Implementation","text":"

Let's start with a simple example. Given an array nums of length \\(n\\), where the elements are all \"non-negative integers\", the overall flow of counting sort is shown in Figure 11-16.

  1. Traverse the array to find the largest number, denoted as \\(m\\), and then create an auxiliary array counter of length \\(m + 1\\).
  2. Use counter to count the number of occurrences of each number in nums, where counter[num] corresponds to the number of occurrences of the number num. The counting method is simple: just traverse nums (let the current number be num), and increase counter[num] by \\(1\\) in each round.
  3. Since each index of counter is naturally ordered, this is equivalent to all numbers being sorted. Next, we traverse counter and fill in nums in ascending order based on the number of occurrences of each number.

Figure 11-16   Counting sort flow

The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
def counting_sort_naive(nums: list[int]):\n    \"\"\"Counting sort\"\"\"\n    # Simple implementation, cannot be used for sorting objects\n    # 1. Count the maximum element m in the array\n    m = 0\n    for num in nums:\n        m = max(m, num)\n    # 2. Count the occurrence of each number\n    # counter[num] represents the occurrence of num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Traverse counter, filling each element back into the original array nums\n    i = 0\n    for num in range(m + 1):\n        for _ in range(counter[num]):\n            nums[i] = num\n            i += 1\n
counting_sort.cpp
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(vector<int> &nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.java
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.cs
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid CountingSortNaive(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.go
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunc countingSortNaive(nums []int) {\n    // 1. Count the maximum element m in the array\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    for i, num := 0, 0; num < m+1; num++ {\n        for j := 0; j < counter[num]; j++ {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
counting_sort.swift
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunc countingSortNaive(nums: inout [Int]) {\n    // 1. Count the maximum element m in the array\n    let m = nums.max()!\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    var i = 0\n    for num in 0 ..< m + 1 {\n        for _ in 0 ..< counter[num] {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
counting_sort.js
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunction countingSortNaive(nums) {\n    // 1. Count the maximum element m in the array\n    let m = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.ts
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunction countingSortNaive(nums: number[]): void {\n    // 1. Count the maximum element m in the array\n    let m: number = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.dart
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(List<int> nums) {\n  // 1. Count the maximum element m in the array\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Count the occurrence of each number\n  // counter[_num] represents occurrence count of _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Traverse counter, filling each element back into the original array nums\n  int i = 0;\n  for (int _num = 0; _num < m + 1; _num++) {\n    for (int j = 0; j < counter[_num]; j++, i++) {\n      nums[i] = _num;\n    }\n  }\n}\n
counting_sort.rs
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfn counting_sort_naive(nums: &mut [i32]) {\n    // 1. Count the maximum element m in the array\n    let m = *nums.iter().max().unwrap();\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    let mut counter = vec![0; m as usize + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    let mut i = 0;\n    for num in 0..m + 1 {\n        for _ in 0..counter[num as usize] {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
counting_sort.c
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(int nums[], int size) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int *counter = calloc(m + 1, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n    // 4. Free memory\n    free(counter);\n}\n
counting_sort.kt
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfun countingSortNaive(nums: IntArray) {\n    // 1. Count the maximum element m in the array\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    var i = 0\n    for (num in 0..<m + 1) {\n        var j = 0\n        while (j < counter[num]) {\n            nums[i] = num\n            j++\n            i++\n        }\n    }\n}\n
counting_sort.rb
### Counting sort ###\ndef counting_sort_naive(nums)\n  # Simple implementation, cannot be used for sorting objects\n  # 1. Count the maximum element m in the array\n  m = 0\n  nums.each { |num| m = [m, num].max }\n  # 2. Count the occurrence of each number\n  # counter[num] represents the occurrence of num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Traverse counter, filling each element back into the original array nums\n  i = 0\n  for num in 0...(m + 1)\n    (0...counter[num]).each do\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n

Connection between counting sort and bucket sort

From the perspective of bucket sort, we can regard each index of the counting array counter in counting sort as a bucket, and the process of counting quantities as distributing each element to the corresponding bucket. Essentially, counting sort is a special case of bucket sort for integer data.

","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1192-complete-implementation","level":2,"title":"11.9.2   Complete Implementation","text":"

Observant readers may have noticed that if the input data is objects, step 3. above becomes invalid. Suppose the input data is product objects, and we want to sort the products by price (a member variable of the class), but the above algorithm can only give the sorting result of prices.

So how can we obtain the sorting result of the original data? We first calculate the \"prefix sum\" of counter. As the name suggests, the prefix sum at index i, prefix[i], equals the sum of the first i elements of the array:

\\[ \\text{prefix}[i] = \\sum_{j=0}^i \\text{counter[j]} \\]

The prefix sum has a clear meaning: prefix[num] - 1 represents the index of the last occurrence of element num in the result array res. This information is very critical because it tells us where each element should appear in the result array. Next, we traverse each element num of the original array nums in reverse order, performing the following two steps in each iteration.

  1. Fill num into the array res at index prefix[num] - 1.
  2. Decrease the prefix sum prefix[num] by \\(1\\) to get the index for the next placement of num.

After the traversal is complete, the array res contains the sorted result, and finally res is used to overwrite the original array nums. The complete counting sort flow is shown in Figure 11-17.

<1><2><3><4><5><6><7><8>

Figure 11-17   Counting sort steps

The implementation code of counting sort is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
def counting_sort(nums: list[int]):\n    \"\"\"Counting sort\"\"\"\n    # Complete implementation, can sort objects and is a stable sort\n    # 1. Count the maximum element m in the array\n    m = max(nums)\n    # 2. Count the occurrence of each number\n    # counter[num] represents the occurrence of num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    # counter[num]-1 is the last index where num appears in res\n    for i in range(m):\n        counter[i + 1] += counter[i]\n    # 4. Traverse nums in reverse order, placing each element into the result array res\n    # Initialize the array res to record results\n    n = len(nums)\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        num = nums[i]\n        res[counter[num] - 1] = num  # Place num at the corresponding index\n        counter[num] -= 1  # Decrement the prefix sum by 1, getting the next index to place num\n    # Use result array res to overwrite the original array nums\n    for i in range(n):\n        nums[i] = res[i]\n
counting_sort.cpp
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(vector<int> &nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    int n = nums.size();\n    vector<int> res(n);\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--;              // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    nums = res;\n}\n
counting_sort.java
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    int n = nums.length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.cs
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid CountingSort(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    int n = nums.Length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.go
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunc countingSort(nums []int) {\n    // 1. Count the maximum element m in the array\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i := 0; i < m; i++ {\n        counter[i+1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    n := len(nums)\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        num := nums[i]\n        // Place num at the corresponding index\n        res[counter[num]-1] = num\n        // Decrement the prefix sum by 1, getting the next index to place num\n        counter[num]--\n    }\n    // Use result array res to overwrite the original array nums\n    copy(nums, res)\n}\n
counting_sort.swift
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunc countingSort(nums: inout [Int]) {\n    // 1. Count the maximum element m in the array\n    let m = nums.max()!\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i in 0 ..< m {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let num = nums[i]\n        res[counter[num] - 1] = num // Place num at the corresponding index\n        counter[num] -= 1 // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n
counting_sort.js
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunction countingSort(nums) {\n    // 1. Count the maximum element m in the array\n    let m = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    const n = nums.length;\n    const res = new Array(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.ts
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunction countingSort(nums: number[]): void {\n    // 1. Count the maximum element m in the array\n    let m: number = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    const n = nums.length;\n    const res: number[] = new Array<number>(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.dart
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(List<int> nums) {\n  // 1. Count the maximum element m in the array\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Count the occurrence of each number\n  // counter[_num] represents occurrence count of _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n  // That is, counter[_num]-1 is the last occurrence index of _num in res\n  for (int i = 0; i < m; i++) {\n    counter[i + 1] += counter[i];\n  }\n  // 4. Traverse nums in reverse order, placing each element into the result array res\n  // Initialize the array res to record results\n  int n = nums.length;\n  List<int> res = List.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int _num = nums[i];\n    res[counter[_num] - 1] = _num; // Place _num at corresponding index\n    counter[_num]--; // Decrement prefix sum by 1 to get next placement index for _num\n  }\n  // Use result array res to overwrite the original array nums\n  nums.setAll(0, res);\n}\n
counting_sort.rs
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfn counting_sort(nums: &mut [i32]) {\n    // 1. Count the maximum element m in the array\n    let m = *nums.iter().max().unwrap() as usize;\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    let mut counter = vec![0; m + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i in 0..m {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    let n = nums.len();\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let num = nums[i];\n        res[counter[num as usize] - 1] = num; // Place num at the corresponding index\n        counter[num as usize] -= 1; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    nums.copy_from_slice(&res)\n}\n
counting_sort.c
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(int nums[], int size) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int *counter = calloc(m, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    int *res = malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--;              // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    memcpy(nums, res, size * sizeof(int));\n    // 5. Free memory\n    free(res);\n    free(counter);\n}\n
counting_sort.kt
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfun countingSort(nums: IntArray) {\n    // 1. Count the maximum element m in the array\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (i in 0..<m) {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    val n = nums.size\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val num = nums[i]\n        res[counter[num] - 1] = num // Place num at the corresponding index\n        counter[num]-- // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (i in 0..<n) {\n        nums[i] = res[i]\n    }\n}\n
counting_sort.rb
### Counting sort ###\ndef counting_sort(nums)\n  # Complete implementation, can sort objects and is a stable sort\n  # 1. Count the maximum element m in the array\n  m = nums.max\n  # 2. Count the occurrence of each number\n  # counter[num] represents the occurrence of num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n  # counter[num]-1 is the last index where num appears in res\n  (0...m).each { |i| counter[i + 1] += counter[i] }\n  # 4. Traverse nums in reverse, fill elements into result array res\n  # Initialize the array res to record results\n  n = nums.length\n  res = Array.new(n, 0)\n  (n - 1).downto(0).each do |i|\n    num = nums[i]\n    res[counter[num] - 1] = num # Place num at the corresponding index\n    counter[num] -= 1 # Decrement the prefix sum by 1, getting the next index to place num\n  end\n  # Use result array res to overwrite the original array nums\n  (0...n).each { |i| nums[i] = res[i] }\nend\n
","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1193-algorithm-characteristics","level":2,"title":"11.9.3   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n + m)\\), non-adaptive sorting: Involves traversing nums and traversing counter, both using linear time. Generally, \\(n \\gg m\\), and time complexity tends toward \\(O(n)\\).
  • Space complexity of \\(O(n + m)\\), non-in-place sorting: Uses arrays res and counter of lengths \\(n\\) and \\(m\\) respectively.
  • Stable sorting: Since elements are filled into res in a \"right-to-left\" order, traversing nums in reverse can avoid changing the relative positions of equal elements, thereby achieving stable sorting. In fact, traversing nums in forward order can also yield correct sorting results, but the result would be unstable.
","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1194-limitations","level":2,"title":"11.9.4   Limitations","text":"

By this point, you might think counting sort is very clever, as it can achieve efficient sorting just by counting quantities. However, the prerequisites for using counting sort are relatively strict.

Counting sort is only suitable for non-negative integers. If you want to apply it to other types of data, you need to ensure that the data can be converted to non-negative integers without changing the relative size relationships between elements. For example, for an integer array containing negative numbers, you can first add a constant to all numbers to convert them all to positive numbers, and then convert them back after sorting is complete.

Counting sort is suitable for situations where the data volume is large but the data range is small. For example, in the above example, \\(m\\) cannot be too large, otherwise it will occupy too much space. And when \\(n \\ll m\\), counting sort uses \\(O(m)\\) time, which may be slower than \\(O(n \\log n)\\) sorting algorithms.

","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/","level":1,"title":"11.7   Heap Sort","text":"

Tip

Before reading this section, please ensure you have completed the \"Heap\" chapter.

Heap sort (heap sort) is an efficient sorting algorithm based on the heap data structure. We can use the \"build heap operation\" and \"element out-heap operation\" that we have already learned to implement heap sort.

  1. Input the array and build a min-heap, at which point the smallest element is at the heap top.
  2. Continuously perform the out-heap operation, record the out-heap elements in sequence, and an ascending sorted sequence can be obtained.

Although the above method is feasible, it requires an additional array to save the popped elements, which is quite wasteful of space. In practice, we usually use a more elegant implementation method.

","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1171-algorithm-flow","level":2,"title":"11.7.1   Algorithm Flow","text":"

Assume the array length is \\(n\\). The flow of heap sort is shown in Figure 11-12.

  1. Input the array and build a max-heap. After completion, the largest element is at the heap top.
  2. Swap the heap top element (first element) with the heap bottom element (last element). After the swap is complete, reduce the heap length by \\(1\\) and increase the count of sorted elements by \\(1\\).
  3. Starting from the heap top element, perform top-to-bottom heapify operation (sift down). After heapify is complete, the heap property is restored.
  4. Loop through steps 2. and 3. After looping \\(n - 1\\) rounds, the array sorting can be completed.

Tip

In fact, the element out-heap operation also includes steps 2. and 3., with just an additional step to pop the element.

<1><2><3><4><5><6><7><8><9><10><11><12>

Figure 11-12   Heap sort steps

In the code implementation, we use the same top-to-bottom heapify function sift_down() from the \"Heap\" chapter. It is worth noting that since the heap length will decrease as the largest element is extracted, we need to add a length parameter \\(n\\) to the sift_down() function to specify the current effective length of the heap. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap_sort.py
def sift_down(nums: list[int], n: int, i: int):\n    \"\"\"Heap length is n, start heapifying node i, from top to bottom\"\"\"\n    while True:\n        # Determine the largest node among i, l, r, noted as ma\n        l = 2 * i + 1\n        r = 2 * i + 2\n        ma = i\n        if l < n and nums[l] > nums[ma]:\n            ma = l\n        if r < n and nums[r] > nums[ma]:\n            ma = r\n        # If node i is the largest or indices l, r are out of bounds, no further heapification needed, break\n        if ma == i:\n            break\n        # Swap two nodes\n        nums[i], nums[ma] = nums[ma], nums[i]\n        # Loop downwards heapification\n        i = ma\n\ndef heap_sort(nums: list[int]):\n    \"\"\"Heap sort\"\"\"\n    # Build heap operation: heapify all nodes except leaves\n    for i in range(len(nums) // 2 - 1, -1, -1):\n        sift_down(nums, len(nums), i)\n    # Extract the largest element from the heap and repeat for n-1 rounds\n    for i in range(len(nums) - 1, 0, -1):\n        # Swap the root node with the rightmost leaf node (swap the first element with the last element)\n        nums[0], nums[i] = nums[i], nums[0]\n        # Start heapifying the root node, from top to bottom\n        sift_down(nums, i, 0)\n
heap_sort.cpp
/* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(vector<int> &nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Swap two nodes\n        if (ma == i) {\n            break;\n        }\n        // Swap two nodes\n        swap(nums[i], nums[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(vector<int> &nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.size() / 2 - 1; i >= 0; --i) {\n        siftDown(nums, nums.size(), i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.size() - 1; i > 0; --i) {\n        // Delete node\n        swap(nums[0], nums[i]);\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.java
/* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(int[] nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(int[] nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.length / 2 - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.cs
/* Heap length is n, start heapifying node i, from top to bottom */\nvoid SiftDown(int[] nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        (nums[ma], nums[i]) = (nums[i], nums[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid HeapSort(int[] nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.Length / 2 - 1; i >= 0; i--) {\n        SiftDown(nums, nums.Length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Delete node\n        (nums[i], nums[0]) = (nums[0], nums[i]);\n        // Start heapifying the root node, from top to bottom\n        SiftDown(nums, i, 0);\n    }\n}\n
heap_sort.go
/* Heap length is n, start heapifying node i, from top to bottom */\nfunc siftDown(nums *[]int, n, i int) {\n    for true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        l := 2*i + 1\n        r := 2*i + 2\n        ma := i\n        if l < n && (*nums)[l] > (*nums)[ma] {\n            ma = l\n        }\n        if r < n && (*nums)[r] > (*nums)[ma] {\n            ma = r\n        }\n        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfunc heapSort(nums *[]int) {\n    // Build heap operation: heapify all nodes except leaves\n    for i := len(*nums)/2 - 1; i >= 0; i-- {\n        siftDown(nums, len(*nums), i)\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i := len(*nums) - 1; i > 0; i-- {\n        // Delete node\n        (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0)\n    }\n}\n
heap_sort.swift
/* Heap length is n, start heapifying node i, from top to bottom */\nfunc siftDown(nums: inout [Int], n: Int, i: Int) {\n    var i = i\n    while true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let l = 2 * i + 1\n        let r = 2 * i + 2\n        var ma = i\n        if l < n, nums[l] > nums[ma] {\n            ma = l\n        }\n        if r < n, nums[r] > nums[ma] {\n            ma = r\n        }\n        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        nums.swapAt(i, ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfunc heapSort(nums: inout [Int]) {\n    // Build heap operation: heapify all nodes except leaves\n    for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {\n        siftDown(nums: &nums, n: nums.count, i: i)\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i in nums.indices.dropFirst().reversed() {\n        // Delete node\n        nums.swapAt(0, i)\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums: &nums, n: i, i: 0)\n    }\n}\n
heap_sort.js
/* Heap length is n, start heapifying node i, from top to bottom */\nfunction siftDown(nums, n, i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // Swap two nodes\n        if (ma === i) {\n            break;\n        }\n        // Swap two nodes\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfunction heapSort(nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.ts
/* Heap length is n, start heapifying node i, from top to bottom */\nfunction siftDown(nums: number[], n: number, i: number): void {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // Swap two nodes\n        if (ma === i) {\n            break;\n        }\n        // Swap two nodes\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfunction heapSort(nums: number[]): void {\n    // Build heap operation: heapify all nodes except leaves\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.dart
/* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(List<int> nums, int n, int i) {\n  while (true) {\n    // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n    int l = 2 * i + 1;\n    int r = 2 * i + 2;\n    int ma = i;\n    if (l < n && nums[l] > nums[ma]) ma = l;\n    if (r < n && nums[r] > nums[ma]) ma = r;\n    // Swap two nodes\n    if (ma == i) break;\n    // Swap two nodes\n    int temp = nums[i];\n    nums[i] = nums[ma];\n    nums[ma] = temp;\n    // Loop downwards heapification\n    i = ma;\n  }\n}\n\n/* Heap sort */\nvoid heapSort(List<int> nums) {\n  // Build heap operation: heapify all nodes except leaves\n  for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {\n    siftDown(nums, nums.length, i);\n  }\n  // Extract the largest element from the heap and repeat for n-1 rounds\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Delete node\n    int tmp = nums[0];\n    nums[0] = nums[i];\n    nums[i] = tmp;\n    // Start heapifying the root node, from top to bottom\n    siftDown(nums, i, 0);\n  }\n}\n
heap_sort.rs
/* Heap length is n, start heapifying node i, from top to bottom */\nfn sift_down(nums: &mut [i32], n: usize, mut i: usize) {\n    loop {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let mut ma = i;\n        if l < n && nums[l] > nums[ma] {\n            ma = l;\n        }\n        if r < n && nums[r] > nums[ma] {\n            ma = r;\n        }\n        // Swap two nodes\n        if ma == i {\n            break;\n        }\n        // Swap two nodes\n        nums.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfn heap_sort(nums: &mut [i32]) {\n    // Build heap operation: heapify all nodes except leaves\n    for i in (0..nums.len() / 2).rev() {\n        sift_down(nums, nums.len(), i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i in (1..nums.len()).rev() {\n        // Delete node\n        nums.swap(0, i);\n        // Start heapifying the root node, from top to bottom\n        sift_down(nums, i, 0);\n    }\n}\n
heap_sort.c
/* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(int nums[], int n, int i) {\n    while (1) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Swap two nodes\n        if (ma == i) {\n            break;\n        }\n        // Swap two nodes\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(int nums[], int n) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = n / 2 - 1; i >= 0; --i) {\n        siftDown(nums, n, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = n - 1; i > 0; --i) {\n        // Delete node\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.kt
/* Heap length is n, start heapifying node i, from top to bottom */\nfun siftDown(nums: IntArray, n: Int, li: Int) {\n    var i = li\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        val l = 2 * i + 1\n        val r = 2 * i + 2\n        var ma = i\n        if (l < n && nums[l] > nums[ma]) \n            ma = l\n        if (r < n && nums[r] > nums[ma]) \n            ma = r\n        // Swap two nodes\n        if (ma == i) \n            break\n        // Swap two nodes\n        val temp = nums[i]\n        nums[i] = nums[ma]\n        nums[ma] = temp\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfun heapSort(nums: IntArray) {\n    // Build heap operation: heapify all nodes except leaves\n    for (i in nums.size / 2 - 1 downTo 0) {\n        siftDown(nums, nums.size, i)\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (i in nums.size - 1 downTo 1) {\n        // Delete node\n        val temp = nums[0]\n        nums[0] = nums[i]\n        nums[i] = temp\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0)\n    }\n}\n
heap_sort.rb
### Heap length is n, heapify from node i, top to bottom ###\ndef sift_down(nums, n, i)\n  while true\n    # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n    l = 2 * i + 1\n    r = 2 * i + 2\n    ma = i\n    ma = l if l < n && nums[l] > nums[ma]\n    ma = r if r < n && nums[r] > nums[ma]\n    # Swap two nodes\n    break if ma == i\n    # Swap two nodes\n    nums[i], nums[ma] = nums[ma], nums[i]\n    # Loop downwards heapification\n    i = ma\n  end\nend\n\n### Heap sort ###\ndef heap_sort(nums)\n  # Build heap operation: heapify all nodes except leaves\n  (nums.length / 2 - 1).downto(0) do |i|\n    sift_down(nums, nums.length, i)\n  end\n  # Extract the largest element from the heap and repeat for n-1 rounds\n  (nums.length - 1).downto(1) do |i|\n    # Delete node\n    nums[0], nums[i] = nums[i], nums[0]\n    # Start heapifying the root node, from top to bottom\n    sift_down(nums, i, 0)\n  end\nend\n
","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1172-algorithm-characteristics","level":2,"title":"11.7.2   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n \\log n)\\), non-adaptive sorting: The build heap operation uses \\(O(n)\\) time. Extracting the largest element from the heap has a time complexity of \\(O(\\log n)\\), looping a total of \\(n - 1\\) rounds.
  • Space complexity of \\(O(1)\\), in-place sorting: A few pointer variables use \\(O(1)\\) space. Element swapping and heapify operations are both performed on the original array.
  • Non-stable sorting: When swapping the heap top element and heap bottom element, the relative positions of equal elements may change.
","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/","level":1,"title":"11.4   Insertion Sort","text":"

Insertion sort (insertion sort) is a simple sorting algorithm that works very similarly to the process of manually organizing a deck of cards.

Specifically, we select a base element from the unsorted interval, compare the element with elements in the sorted interval to its left one by one, and insert the element into the correct position.

Figure 11-6 shows the operation flow of inserting an element into the array. Let the base element be base. We need to move all elements from the target index to base one position to the right, and then assign base to the target index.

Figure 11-6   Single insertion operation

","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141-algorithm-flow","level":2,"title":"11.4.1   Algorithm Flow","text":"

The overall flow of insertion sort is shown in Figure 11-7.

  1. Initially, the first element of the array has completed sorting.
  2. Select the second element of the array as base, and after inserting it into the correct position, the first 2 elements of the array are sorted.
  3. Select the third element as base, and after inserting it into the correct position, the first 3 elements of the array are sorted.
  4. And so on. In the last round, select the last element as base, and after inserting it into the correct position, all elements are sorted.

Figure 11-7   Insertion sort flow

Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby insertion_sort.py
def insertion_sort(nums: list[int]):\n    \"\"\"Insertion sort\"\"\"\n    # Outer loop: sorted interval is [0, i-1]\n    for i in range(1, len(nums)):\n        base = nums[i]\n        j = i - 1\n        # Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0 and nums[j] > base:\n            nums[j + 1] = nums[j]  # Move nums[j] to the right by one position\n            j -= 1\n        nums[j + 1] = base  # Assign base to the correct position\n
insertion_sort.cpp
/* Insertion sort */\nvoid insertionSort(vector<int> &nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.size(); i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
insertion_sort.java
/* Insertion sort */\nvoid insertionSort(int[] nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.length; i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base;        // Assign base to the correct position\n    }\n}\n
insertion_sort.cs
/* Insertion sort */\nvoid InsertionSort(int[] nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.Length; i++) {\n        int bas = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > bas) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = bas;         // Assign base to the correct position\n    }\n}\n
insertion_sort.go
/* Insertion sort */\nfunc insertionSort(nums []int) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i := 1; i < len(nums); i++ {\n        base := nums[i]\n        j := i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        for j >= 0 && nums[j] > base {\n            nums[j+1] = nums[j] // Move nums[j] to the right by one position\n            j--\n        }\n        nums[j+1] = base // Assign base to the correct position\n    }\n}\n
insertion_sort.swift
/* Insertion sort */\nfunc insertionSort(nums: inout [Int]) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i in nums.indices.dropFirst() {\n        let base = nums[i]\n        var j = i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0, nums[j] > base {\n            nums[j + 1] = nums[j] // Move nums[j] to the right by one position\n            j -= 1\n        }\n        nums[j + 1] = base // Assign base to the correct position\n    }\n}\n
insertion_sort.js
/* Insertion sort */\nfunction insertionSort(nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        let base = nums[i],\n            j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
insertion_sort.ts
/* Insertion sort */\nfunction insertionSort(nums: number[]): void {\n    // Outer loop: sorted interval is [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        const base = nums[i];\n        let j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
insertion_sort.dart
/* Insertion sort */\nvoid insertionSort(List<int> nums) {\n  // Outer loop: sorted interval is [0, i-1]\n  for (int i = 1; i < nums.length; i++) {\n    int base = nums[i], j = i - 1;\n    // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n    while (j >= 0 && nums[j] > base) {\n      nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n      j--;\n    }\n    nums[j + 1] = base; // Assign base to the correct position\n  }\n}\n
insertion_sort.rs
/* Insertion sort */\nfn insertion_sort(nums: &mut [i32]) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i in 1..nums.len() {\n        let (base, mut j) = (nums[i], (i - 1) as i32);\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0 && nums[j as usize] > base {\n            nums[(j + 1) as usize] = nums[j as usize]; // Move nums[j] to the right by one position\n            j -= 1;\n        }\n        nums[(j + 1) as usize] = base; // Assign base to the correct position\n    }\n}\n
insertion_sort.c
/* Insertion sort */\nvoid insertionSort(int nums[], int size) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < size; i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            // Move nums[j] to the right by one position\n            nums[j + 1] = nums[j];\n            j--;\n        }\n        // Assign base to the correct position\n        nums[j + 1] = base;\n    }\n}\n
insertion_sort.kt
/* Insertion sort */\nfun insertionSort(nums: IntArray) {\n    // Outer loop: sorted elements are 1, 2, ..., n\n    for (i in nums.indices) {\n        val base = nums[i]\n        var j = i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j] // Move nums[j] to the right by one position\n            j--\n        }\n        nums[j + 1] = base        // Assign base to the correct position\n    }\n}\n
insertion_sort.rb
### Insertion sort ###\ndef insertion_sort(nums)\n  n = nums.length\n  # Outer loop: sorted interval is [0, i-1]\n  for i in 1...n\n    base = nums[i]\n    j = i - 1\n    # Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n    while j >= 0 && nums[j] > base\n      nums[j + 1] = nums[j] # Move nums[j] to the right by one position\n      j -= 1\n    end\n    nums[j + 1] = base # Assign base to the correct position\n  end\nend\n
","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1142-algorithm-characteristics","level":2,"title":"11.4.2   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n^2)\\), adaptive sorting: In the worst case, each insertion operation requires loops of \\(n - 1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), \\(1\\), summing to \\((n - 1) n / 2\\), so the time complexity is \\(O(n^2)\\). When encountering ordered data, the insertion operation will terminate early. When the input array is completely ordered, insertion sort achieves the best-case time complexity of \\(O(n)\\).
  • Space complexity of \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
  • Stable sorting: During the insertion operation process, we insert elements to the right of equal elements, without changing their order.
","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1143-advantages-of-insertion-sort","level":2,"title":"11.4.3   Advantages of Insertion Sort","text":"

The time complexity of insertion sort is \\(O(n^2)\\), while the time complexity of quick sort, which we will learn about next, is \\(O(n \\log n)\\). Although insertion sort has a higher time complexity, insertion sort is usually faster for smaller data volumes.

This conclusion is similar to the applicable situations of linear search and binary search. Algorithms like quick sort with \\(O(n \\log n)\\) complexity are sorting algorithms based on divide-and-conquer strategy and often contain more unit computation operations. When the data volume is small, \\(n^2\\) and \\(n \\log n\\) are numerically close, and complexity does not dominate; the number of unit operations per round plays a decisive role.

In fact, the built-in sorting functions in many programming languages (such as Java) adopt insertion sort. The general approach is: for long arrays, use sorting algorithms based on divide-and-conquer strategy, such as quick sort; for short arrays, directly use insertion sort.

Although bubble sort, selection sort, and insertion sort all have a time complexity of \\(O(n^2)\\), in actual situations, insertion sort is used significantly more frequently than bubble sort and selection sort, mainly for the following reasons.

  • Bubble sort is based on element swapping, requiring the use of a temporary variable, involving 3 unit operations; insertion sort is based on element assignment, requiring only 1 unit operation. Therefore, the computational overhead of bubble sort is usually higher than that of insertion sort.
  • Selection sort has a time complexity of \\(O(n^2)\\) in any case. If given a set of partially ordered data, insertion sort is usually more efficient than selection sort.
  • Selection sort is unstable and cannot be applied to multi-level sorting.
","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   Merge Sort","text":"

Merge sort (merge sort) is a sorting algorithm based on the divide-and-conquer strategy, which includes the \"divide\" and \"merge\" phases shown in Figure 11-10.

  1. Divide phase: Recursively split the array from the midpoint, transforming the sorting problem of a long array into the sorting problems of shorter arrays.
  2. Merge phase: When the sub-array length is 1, terminate the division and start merging, continuously merging two shorter sorted arrays into one longer sorted array until the process is complete.

Figure 11-10   Divide and merge phases of merge sort

","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161-algorithm-flow","level":2,"title":"11.6.1   Algorithm Flow","text":"

As shown in Figure 11-11, the \"divide phase\" recursively splits the array from the midpoint into two sub-arrays from top to bottom.

  1. Calculate the array midpoint mid, recursively divide the left sub-array (interval [left, mid]) and right sub-array (interval [mid + 1, right]).
  2. Recursively execute step 1. until the sub-array interval length is 1, then terminate.

The \"merge phase\" merges the left sub-array and right sub-array into a sorted array from bottom to top. Note that merging starts from sub-arrays of length 1, and each sub-array in the merge phase is sorted.

<1><2><3><4><5><6><7><8><9><10>

Figure 11-11   Merge sort steps

It can be observed that the recursive order of merge sort is consistent with the post-order traversal of a binary tree.

  • Post-order traversal: First recursively traverse the left subtree, then recursively traverse the right subtree, and finally process the root node.
  • Merge sort: First recursively process the left sub-array, then recursively process the right sub-array, and finally perform the merge.

The implementation of merge sort is shown in the code below. Note that the interval to be merged in nums is [left, right], while the corresponding interval in tmp is [0, right - left].

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby merge_sort.py
def merge(nums: list[int], left: int, mid: int, right: int):\n    \"\"\"Merge left subarray and right subarray\"\"\"\n    # Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    # Create a temporary array tmp to store the merged results\n    tmp = [0] * (right - left + 1)\n    # Initialize the start indices of the left and right subarrays\n    i, j, k = left, mid + 1, 0\n    # While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while i <= mid and j <= right:\n        if nums[i] <= nums[j]:\n            tmp[k] = nums[i]\n            i += 1\n        else:\n            tmp[k] = nums[j]\n            j += 1\n        k += 1\n    # Copy the remaining elements of the left and right subarrays into the temporary array\n    while i <= mid:\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    while j <= right:\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    # Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k in range(0, len(tmp)):\n        nums[left + k] = tmp[k]\n\ndef merge_sort(nums: list[int], left: int, right: int):\n    \"\"\"Merge sort\"\"\"\n    # Termination condition\n    if left >= right:\n        return  # Terminate recursion when subarray length is 1\n    # Divide and conquer stage\n    mid = (left + right) // 2  # Calculate midpoint\n    merge_sort(nums, left, mid)  # Recursively process the left subarray\n    merge_sort(nums, mid + 1, right)  # Recursively process the right subarray\n    # Merge stage\n    merge(nums, left, mid, right)\n
merge_sort.cpp
/* Merge left subarray and right subarray */\nvoid merge(vector<int> &nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    vector<int> tmp(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.size(); k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid mergeSort(vector<int> &nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    mergeSort(nums, left, mid);      // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.java
/* Merge left subarray and right subarray */\nvoid merge(int[] nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int[] tmp = new int[right - left + 1];\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid mergeSort(int[] nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2; // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.cs
/* Merge left subarray and right subarray */\nvoid Merge(int[] nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int[] tmp = new int[right - left + 1];\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.Length; ++k) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid MergeSort(int[] nums, int left, int right) {\n    // Termination condition\n    if (left >= right) return;       // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    MergeSort(nums, left, mid);      // Recursively process the left subarray\n    MergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    Merge(nums, left, mid, right);\n}\n
merge_sort.go
/* Merge left subarray and right subarray */\nfunc merge(nums []int, left, mid, right int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    tmp := make([]int, right-left+1)\n    // Initialize the start indices of the left and right subarrays\n    i, j, k := left, mid+1, 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    for i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i++\n        } else {\n            tmp[k] = nums[j]\n            j++\n        }\n        k++\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    for i <= mid {\n        tmp[k] = nums[i]\n        i++\n        k++\n    }\n    for j <= right {\n        tmp[k] = nums[j]\n        j++\n        k++\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k := 0; k < len(tmp); k++ {\n        nums[left+k] = tmp[k]\n    }\n}\n\n/* Merge sort */\nfunc mergeSort(nums []int, left, right int) {\n    // Termination condition\n    if left >= right {\n        return\n    }\n    // Divide and conquer stage\n    mid := left + (right - left) / 2\n    mergeSort(nums, left, mid)\n    mergeSort(nums, mid+1, right)\n    // Merge stage\n    merge(nums, left, mid, right)\n}\n
merge_sort.swift
/* Merge left subarray and right subarray */\nfunc merge(nums: inout [Int], left: Int, mid: Int, right: Int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    var tmp = Array(repeating: 0, count: right - left + 1)\n    // Initialize the start indices of the left and right subarrays\n    var i = left, j = mid + 1, k = 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while i <= mid, j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i += 1\n        } else {\n            tmp[k] = nums[j]\n            j += 1\n        }\n        k += 1\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while i <= mid {\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    }\n    while j <= right {\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k in tmp.indices {\n        nums[left + k] = tmp[k]\n    }\n}\n\n/* Merge sort */\nfunc mergeSort(nums: inout [Int], left: Int, right: Int) {\n    // Termination condition\n    if left >= right { // Terminate recursion when subarray length is 1\n        return\n    }\n    // Divide and conquer stage\n    let mid = left + (right - left) / 2 // Calculate midpoint\n    mergeSort(nums: &nums, left: left, right: mid) // Recursively process the left subarray\n    mergeSort(nums: &nums, left: mid + 1, right: right) // Recursively process the right subarray\n    // Merge stage\n    merge(nums: &nums, left: left, mid: mid, right: right)\n}\n
merge_sort.js
/* Merge left subarray and right subarray */\nfunction merge(nums, left, mid, right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    const tmp = new Array(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfunction mergeSort(nums, left, right) {\n    // Termination condition\n    if (left >= right) return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.ts
/* Merge left subarray and right subarray */\nfunction merge(nums: number[], left: number, mid: number, right: number): void {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    const tmp = new Array(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfunction mergeSort(nums: number[], left: number, right: number): void {\n    // Termination condition\n    if (left >= right) return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.dart
/* Merge left subarray and right subarray */\nvoid merge(List<int> nums, int left, int mid, int right) {\n  // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n  // Create a temporary array tmp to store the merged results\n  List<int> tmp = List.filled(right - left + 1, 0);\n  // Initialize the start indices of the left and right subarrays\n  int i = left, j = mid + 1, k = 0;\n  // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n  while (i <= mid && j <= right) {\n    if (nums[i] <= nums[j])\n      tmp[k++] = nums[i++];\n    else\n      tmp[k++] = nums[j++];\n  }\n  // Copy the remaining elements of the left and right subarrays into the temporary array\n  while (i <= mid) {\n    tmp[k++] = nums[i++];\n  }\n  while (j <= right) {\n    tmp[k++] = nums[j++];\n  }\n  // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n  for (k = 0; k < tmp.length; k++) {\n    nums[left + k] = tmp[k];\n  }\n}\n\n/* Merge sort */\nvoid mergeSort(List<int> nums, int left, int right) {\n  // Termination condition\n  if (left >= right) return; // Terminate recursion when subarray length is 1\n  // Divide and conquer stage\n  int mid = left + (right - left) ~/ 2; // Calculate midpoint\n  mergeSort(nums, left, mid); // Recursively process the left subarray\n  mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n  // Merge stage\n  merge(nums, left, mid, right);\n}\n
merge_sort.rs
/* Merge left subarray and right subarray */\nfn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    let tmp_size = right - left + 1;\n    let mut tmp = vec![0; tmp_size];\n    // Initialize the start indices of the left and right subarrays\n    let (mut i, mut j, mut k) = (left, mid + 1, 0);\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i];\n            i += 1;\n        } else {\n            tmp[k] = nums[j];\n            j += 1;\n        }\n        k += 1;\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while i <= mid {\n        tmp[k] = nums[i];\n        k += 1;\n        i += 1;\n    }\n    while j <= right {\n        tmp[k] = nums[j];\n        k += 1;\n        j += 1;\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k in 0..tmp_size {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfn merge_sort(nums: &mut [i32], left: usize, right: usize) {\n    // Termination condition\n    if left >= right {\n        return; // Terminate recursion when subarray length is 1\n    }\n\n    // Divide and conquer stage\n    let mid = left + (right - left) / 2; // Calculate midpoint\n    merge_sort(nums, left, mid); // Recursively process the left subarray\n    merge_sort(nums, mid + 1, right); // Recursively process the right subarray\n\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.c
/* Merge left subarray and right subarray */\nvoid merge(int *nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int tmpSize = right - left + 1;\n    int *tmp = (int *)malloc(tmpSize * sizeof(int));\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmpSize; ++k) {\n        nums[left + k] = tmp[k];\n    }\n    // Free memory\n    free(tmp);\n}\n\n/* Merge sort */\nvoid mergeSort(int *nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    mergeSort(nums, left, mid);      // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.kt
/* Merge left subarray and right subarray */\nfun merge(nums: IntArray, left: Int, mid: Int, right: Int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    val tmp = IntArray(right - left + 1)\n    // Initialize the start indices of the left and right subarrays\n    var i = left\n    var j = mid + 1\n    var k = 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++]\n        else\n            tmp[k++] = nums[j++]\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++]\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++]\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (l in tmp.indices) {\n        nums[left + l] = tmp[l]\n    }\n}\n\n/* Merge sort */\nfun mergeSort(nums: IntArray, left: Int, right: Int) {\n    // Termination condition\n    if (left >= right) return  // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    val mid = left + (right - left) / 2 // Calculate midpoint\n    mergeSort(nums, left, mid) // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right) // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right)\n}\n
merge_sort.rb
### Merge left and right subarrays ###\ndef merge(nums, left, mid, right)\n  # Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n  # Create temporary array tmp to store merged result\n  tmp = Array.new(right - left + 1, 0)\n  # Initialize the start indices of the left and right subarrays\n  i, j, k = left, mid + 1, 0\n  # While both subarrays still have elements, compare and copy the smaller element into the temporary array\n  while i <= mid && j <= right\n    if nums[i] <= nums[j]\n      tmp[k] = nums[i]\n      i += 1\n    else\n      tmp[k] = nums[j]\n      j += 1\n    end\n    k += 1\n  end\n  # Copy the remaining elements of the left and right subarrays into the temporary array\n  while i <= mid\n    tmp[k] = nums[i]\n    i += 1\n    k += 1\n  end\n  while j <= right\n    tmp[k] = nums[j]\n    j += 1\n    k += 1\n  end\n  # Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n  (0...tmp.length).each do |k|\n    nums[left + k] = tmp[k]\n  end\nend\n\n### Merge sort ###\ndef merge_sort(nums, left, right)\n  # Termination condition\n  # Terminate recursion when subarray length is 1\n  return if left >= right\n  # Divide and conquer stage\n  mid = left + (right - left) / 2 # Calculate midpoint\n  merge_sort(nums, left, mid) # Recursively process the left subarray\n  merge_sort(nums, mid + 1, right) # Recursively process the right subarray\n  # Merge stage\n  merge(nums, left, mid, right)\nend\n
","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1162-algorithm-characteristics","level":2,"title":"11.6.2   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n \\log n)\\), non-adaptive sorting: The division produces a recursion tree of height \\(\\log n\\), and the total number of merge operations at each level is \\(n\\), so the overall time complexity is \\(O(n \\log n)\\).
  • Space complexity of \\(O(n)\\), non-in-place sorting: The recursion depth is \\(\\log n\\), using \\(O(\\log n)\\) size of stack frame space. The merge operation requires the aid of an auxiliary array, using \\(O(n)\\) size of additional space.
  • Stable sorting: In the merge process, the order of equal elements remains unchanged.
","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1163-linked-list-sorting","level":2,"title":"11.6.3   Linked List Sorting","text":"

For linked lists, merge sort has significant advantages over other sorting algorithms, and can optimize the space complexity of linked list sorting tasks to \\(O(1)\\).

  • Divide phase: \"Iteration\" can be used instead of \"recursion\" to implement linked list division work, thus saving the stack frame space used by recursion.
  • Merge phase: In linked lists, node insertion and deletion operations can be achieved by just changing references (pointers), so there is no need to create additional linked lists during the merge phase (merging two short ordered linked lists into one long ordered linked list).

The specific implementation details are quite complex, and interested readers can consult related materials for learning.

","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   Quick Sort","text":"

Quick sort (quick sort) is a sorting algorithm based on the divide-and-conquer strategy, which operates efficiently and is widely applied.

The core operation of quick sort is \"sentinel partitioning\", which aims to: select a certain element in the array as the \"pivot\", move all elements smaller than the pivot to its left, and move elements larger than the pivot to its right. Specifically, the flow of sentinel partitioning is shown in Figure 11-8.

  1. Select the leftmost element of the array as the pivot, and initialize two pointers i and j pointing to the two ends of the array.
  2. Set up a loop in which i (j) is used in each round to find the first element larger (smaller) than the pivot, and then swap these two elements.
  3. Loop through step 2. until i and j meet, and finally swap the pivot to the boundary line of the two sub-arrays.
<1><2><3><4><5><6><7><8><9>

Figure 11-8   Sentinel partitioning steps

After sentinel partitioning is complete, the original array is divided into three parts: left sub-array, pivot, right sub-array, satisfying \"any element in left sub-array \\(\\leq\\) pivot \\(\\leq\\) any element in right sub-array\". Therefore, we next only need to sort these two sub-arrays.

Divide-and-conquer strategy of quick sort

The essence of sentinel partitioning is to simplify the sorting problem of a longer array into the sorting problems of two shorter arrays.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Sentinel partition\"\"\"\n    # Use nums[left] as the pivot\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Search from right to left for the first element smaller than the pivot\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Search from left to right for the first element greater than the pivot\n        # Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    # Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Return the index of the pivot\n
quick_sort.cpp
/* Sentinel partition */\nint partition(vector<int> &nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Search from left to right for the first element greater than the pivot\n        swap(nums[i], nums[j]); // Swap these two elements\n    }\n    swap(nums[i], nums[left]);  // Swap the pivot to the boundary between the two subarrays\n    return i;                   // Return the index of the pivot\n}\n
quick_sort.java
/* Swap elements */\nvoid swap(int[] nums, int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Sentinel partition */\nint partition(int[] nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
quick_sort.cs
/* Swap elements */\nvoid Swap(int[] nums, int i, int j) {\n    (nums[j], nums[i]) = (nums[i], nums[j]);\n}\n\n/* Sentinel partition */\nint Partition(int[] nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        Swap(nums, i, j); // Swap these two elements\n    }\n    Swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
quick_sort.go
/* Sentinel partition */\nfunc (q *quickSort) partition(nums []int, left, right int) int {\n    // Use nums[left] as the pivot\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Search from right to left for the first element smaller than the pivot\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Return the index of the pivot\n}\n
quick_sort.swift
/* Sentinel partition */\nfunc partition(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while i < j {\n        while i < j, nums[j] >= nums[left] {\n            j -= 1 // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j, nums[i] <= nums[left] {\n            i += 1 // Search from left to right for the first element greater than the pivot\n        }\n        nums.swapAt(i, j) // Swap these two elements\n    }\n    nums.swapAt(i, left) // Swap the pivot to the boundary between the two subarrays\n    return i // Return the index of the pivot\n}\n
quick_sort.js
/* Swap elements */\nswap(nums, i, j) {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Sentinel partition */\npartition(nums, left, right) {\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
quick_sort.ts
/* Swap elements */\nswap(nums: number[], i: number, j: number): void {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Sentinel partition */\npartition(nums: number[], left: number, right: number): number {\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
quick_sort.dart
/* Swap elements */\nvoid _swap(List<int> nums, int i, int j) {\n  int tmp = nums[i];\n  nums[i] = nums[j];\n  nums[j] = tmp;\n}\n\n/* Sentinel partition */\nint _partition(List<int> nums, int left, int right) {\n  // Use nums[left] as the pivot\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n    while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n    _swap(nums, i, j); // Swap these two elements\n  }\n  _swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n  return i; // Return the index of the pivot\n}\n
quick_sort.rs
/* Sentinel partition */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Use nums[left] as the pivot\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        nums.swap(i, j); // Swap these two elements\n    }\n    nums.swap(i, left); // Swap the pivot to the boundary between the two subarrays\n    i // Return the index of the pivot\n}\n
quick_sort.c
/* Swap elements */\nvoid swap(int nums[], int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Sentinel partition */\nint partition(int nums[], int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap these two elements\n        swap(nums, i, j);\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    swap(nums, i, left);\n    // Return the index of the pivot\n    return i;\n}\n
quick_sort.kt
/* Swap elements */\nfun swap(nums: IntArray, i: Int, j: Int) {\n    val temp = nums[i]\n    nums[i] = nums[j]\n    nums[j] = temp\n}\n\n/* Sentinel partition */\nfun partition(nums: IntArray, left: Int, right: Int): Int {\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--           // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++           // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j)  // Swap these two elements\n    }\n    swap(nums, i, left)   // Swap the pivot to the boundary between the two subarrays\n    return i              // Return the index of the pivot\n}\n
quick_sort.rb
### Sentinel partition ###\ndef partition(nums, left, right)\n  # Use nums[left] as the pivot\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Search from right to left for the first element smaller than the pivot\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Search from left to right for the first element greater than the pivot\n    end\n    # Swap elements\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Swap the pivot to the boundary between the two subarrays\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Return the index of the pivot\nend\n
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1151-algorithm-flow","level":2,"title":"11.5.1   Algorithm Flow","text":"

The overall flow of quick sort is shown in Figure 11-9.

  1. First, perform one \"sentinel partitioning\" on the original array to obtain the unsorted left sub-array and right sub-array.
  2. Then, recursively perform \"sentinel partitioning\" on the left sub-array and right sub-array respectively.
  3. Continue recursively until the sub-array length is 1, at which point sorting of the entire array is complete.

Figure 11-9   Quick sort flow

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Quick sort\"\"\"\n    # Terminate recursion when subarray length is 1\n    if left >= right:\n        return\n    # Sentinel partition\n    pivot = self.partition(nums, left, right)\n    # Recursively process the left subarray and right subarray\n    self.quick_sort(nums, left, pivot - 1)\n    self.quick_sort(nums, pivot + 1, right)\n
quick_sort.cpp
/* Quick sort */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.java
/* Quick sort */\nvoid quickSort(int[] nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.cs
/* Quick sort */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = Partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    QuickSort(nums, left, pivot - 1);\n    QuickSort(nums, pivot + 1, right);\n}\n
quick_sort.go
/* Quick sort */\nfunc (q *quickSort) quickSort(nums []int, left, right int) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return\n    }\n    // Sentinel partition\n    pivot := q.partition(nums, left, right)\n    // Recursively process the left subarray and right subarray\n    q.quickSort(nums, left, pivot-1)\n    q.quickSort(nums, pivot+1, right)\n}\n
quick_sort.swift
/* Quick sort */\nfunc quickSort(nums: inout [Int], left: Int, right: Int) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return\n    }\n    // Sentinel partition\n    let pivot = partition(nums: &nums, left: left, right: right)\n    // Recursively process the left subarray and right subarray\n    quickSort(nums: &nums, left: left, right: pivot - 1)\n    quickSort(nums: &nums, left: pivot + 1, right: right)\n}\n
quick_sort.js
/* Quick sort */\nquickSort(nums, left, right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) return;\n    // Sentinel partition\n    const pivot = this.partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
quick_sort.ts
/* Quick sort */\nquickSort(nums: number[], left: number, right: number): void {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) {\n        return;\n    }\n    // Sentinel partition\n    const pivot = this.partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
quick_sort.dart
/* Quick sort */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Terminate recursion when subarray length is 1\n  if (left >= right) return;\n  // Sentinel partition\n  int pivot = _partition(nums, left, right);\n  // Recursively process the left subarray and right subarray\n  quickSort(nums, left, pivot - 1);\n  quickSort(nums, pivot + 1, right);\n}\n
quick_sort.rs
/* Quick sort */\npub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return;\n    }\n    // Sentinel partition\n    let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n    // Recursively process the left subarray and right subarray\n    Self::quick_sort(left, pivot - 1, nums);\n    Self::quick_sort(pivot + 1, right, nums);\n}\n
quick_sort.c
/* Quick sort */\nvoid quickSort(int nums[], int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) {\n        return;\n    }\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.kt
/* Quick sort */\nfun quickSort(nums: IntArray, left: Int, right: Int) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) return\n    // Sentinel partition\n    val pivot = partition(nums, left, right)\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1)\n    quickSort(nums, pivot + 1, right)\n}\n
quick_sort.rb
### Quick sort class ###\ndef quick_sort(nums, left, right)\n  # Recurse when subarray length is not 1\n  if left < right\n    # Sentinel partition\n    pivot = partition(nums, left, right)\n    # Recursively process the left subarray and right subarray\n    quick_sort(nums, left, pivot - 1)\n    quick_sort(nums, pivot + 1, right)\n  end\n  nums\nend\n
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1152-algorithm-characteristics","level":2,"title":"11.5.2   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n \\log n)\\), non-adaptive sorting: In the average case, the number of recursive levels of sentinel partitioning is \\(\\log n\\), and the total number of loops at each level is \\(n\\), using \\(O(n \\log n)\\) time overall. In the worst case, each round of sentinel partitioning divides an array of length \\(n\\) into two sub-arrays of length \\(0\\) and \\(n - 1\\), at which point the number of recursive levels reaches \\(n\\), the number of loops at each level is \\(n\\), and the total time used is \\(O(n^2)\\).
  • Space complexity of \\(O(n)\\), in-place sorting: In the case where the input array is completely reversed, the worst recursive depth reaches \\(n\\), using \\(O(n)\\) stack frame space. The sorting operation is performed on the original array without the aid of an additional array.
  • Non-stable sorting: In the last step of sentinel partitioning, the pivot may be swapped to the right of equal elements.
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1153-why-is-quick-sort-fast","level":2,"title":"11.5.3   Why Is Quick Sort Fast","text":"

From the name, we can see that quick sort should have certain advantages in terms of efficiency. Although the average time complexity of quick sort is the same as \"merge sort\" and \"heap sort\", quick sort is usually more efficient, mainly for the following reasons.

  • The probability of the worst case occurring is very low: Although the worst-case time complexity of quick sort is \\(O(n^2)\\), which is not as stable as merge sort, in the vast majority of cases, quick sort can run with a time complexity of \\(O(n \\log n)\\).
  • High cache utilization: When performing sentinel partitioning operations, the system can load the entire sub-array into the cache, so element access efficiency is relatively high. Algorithms like \"heap sort\" require jump-style access to elements, thus lacking this characteristic.
  • Small constant coefficient of complexity: Among the three algorithms mentioned above, quick sort has the smallest total number of operations such as comparisons, assignments, and swaps. This is similar to the reason why \"insertion sort\" is faster than \"bubble sort\".
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154-pivot-optimization","level":2,"title":"11.5.4   Pivot Optimization","text":"

Quick sort may have reduced time efficiency for certain inputs. Take an extreme example: suppose the input array is completely reversed. Since we select the leftmost element as the pivot, after sentinel partitioning is complete, the pivot is swapped to the rightmost end of the array, causing the left sub-array length to be \\(n - 1\\) and the right sub-array length to be \\(0\\). If we recurse down like this, each round of sentinel partitioning will have a sub-array length of \\(0\\), the divide-and-conquer strategy fails, and quick sort degrades to a form approximate to \"bubble sort\".

To avoid this situation as much as possible, we can optimize the pivot selection strategy in sentinel partitioning. For example, we can randomly select an element as the pivot. However, if luck is not good and we select a non-ideal pivot every time, efficiency is still not satisfactory.

It should be noted that programming languages usually generate \"pseudo-random numbers\". If we construct a specific test case for a pseudo-random number sequence, the efficiency of quick sort may still degrade.

For further improvement, we can select three candidate elements in the array (usually the first, last, and middle elements of the array), and use the median of these three candidate elements as the pivot. In this way, the probability that the pivot is \"neither too small nor too large\" will be greatly increased. Of course, we can also select more candidate elements to further improve the robustness of the algorithm. After adopting this method, the probability of time complexity degrading to \\(O(n^2)\\) is greatly reduced.

Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n    \"\"\"Select the median of three candidate elements\"\"\"\n    l, m, r = nums[left], nums[mid], nums[right]\n    if (l <= m <= r) or (r <= m <= l):\n        return mid  # m is between l and r\n    if (m <= l <= r) or (r <= l <= m):\n        return left  # l is between m and r\n    return right\n\ndef partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Sentinel partition (median of three)\"\"\"\n    # Use nums[left] as the pivot\n    med = self.median_three(nums, left, (left + right) // 2, right)\n    # Swap the median to the array's leftmost position\n    nums[left], nums[med] = nums[med], nums[left]\n    # Use nums[left] as the pivot\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Search from right to left for the first element smaller than the pivot\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Search from left to right for the first element greater than the pivot\n        # Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    # Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Return the index of the pivot\n
quick_sort.cpp
/* Select the median of three candidate elements */\nint medianThree(vector<int> &nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partition(vector<int> &nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums[left], nums[med]);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Search from left to right for the first element greater than the pivot\n        swap(nums[i], nums[j]); // Swap these two elements\n    }\n    swap(nums[i], nums[left]);  // Swap the pivot to the boundary between the two subarrays\n    return i;                   // Return the index of the pivot\n}\n
quick_sort.java
/* Select the median of three candidate elements */\nint medianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partition(int[] nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
quick_sort.cs
/* Select the median of three candidate elements */\nint MedianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint Partition(int[] nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = MedianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    Swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        Swap(nums, i, j); // Swap these two elements\n    }\n    Swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
quick_sort.go
/* Select the median of three candidate elements */\nfunc (q *quickSortMedian) medianThree(nums []int, left, mid, right int) int {\n    l, m, r := nums[left], nums[mid], nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l is between m and r\n    }\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfunc (q *quickSortMedian) partition(nums []int, left, right int) int {\n    // Use nums[left] as the pivot\n    med := q.medianThree(nums, left, (left+right)/2, right)\n    // Swap the median to the array's leftmost position\n    nums[left], nums[med] = nums[med], nums[left]\n    // Use nums[left] as the pivot\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Search from right to left for the first element smaller than the pivot\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Return the index of the pivot\n}\n
quick_sort.swift
/* Select the median of three candidate elements */\nfunc medianThree(nums: [Int], left: Int, mid: Int, right: Int) -> Int {\n    let l = nums[left]\n    let m = nums[mid]\n    let r = nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l is between m and r\n    }\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Select the median of three candidate elements\n    let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right)\n    // Swap the median to the array's leftmost position\n    nums.swapAt(left, med)\n    return partition(nums: &nums, left: left, right: right)\n}\n
quick_sort.js
/* Select the median of three candidate elements */\nmedianThree(nums, left, mid, right) {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m is between l and r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l is between m and r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Sentinel partition (median of three) */\npartition(nums, left, right) {\n    // Select the median of three candidate elements\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Swap the median to the array's leftmost position\n    this.swap(nums, left, med);\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
quick_sort.ts
/* Select the median of three candidate elements */\nmedianThree(\n    nums: number[],\n    left: number,\n    mid: number,\n    right: number\n): number {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m is between l and r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l is between m and r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Sentinel partition (median of three) */\npartition(nums: number[], left: number, right: number): number {\n    // Select the median of three candidate elements\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Swap the median to the array's leftmost position\n    this.swap(nums, left, med);\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Search from left to right for the first element greater than the pivot\n        }\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
quick_sort.dart
/* Select the median of three candidate elements */\nint _medianThree(List<int> nums, int left, int mid, int right) {\n  int l = nums[left], m = nums[mid], r = nums[right];\n  if ((l <= m && m <= r) || (r <= m && m <= l))\n    return mid; // m is between l and r\n  if ((m <= l && l <= r) || (r <= l && l <= m))\n    return left; // l is between m and r\n  return right;\n}\n\n/* Sentinel partition (median of three) */\nint _partition(List<int> nums, int left, int right) {\n  // Select the median of three candidate elements\n  int med = _medianThree(nums, left, (left + right) ~/ 2, right);\n  // Swap the median to the array's leftmost position\n  _swap(nums, left, med);\n  // Use nums[left] as the pivot\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n    while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n    _swap(nums, i, j); // Swap these two elements\n  }\n  _swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n  return i; // Return the index of the pivot\n}\n
quick_sort.rs
/* Select the median of three candidate elements */\nfn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize {\n    let (l, m, r) = (nums[left], nums[mid], nums[right]);\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid; // m is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left; // l is between m and r\n    }\n    right\n}\n\n/* Sentinel partition (median of three) */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Select the median of three candidate elements\n    let med = Self::median_three(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    nums.swap(left, med);\n    // Use nums[left] as the pivot\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        nums.swap(i, j); // Swap these two elements\n    }\n    nums.swap(i, left); // Swap the pivot to the boundary between the two subarrays\n    i // Return the index of the pivot\n}\n
quick_sort.c
/* Select the median of three candidate elements */\nint medianThree(int nums[], int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partitionMedian(int nums[], int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--; // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i;            // Return the index of the pivot\n}\n
quick_sort.kt
/* Select the median of three candidate elements */\nfun medianThree(nums: IntArray, left: Int, mid: Int, right: Int): Int {\n    val l = nums[left]\n    val m = nums[mid]\n    val r = nums[right]\n    if ((m in l..r) || (m in r..l))\n        return mid  // m is between l and r\n    if ((l in m..r) || (l in r..m))\n        return left // l is between m and r\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n    // Select the median of three candidate elements\n    val med = medianThree(nums, left, (left + right) / 2, right)\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med)\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--                      // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++                      // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j)             // Swap these two elements\n    }\n    swap(nums, i, left)              // Swap the pivot to the boundary between the two subarrays\n    return i                         // Return the index of the pivot\n}\n
quick_sort.rb
### Select median of three candidate elements ###\ndef median_three(nums, left, mid, right)\n  # Select the median of three candidate elements\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m is between l and r\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l is between m and r\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n### Sentinel partition (median of three) ###\ndef partition(nums, left, right)\n  ### Use nums[left] as pivot\n  med = median_three(nums, left, (left + right) / 2, right)\n  # Swap median to leftmost position of array\n  nums[left], nums[med] = nums[med], nums[left]\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Search from right to left for the first element smaller than the pivot\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Search from left to right for the first element greater than the pivot\n    end\n    # Swap elements\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Swap the pivot to the boundary between the two subarrays\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Return the index of the pivot\nend\n
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1155-recursive-depth-optimization","level":2,"title":"11.5.5   Recursive Depth Optimization","text":"

For certain inputs, quick sort may occupy more space. Taking a completely ordered input array as an example, let the length of the sub-array in recursion be \\(m\\). Each round of sentinel partitioning will produce a left sub-array of length \\(0\\) and a right sub-array of length \\(m - 1\\), which means that the problem scale reduced per recursive call is very small (only one element is reduced), and the height of the recursion tree will reach \\(n - 1\\), at which point \\(O(n)\\) size of stack frame space is required.

To prevent the accumulation of stack frame space, we can compare the lengths of the two sub-arrays after each round of sentinel sorting is complete, and only recurse on the shorter sub-array. Since the length of the shorter sub-array will not exceed \\(n / 2\\), this method can ensure that the recursion depth does not exceed \\(\\log n\\), thus optimizing the worst-case space complexity to \\(O(\\log n)\\). The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Quick sort (recursion depth optimization)\"\"\"\n    # Terminate when subarray length is 1\n    while left < right:\n        # Sentinel partition operation\n        pivot = self.partition(nums, left, right)\n        # Perform quick sort on the shorter of the two subarrays\n        if pivot - left < right - pivot:\n            self.quick_sort(nums, left, pivot - 1)  # Recursively sort the left subarray\n            left = pivot + 1  # Remaining unsorted interval is [pivot + 1, right]\n        else:\n            self.quick_sort(nums, pivot + 1, right)  # Recursively sort the right subarray\n            right = pivot - 1  # Remaining unsorted interval is [left, pivot - 1]\n
quick_sort.cpp
/* Quick sort (recursion depth optimization) */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1;                 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1;                 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.java
/* Quick sort (recursion depth optimization) */\nvoid quickSort(int[] nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.cs
/* Quick sort (recursion depth optimization) */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = Partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            QuickSort(nums, left, pivot - 1);  // Recursively sort the left subarray\n            left = pivot + 1;  // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            QuickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.go
/* Quick sort (recursion depth optimization) */\nfunc (q *quickSortTailCall) quickSort(nums []int, left, right int) {\n    // Terminate when subarray length is 1\n    for left < right {\n        // Sentinel partition operation\n        pivot := q.partition(nums, left, right)\n        // Perform quick sort on the shorter of the two subarrays\n        if pivot-left < right-pivot {\n            q.quickSort(nums, left, pivot-1) // Recursively sort the left subarray\n            left = pivot + 1                 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            q.quickSort(nums, pivot+1, right) // Recursively sort the right subarray\n            right = pivot - 1                 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.swift
/* Quick sort (recursion depth optimization) */\nfunc quickSortTailCall(nums: inout [Int], left: Int, right: Int) {\n    var left = left\n    var right = right\n    // Terminate when subarray length is 1\n    while left < right {\n        // Sentinel partition operation\n        let pivot = partition(nums: &nums, left: left, right: right)\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left) < (right - pivot) {\n            quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // Recursively sort the left subarray\n            left = pivot + 1 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // Recursively sort the right subarray\n            right = pivot - 1 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.js
/* Quick sort (recursion depth optimization) */\nquickSort(nums, left, right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        let pivot = this.partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.ts
/* Quick sort (recursion depth optimization) */\nquickSort(nums: number[], left: number, right: number): void {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        let pivot = this.partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.dart
/* Quick sort (recursion depth optimization) */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Terminate when subarray length is 1\n  while (left < right) {\n    // Sentinel partition operation\n    int pivot = _partition(nums, left, right);\n    // Perform quick sort on the shorter of the two subarrays\n    if (pivot - left < right - pivot) {\n      quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n      left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n    } else {\n      quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n      right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n    }\n  }\n}\n
quick_sort.rs
/* Quick sort (recursion depth optimization) */\npub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {\n    // Terminate when subarray length is 1\n    while left < right {\n        // Sentinel partition operation\n        let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n        // Perform quick sort on the shorter of the two subarrays\n        if pivot - left < right - pivot {\n            Self::quick_sort(left, pivot - 1, nums); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            Self::quick_sort(pivot + 1, right, nums); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.c
/* Quick sort (recursion depth optimization) */\nvoid quickSortTailCall(int nums[], int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            // Recursively sort the left subarray\n            quickSortTailCall(nums, left, pivot - 1);\n            // Remaining unsorted interval is [pivot + 1, right]\n            left = pivot + 1;\n        } else {\n            // Recursively sort the right subarray\n            quickSortTailCall(nums, pivot + 1, right);\n            // Remaining unsorted interval is [left, pivot - 1]\n            right = pivot - 1;\n        }\n    }\n}\n
quick_sort.kt
/* Quick sort (recursion depth optimization) */\nfun quickSortTailCall(nums: IntArray, left: Int, right: Int) {\n    // Terminate when subarray length is 1\n    var l = left\n    var r = right\n    while (l < r) {\n        // Sentinel partition operation\n        val pivot = partition(nums, l, r)\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - l < r - pivot) {\n            quickSort(nums, l, pivot - 1) // Recursively sort the left subarray\n            l = pivot + 1 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, r) // Recursively sort the right subarray\n            r = pivot - 1 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.rb
### Quick sort (recursion depth optimization) ###\ndef quick_sort(nums, left, right)\n  # Recurse when subarray length is not 1\n  while left < right\n    # Sentinel partition\n    pivot = partition(nums, left, right)\n    # Perform quick sort on the shorter of the two subarrays\n    if pivot - left < right - pivot\n      quick_sort(nums, left, pivot - 1)\n      left = pivot + 1 # Remaining unsorted interval is [pivot + 1, right]\n    else\n      quick_sort(nums, pivot + 1, right)\n      right = pivot - 1 # Remaining unsorted interval is [left, pivot - 1]\n    end\n  end\nend\n
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/","level":1,"title":"11.10   Radix Sort","text":"

The previous section introduced counting sort, which is suitable for situations where the data volume \\(n\\) is large but the data range \\(m\\) is small. Suppose we need to sort \\(n = 10^6\\) student IDs, and the student ID is an 8-digit number, which means the data range \\(m = 10^8\\) is very large. Using counting sort would require allocating a large amount of memory space, whereas radix sort can avoid this situation.

Radix sort (radix sort) has a core idea consistent with counting sort, which also achieves sorting by counting quantities. Building on this, radix sort utilizes the progressive relationship between the digits of numbers, sorting each digit in turn to obtain the final sorting result.

","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11101-algorithm-flow","level":2,"title":"11.10.1   Algorithm Flow","text":"

Taking student ID data as an example, assume the lowest digit is the \\(1\\)st digit and the highest digit is the \\(8\\)th digit. The flow of radix sort is shown in Figure 11-18.

  1. Initialize the digit \\(k = 1\\).
  2. Perform \"counting sort\" on the \\(k\\)th digit of the student IDs. After completion, the data will be sorted from smallest to largest according to the \\(k\\)th digit.
  3. Increase \\(k\\) by \\(1\\), then return to step 2. and continue iterating until all digits are sorted, at which point the process ends.

Figure 11-18   Radix sort algorithm flow

Below we analyze the code implementation. For a \\(d\\)-base number \\(x\\), to get its \\(k\\)th digit \\(x_k\\), the following calculation formula can be used:

\\[ x_k = \\lfloor\\frac{x}{d^{k-1}}\\rfloor \\bmod d \\]

Where \\(\\lfloor a \\rfloor\\) denotes rounding down the floating-point number \\(a\\), and \\(\\bmod \\: d\\) denotes taking the modulo (remainder) with respect to \\(d\\). For student ID data, \\(d = 10\\) and \\(k \\in [1, 8]\\).

Additionally, we need to slightly modify the counting sort code to make it sort based on the \\(k\\)th digit of the number:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby radix_sort.py
def digit(num: int, exp: int) -> int:\n    \"\"\"Get the k-th digit of element num, where exp = 10^(k-1)\"\"\"\n    # Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"Counting sort (based on nums k-th digit)\"\"\"\n    # Decimal digit range is 0~9, therefore need a bucket array of length 10\n    counter = [0] * 10\n    n = len(nums)\n    # Count the occurrence of digits 0~9\n    for i in range(n):\n        d = digit(nums[i], exp)  # Get the k-th digit of nums[i], noted as d\n        counter[d] += 1  # Count the occurrence of digit d\n    # Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in range(1, 10):\n        counter[i] += counter[i - 1]\n    # Traverse in reverse, based on bucket statistics, place each element into res\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        d = digit(nums[i], exp)\n        j = counter[d] - 1  # Get the index j for d in the array\n        res[j] = nums[i]  # Place the current element at index j\n        counter[d] -= 1  # Decrease the count of d by 1\n    # Use result to overwrite the original array nums\n    for i in range(n):\n        nums[i] = res[i]\n\ndef radix_sort(nums: list[int]):\n    \"\"\"Radix sort\"\"\"\n    # Get the maximum element of the array, used to determine the maximum number of digits\n    m = max(nums)\n    # Traverse from the lowest to the highest digit\n    exp = 1\n    while exp <= m:\n        # Perform counting sort on the k-th digit of array elements\n        # k = 1 -> exp = 1\n        # k = 2 -> exp = 10\n        # i.e., exp = 10^(k-1)\n        counting_sort_digit(nums, exp)\n        exp *= 10\n
radix_sort.cpp
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    vector<int> counter(10, 0);\n    int n = nums.size();\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    vector<int> res(n, 0);\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(vector<int> &nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = *max_element(nums.begin(), nums.end());\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10)\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n}\n
radix_sort.java
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(int[] nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int[] counter = new int[10];\n    int n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(int[] nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = Integer.MIN_VALUE;\n    for (int num : nums)\n        if (num > m)\n            m = num;\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.cs
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nint Digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid CountingSortDigit(int[] nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int[] counter = new int[10];\n    int n = nums.Length;\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = Digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = Digit(nums[i], exp);\n        int j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nvoid RadixSort(int[] nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = int.MinValue;\n    foreach (int num in nums) {\n        if (num > m) m = num;\n    }\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        CountingSortDigit(nums, exp);\n    }\n}\n
radix_sort.go
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunc digit(num, exp int) int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunc countingSortDigit(nums []int, exp int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    counter := make([]int, 10)\n    n := len(nums)\n    // Count the occurrence of digits 0~9\n    for i := 0; i < n; i++ {\n        d := digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d\n        counter[d]++             // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i := 1; i < 10; i++ {\n        counter[i] += counter[i-1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        d := digit(nums[i], exp)\n        j := counter[d] - 1 // Get the index j for d in the array\n        res[j] = nums[i]    // Place the current element at index j\n        counter[d]--        // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for i := 0; i < n; i++ {\n        nums[i] = res[i]\n    }\n}\n\n/* Radix sort */\nfunc radixSort(nums []int) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    max := math.MinInt\n    for _, num := range nums {\n        if num > max {\n            max = num\n        }\n    }\n    // Traverse from the lowest to the highest digit\n    for exp := 1; max >= exp; exp *= 10 {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n    }\n}\n
radix_sort.swift
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    var counter = Array(repeating: 0, count: 10)\n    // Count the occurrence of digits 0~9\n    for i in nums.indices {\n        let d = digit(num: nums[i], exp: exp) // Get the k-th digit of nums[i], noted as d\n        counter[d] += 1 // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in 1 ..< 10 {\n        counter[i] += counter[i - 1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let d = digit(num: nums[i], exp: exp)\n        let j = counter[d] - 1 // Get the index j for d in the array\n        res[j] = nums[i] // Place the current element at index j\n        counter[d] -= 1 // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n\n/* Radix sort */\nfunc radixSort(nums: inout [Int]) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    var m = Int.min\n    for num in nums {\n        if num > m {\n            m = num\n        }\n    }\n    // Traverse from the lowest to the highest digit\n    for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums: &nums, exp: exp)\n    }\n}\n
radix_sort.js
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunction digit(num, exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return Math.floor(num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunction countingSortDigit(nums, exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d]--; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nfunction radixSort(nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m = Math.max(... nums);\n    // Traverse from the lowest to the highest digit\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.ts
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunction digit(num: number, exp: number): number {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return Math.floor(num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d]--; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nfunction radixSort(nums: number[]): void {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m: number = Math.max(... nums);\n    // Traverse from the lowest to the highest digit\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.dart
/* Get k-th digit of element _num, where exp = 10^(k-1) */\nint digit(int _num, int exp) {\n  // Passing exp instead of k can avoid repeated expensive exponentiation here\n  return (_num ~/ exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(List<int> nums, int exp) {\n  // Decimal digit range is 0~9, therefore need a bucket array of length 10\n  List<int> counter = List<int>.filled(10, 0);\n  int n = nums.length;\n  // Count the occurrence of digits 0~9\n  for (int i = 0; i < n; i++) {\n    int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n    counter[d]++; // Count the occurrence of digit d\n  }\n  // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n  for (int i = 1; i < 10; i++) {\n    counter[i] += counter[i - 1];\n  }\n  // Traverse in reverse, based on bucket statistics, place each element into res\n  List<int> res = List<int>.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int d = digit(nums[i], exp);\n    int j = counter[d] - 1; // Get the index j for d in the array\n    res[j] = nums[i]; // Place the current element at index j\n    counter[d]--; // Decrease the count of d by 1\n  }\n  // Use result to overwrite the original array nums\n  for (int i = 0; i < n; i++) nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(List<int> nums) {\n  // Get the maximum element of the array, used to determine the maximum number of digits\n  // In Dart, int length is 64 bits\n  int m = -1 << 63;\n  for (int _num in nums) if (_num > m) m = _num;\n  // Traverse from the lowest to the highest digit\n  for (int exp = 1; exp <= m; exp *= 10)\n    // Perform counting sort on the k-th digit of array elements\n    // k = 1 -> exp = 1\n    // k = 2 -> exp = 10\n    // i.e., exp = 10^(k-1)\n    countingSortDigit(nums, exp);\n}\n
radix_sort.rs
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfn digit(num: i32, exp: i32) -> usize {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return ((num / exp) % 10) as usize;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfn counting_sort_digit(nums: &mut [i32], exp: i32) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    let mut counter = [0; 10];\n    let n = nums.len();\n    // Count the occurrence of digits 0~9\n    for i in 0..n {\n        let d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d] += 1; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in 1..10 {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let d = digit(nums[i], exp);\n        let j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d] -= 1; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    nums.copy_from_slice(&res);\n}\n\n/* Radix sort */\nfn radix_sort(nums: &mut [i32]) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m = *nums.into_iter().max().unwrap();\n    // Traverse from the lowest to the highest digit\n    let mut exp = 1;\n    while exp <= m {\n        counting_sort_digit(nums, exp);\n        exp *= 10;\n    }\n}\n
radix_sort.c
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int *counter = (int *)malloc((sizeof(int) * 10));\n    memset(counter, 0, sizeof(int) * 10); // Initialize to 0 to support subsequent memory release\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < size; i++) {\n        // Get the k-th digit of nums[i], noted as d\n        int d = digit(nums[i], exp);\n        // Count the occurrence of digit d\n        counter[d]++;\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    int *res = (int *)malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < size; i++) {\n        nums[i] = res[i];\n    }\n    // Free memory\n    free(res);\n    free(counter);\n}\n\n/* Radix sort */\nvoid radixSort(int nums[], int size) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int max = INT32_MIN;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > max) {\n            max = nums[i];\n        }\n    }\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; max >= exp; exp *= 10)\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, size, exp);\n}\n
radix_sort.kt
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfun digit(num: Int, exp: Int): Int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    val counter = IntArray(10)\n    val n = nums.size\n    // Count the occurrence of digits 0~9\n    for (i in 0..<n) {\n        val d = digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d\n        counter[d]++                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (i in 1..9) {\n        counter[i] += counter[i - 1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val d = digit(nums[i], exp)\n        val j = counter[d] - 1 // Get the index j for d in the array\n        res[j] = nums[i]       // Place the current element at index j\n        counter[d]--           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (i in 0..<n)\n        nums[i] = res[i]\n}\n\n/* Radix sort */\nfun radixSort(nums: IntArray) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    var m = Int.MIN_VALUE\n    for (num in nums) if (num > m) m = num\n    var exp = 1\n    // Traverse from the lowest to the highest digit\n    while (exp <= m) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n        exp *= 10\n    }\n}\n
radix_sort.rb
### Get k-th digit of element num, where exp = 10^(k-1) ###\ndef digit(num, exp)\n  # Passing exp instead of k avoids expensive exponentiation calculations\n  (num / exp) % 10\nend\n\n### Counting sort (sort by k-th digit of nums) ###\ndef counting_sort_digit(nums, exp)\n  # Decimal digit range is 0~9, therefore need a bucket array of length 10\n  counter = Array.new(10, 0)\n  n = nums.length\n  # Count the occurrence of digits 0~9\n  for i in 0...n\n    d = digit(nums[i], exp) # Get the k-th digit of nums[i], noted as d\n    counter[d] += 1 # Count the occurrence of digit d\n  end\n  # Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n  (1...10).each { |i| counter[i] += counter[i - 1] }\n  # Traverse in reverse, based on bucket statistics, place each element into res\n  res = Array.new(n, 0)\n  for i in (n - 1).downto(0)\n    d = digit(nums[i], exp)\n    j = counter[d] - 1 # Get the index j for d in the array\n    res[j] = nums[i] # Place the current element at index j\n    counter[d] -= 1 # Decrease the count of d by 1\n  end\n  # Use result to overwrite the original array nums\n  (0...n).each { |i| nums[i] = res[i] }\nend\n\n### Radix sort ###\ndef radix_sort(nums)\n  # Get the maximum element of the array, used to determine the maximum number of digits\n  m = nums.max\n  # Traverse from the lowest to the highest digit\n  exp = 1\n  while exp <= m\n    # Perform counting sort on the k-th digit of array elements\n    # k = 1 -> exp = 1\n    # k = 2 -> exp = 10\n    # i.e., exp = 10^(k-1)\n    counting_sort_digit(nums, exp)\n    exp *= 10\n  end\nend\n

Why start sorting from the lowest digit?

In successive sorting rounds, the result of a later round will override the result of an earlier round. For example, if the first round result is \\(a < b\\), while the second round result is \\(a > b\\), then the second round's result will replace the first round's result. Since higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then sort the higher digits.

","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11102-algorithm-characteristics","level":2,"title":"11.10.2   Algorithm Characteristics","text":"

Compared to counting sort, radix sort is suitable for larger numerical ranges, but the prerequisite is that the data must be representable in a fixed number of digits, and the number of digits should not be too large. For example, floating-point numbers are not suitable for radix sort because their number of digits \\(k\\) may be too large, potentially leading to time complexity \\(O(nk) \\gg O(n^2)\\).

  • Time complexity of \\(O(nk)\\), non-adaptive sorting: Let the data volume be \\(n\\), the data be in base \\(d\\), and the maximum number of digits be \\(k\\). Then performing counting sort on a certain digit uses \\(O(n + d)\\) time, and sorting all \\(k\\) digits uses \\(O((n + d)k)\\) time. Typically, both \\(d\\) and \\(k\\) are relatively small, and the time complexity approaches \\(O(n)\\).
  • Space complexity of \\(O(n + d)\\), non-in-place sorting: Same as counting sort, radix sort requires auxiliary arrays res and counter of lengths \\(n\\) and \\(d\\).
  • Stable sorting: When counting sort is stable, radix sort is also stable; when counting sort is unstable, radix sort cannot guarantee obtaining correct sorting results.
","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   Selection Sort","text":"

Selection sort (selection sort) works very simply: it opens a loop, and in each round, selects the smallest element from the unsorted interval and places it at the end of the sorted interval.

Assume the array has length \\(n\\). The algorithm flow of selection sort is shown in Figure 11-2.

  1. Initially, all elements are unsorted, i.e., the unsorted (index) interval is \\([0, n-1]\\).
  2. Select the smallest element in the interval \\([0, n-1]\\) and swap it with the element at index \\(0\\). After completion, the first element of the array is sorted.
  3. Select the smallest element in the interval \\([1, n-1]\\) and swap it with the element at index \\(1\\). After completion, the first 2 elements of the array are sorted.
  4. And so on. After \\(n - 1\\) rounds of selection and swapping, the first \\(n - 1\\) elements of the array are sorted.
  5. The only remaining element must be the largest element, requiring no sorting, so the array sorting is complete.
<1><2><3><4><5><6><7><8><9><10><11>

Figure 11-2   Selection sort steps

In the code, we use \\(k\\) to record the smallest element within the unsorted interval:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby selection_sort.py
def selection_sort(nums: list[int]):\n    \"\"\"Selection sort\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [i, n-1]\n    for i in range(n - 1):\n        # Inner loop: find the smallest element within the unsorted interval\n        k = i\n        for j in range(i + 1, n):\n            if nums[j] < nums[k]:\n                k = j  # Record the index of the smallest element\n        # Swap the smallest element with the first element of the unsorted interval\n        nums[i], nums[k] = nums[k], nums[i]\n
selection_sort.cpp
/* Selection sort */\nvoid selectionSort(vector<int> &nums) {\n    int n = nums.size();\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        swap(nums[i], nums[k]);\n    }\n}\n
selection_sort.java
/* Selection sort */\nvoid selectionSort(int[] nums) {\n    int n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
selection_sort.cs
/* Selection sort */\nvoid SelectionSort(int[] nums) {\n    int n = nums.Length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        (nums[k], nums[i]) = (nums[i], nums[k]);\n    }\n}\n
selection_sort.go
/* Selection sort */\nfunc selectionSort(nums []int) {\n    n := len(nums)\n    // Outer loop: unsorted interval is [i, n-1]\n    for i := 0; i < n-1; i++ {\n        // Inner loop: find the smallest element within the unsorted interval\n        k := i\n        for j := i + 1; j < n; j++ {\n            if nums[j] < nums[k] {\n                // Record the index of the smallest element\n                k = j\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums[i], nums[k] = nums[k], nums[i]\n\n    }\n}\n
selection_sort.swift
/* Selection sort */\nfunc selectionSort(nums: inout [Int]) {\n    // Outer loop: unsorted interval is [i, n-1]\n    for i in nums.indices.dropLast() {\n        // Inner loop: find the smallest element within the unsorted interval\n        var k = i\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[j] < nums[k] {\n                k = j // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums.swapAt(i, k)\n    }\n}\n
selection_sort.js
/* Selection sort */\nfunction selectionSort(nums) {\n    let n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
selection_sort.ts
/* Selection sort */\nfunction selectionSort(nums: number[]): void {\n    let n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
selection_sort.dart
/* Selection sort */\nvoid selectionSort(List<int> nums) {\n  int n = nums.length;\n  // Outer loop: unsorted interval is [i, n-1]\n  for (int i = 0; i < n - 1; i++) {\n    // Inner loop: find the smallest element within the unsorted interval\n    int k = i;\n    for (int j = i + 1; j < n; j++) {\n      if (nums[j] < nums[k]) k = j; // Record the index of the smallest element\n    }\n    // Swap the smallest element with the first element of the unsorted interval\n    int temp = nums[i];\n    nums[i] = nums[k];\n    nums[k] = temp;\n  }\n}\n
selection_sort.rs
/* Selection sort */\nfn selection_sort(nums: &mut [i32]) {\n    if nums.is_empty() {\n        return;\n    }\n    let n = nums.len();\n    // Outer loop: unsorted interval is [i, n-1]\n    for i in 0..n - 1 {\n        // Inner loop: find the smallest element within the unsorted interval\n        let mut k = i;\n        for j in i + 1..n {\n            if nums[j] < nums[k] {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums.swap(i, k);\n    }\n}\n
selection_sort.c
/* Selection sort */\nvoid selectionSort(int nums[], int n) {\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
selection_sort.kt
/* Selection sort */\nfun selectionSort(nums: IntArray) {\n    val n = nums.size\n    // Outer loop: unsorted interval is [i, n-1]\n    for (i in 0..<n - 1) {\n        var k = i\n        // Inner loop: find the smallest element within the unsorted interval\n        for (j in i + 1..<n) {\n            if (nums[j] < nums[k])\n                k = j // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        val temp = nums[i]\n        nums[i] = nums[k]\n        nums[k] = temp\n    }\n}\n
selection_sort.rb
### Selection sort ###\ndef selection_sort(nums)\n  n = nums.length\n  # Outer loop: unsorted interval is [i, n-1]\n  for i in 0...(n - 1)\n    # Inner loop: find the smallest element within the unsorted interval\n    k = i\n    for j in (i + 1)...n\n      if nums[j] < nums[k]\n        k = j # Record the index of the smallest element\n      end\n    end\n    # Swap the smallest element with the first element of the unsorted interval\n    nums[i], nums[k] = nums[k], nums[i]\n  end\nend\n
","path":["Chapter 11. Sorting","11.2   Selection Sort"],"tags":[]},{"location":"chapter_sorting/selection_sort/#1121-algorithm-characteristics","level":2,"title":"11.2.1   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n^2)\\), non-adaptive sorting: The outer loop has \\(n - 1\\) rounds in total. The length of the unsorted interval in the first round is \\(n\\), and the length of the unsorted interval in the last round is \\(2\\). That is, each round of the outer loop contains \\(n\\), \\(n - 1\\), \\(\\dots\\), \\(3\\), \\(2\\) inner loop iterations, summing to \\(\\frac{(n - 1)(n + 2)}{2}\\).
  • Space complexity of \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
  • Non-stable sorting: As shown in Figure 11-3, element nums[i] may be swapped to the right of an element equal to it, causing a change in their relative order.

Figure 11-3   Selection sort non-stability example

","path":["Chapter 11. Sorting","11.2   Selection Sort"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   Sorting Algorithm","text":"

Sorting algorithm (sorting algorithm) is used to arrange a group of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently.

As shown in Figure 11-1, data types in sorting algorithms can be integers, floating-point numbers, characters, or strings, etc. The sorting criterion can be set according to requirements, such as numerical size, character ASCII code order, or custom rules.

Figure 11-1   Data type and criterion examples

","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111-evaluation-dimensions","level":2,"title":"11.1.1   Evaluation Dimensions","text":"

Execution efficiency: We expect the time complexity of sorting algorithms to be as low as possible, with a smaller total number of operations (reducing the constant factor in time complexity). For large data volumes, execution efficiency is particularly important.

In-place property: As the name implies, in-place sorting achieves sorting by operating directly on the original array without requiring additional auxiliary arrays, thus saving memory. Typically, in-place sorting involves fewer data movement operations and runs faster.

Stability: Stable sorting ensures that the relative order of equal elements in the array does not change after sorting is completed.

Stable sorting is a necessary condition for multi-level sorting scenarios. Suppose we have a table storing student information, where column 1 and column 2 are name and age, respectively. In this case, unstable sorting may cause the ordered nature of the input data to be lost:

# Input Data Is Sorted by Name\n# (name, age)\n  ('A', 19)\n  ('B', 18)\n  ('C', 21)\n  ('D', 19)\n  ('E', 23)\n\n# Assuming We Use an Unstable Sorting Algorithm to Sort the List by Age,\n# In the Result, the Relative Positions of ('D', 19) and ('A', 19) Are Changed,\n# And the Property That the Input Data Is Sorted by Name Is Lost\n  ('B', 18)\n  ('D', 19)\n  ('A', 19)\n  ('C', 21)\n  ('E', 23)\n

Adaptability: Adaptive sorting can utilize the existing order information in the input data to reduce the amount of computation, achieving better time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than the average time complexity.

Comparison-based or not: Comparison-based sorting relies on comparison operators (\\(<\\), \\(=\\), \\(>\\)) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of \\(O(n \\log n)\\). Non-comparison sorting does not use comparison operators and can achieve a time complexity of \\(O(n)\\), but its versatility is relatively limited.

","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1112-ideal-sorting-algorithm","level":2,"title":"11.1.2   Ideal Sorting Algorithm","text":"

Fast execution, in-place, stable, adaptive, good versatility. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem.

Next, we will learn about various sorting algorithms together and analyze the advantages and disadvantages of each sorting algorithm based on the above evaluation dimensions.

","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/summary/","level":1,"title":"11.11   Summary","text":"","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_sorting/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Bubble sort achieves sorting by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to \\(O(n)\\).
  • Insertion sort completes sorting by inserting elements from the unsorted interval into the correct position in the sorted interval each round. Although the time complexity of insertion sort is \\(O(n^2)\\), it is very popular in small data volume sorting tasks because it involves relatively few unit operations.
  • Quick sort is implemented based on sentinel partitioning operations. In sentinel partitioning, it is possible to select the worst pivot every time, causing the time complexity to degrade to \\(O(n^2)\\). Introducing median pivot or random pivot can reduce the probability of such degradation. By preferentially recursing on the shorter sub-interval, the recursion depth can be effectively reduced, optimizing the space complexity to \\(O(\\log n)\\).
  • Merge sort includes two phases: divide and merge, which typically embody the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, with a space complexity of \\(O(n)\\); however, the space complexity of sorting a linked list can be optimized to \\(O(1)\\).
  • Bucket sort consists of three steps: distributing data into buckets, sorting within buckets, and merging results. It also embodies the divide-and-conquer strategy and is suitable for very large data volumes. The key to bucket sort is distributing data evenly.
  • Counting sort is a special case of bucket sort, which achieves sorting by counting the number of occurrences of data. Counting sort is suitable for situations where the data volume is large but the data range is limited, and requires that data can be converted to positive integers.
  • Radix sort achieves data sorting by sorting digit by digit, requiring that data can be represented as fixed-digit numbers.
  • Overall, we hope to find a sorting algorithm that is efficient, stable, in-place, and adaptive, with good versatility. However, just like other data structures and algorithms, no sorting algorithm has been found so far that simultaneously possesses all these characteristics. In practical applications, we need to select the appropriate sorting algorithm based on the specific characteristics of the data.
  • Figure 11-19 compares mainstream sorting algorithms in terms of efficiency, stability, in-place property, and adaptability.

Figure 11-19   Sorting algorithm comparison

","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_sorting/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: In what situations is the stability of sorting algorithms necessary?

In reality, we may sort based on a certain attribute of objects. For example, students have two attributes: name and height. We want to implement multi-level sorting: first sort by name to get (A, 180) (B, 185) (C, 170) (D, 170); then sort by height. Because the sorting algorithm is unstable, we may get (D, 170) (C, 170) (A, 180) (B, 185).

It can be seen that the positions of students D and C have been swapped, and the orderliness of names has been disrupted, which is something we don't want to see.

Q: Can the order of \"searching from right to left\" and \"searching from left to right\" in sentinel partitioning be swapped?

No. When we use the leftmost element as the pivot, we must first \"search from right to left\" and then \"search from left to right\". This conclusion is somewhat counterintuitive; let's analyze the reason.

The last step of sentinel partitioning partition() is to swap nums[left] and nums[i]. After the swap is complete, the elements to the left of the pivot are all <= the pivot, which requires that nums[left] >= nums[i] must hold before the last swap. Suppose we first \"search from left to right\", then if we cannot find an element larger than the pivot, we will exit the loop when i == j, at which point it may be that nums[j] == nums[i] > nums[left]. In other words, the last swap operation will swap an element larger than the pivot to the leftmost end of the array, causing sentinel partitioning to fail.

For example, given the array [0, 0, 0, 0, 1], if we first \"search from left to right\", the array after sentinel partitioning is [1, 0, 0, 0, 0], which is incorrect.

Thinking deeper, if we select nums[right] as the pivot, then it's exactly the opposite - we must first \"search from left to right\".

Q: Regarding the optimization of recursion depth in quick sort, why can selecting the shorter array ensure that the recursion depth does not exceed \\(\\log n\\)?

The recursion depth is the number of currently unreturned recursive methods. Each round of sentinel partitioning divides the original array into two sub-arrays. After recursion depth optimization, the length of the sub-array to be recursively processed is at most half of the original array length. Assuming the worst case is always half the length, the final recursion depth will be \\(\\log n\\).

Reviewing the original quick sort, we may continuously recurse on the longer array. In the worst case, it would be \\(n\\), \\(n - 1\\), \\(\\dots\\), \\(2\\), \\(1\\), with a recursion depth of \\(n\\). Recursion depth optimization can avoid this situation.

Q: When all elements in the array are equal, is the time complexity of quick sort \\(O(n^2)\\)? How should this degenerate case be handled?

Yes. For this situation, consider partitioning the array into three parts through sentinel partitioning: less than, equal to, and greater than the pivot. Only recursively process the less than and greater than parts. Under this method, an array where all input elements are equal can complete sorting in just one round of sentinel partitioning.

Q: Why is the worst-case time complexity of bucket sort \\(O(n^2)\\)?

In the worst case, all elements are distributed into the same bucket. If we use an \\(O(n^2)\\) algorithm to sort these elements, the time complexity will be \\(O(n^2)\\).

","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/","level":1,"title":"Chapter 5.   Stack and Queue","text":"

Abstract

Stacks are like stacking cats, while queues are like cats lining up.

They represent LIFO (Last In First Out) and FIFO (First In First Out) logic, respectively.

","path":["Chapter 5. Stack and Queue","Chapter 5.   Stack and Queue"],"tags":[]},{"location":"chapter_stack_and_queue/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 5.1   Stack
  • 5.2   Queue
  • 5.3   Double-Ended Queue
  • 5.4   Summary
","path":["Chapter 5. Stack and Queue","Chapter 5.   Stack and Queue"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   Deque","text":"

In a queue, we can only remove elements from the front or add elements at the rear. As shown in Figure 5-7, a double-ended queue (deque) provides greater flexibility, allowing the addition or removal of elements at both the front and rear.

Figure 5-7   Operations of deque

","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#531-common-deque-operations","level":2,"title":"5.3.1   Common Deque Operations","text":"

The common operations on a deque are shown in Table 5-3. The specific method names depend on the programming language used.

Table 5-3   Efficiency of Deque Operations

Method Description Time Complexity push_first() Add element to front \\(O(1)\\) push_last() Add element to rear \\(O(1)\\) pop_first() Remove front element \\(O(1)\\) pop_last() Remove rear element \\(O(1)\\) peek_first() Access front element \\(O(1)\\) peek_last() Access rear element \\(O(1)\\)

Similarly, we can directly use the deque classes already implemented in programming languages:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby deque.py
from collections import deque\n\n# Initialize deque\ndeq: deque[int] = deque()\n\n# Enqueue elements\ndeq.append(2)      # Add to rear\ndeq.append(5)\ndeq.append(4)\ndeq.appendleft(3)  # Add to front\ndeq.appendleft(1)\n\n# Access elements\nfront: int = deq[0]  # Front element\nrear: int = deq[-1]  # Rear element\n\n# Dequeue elements\npop_front: int = deq.popleft()  # Front element dequeue\npop_rear: int = deq.pop()       # Rear element dequeue\n\n# Get deque length\nsize: int = len(deq)\n\n# Check if deque is empty\nis_empty: bool = len(deq) == 0\n
deque.cpp
/* Initialize deque */\ndeque<int> deque;\n\n/* Enqueue elements */\ndeque.push_back(2);   // Add to rear\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3);  // Add to front\ndeque.push_front(1);\n\n/* Access elements */\nint front = deque.front(); // Front element\nint back = deque.back();   // Rear element\n\n/* Dequeue elements */\ndeque.pop_front();  // Front element dequeue\ndeque.pop_back();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.size();\n\n/* Check if deque is empty */\nbool empty = deque.empty();\n
deque.java
/* Initialize deque */\nDeque<Integer> deque = new LinkedList<>();\n\n/* Enqueue elements */\ndeque.offerLast(2);   // Add to rear\ndeque.offerLast(5);\ndeque.offerLast(4);\ndeque.offerFirst(3);  // Add to front\ndeque.offerFirst(1);\n\n/* Access elements */\nint peekFirst = deque.peekFirst();  // Front element\nint peekLast = deque.peekLast();    // Rear element\n\n/* Dequeue elements */\nint popFirst = deque.pollFirst();  // Front element dequeue\nint popLast = deque.pollLast();    // Rear element dequeue\n\n/* Get deque length */\nint size = deque.size();\n\n/* Check if deque is empty */\nboolean isEmpty = deque.isEmpty();\n
deque.cs
/* Initialize deque */\n// In C#, use LinkedList as a deque\nLinkedList<int> deque = new();\n\n/* Enqueue elements */\ndeque.AddLast(2);   // Add to rear\ndeque.AddLast(5);\ndeque.AddLast(4);\ndeque.AddFirst(3);  // Add to front\ndeque.AddFirst(1);\n\n/* Access elements */\nint peekFirst = deque.First.Value;  // Front element\nint peekLast = deque.Last.Value;    // Rear element\n\n/* Dequeue elements */\ndeque.RemoveFirst();  // Front element dequeue\ndeque.RemoveLast();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.Count;\n\n/* Check if deque is empty */\nbool isEmpty = deque.Count == 0;\n
deque_test.go
/* Initialize deque */\n// In Go, use list as a deque\ndeque := list.New()\n\n/* Enqueue elements */\ndeque.PushBack(2)      // Add to rear\ndeque.PushBack(5)\ndeque.PushBack(4)\ndeque.PushFront(3)     // Add to front\ndeque.PushFront(1)\n\n/* Access elements */\nfront := deque.Front() // Front element\nrear := deque.Back()   // Rear element\n\n/* Dequeue elements */\ndeque.Remove(front)    // Front element dequeue\ndeque.Remove(rear)     // Rear element dequeue\n\n/* Get deque length */\nsize := deque.Len()\n\n/* Check if deque is empty */\nisEmpty := deque.Len() == 0\n
deque.swift
/* Initialize deque */\n// Swift does not have a built-in deque class, can use Array as a deque\nvar deque: [Int] = []\n\n/* Enqueue elements */\ndeque.append(2) // Add to rear\ndeque.append(5)\ndeque.append(4)\ndeque.insert(3, at: 0) // Add to front\ndeque.insert(1, at: 0)\n\n/* Access elements */\nlet peekFirst = deque.first! // Front element\nlet peekLast = deque.last! // Rear element\n\n/* Dequeue elements */\n// When using Array simulation, popFirst has O(n) complexity\nlet popFirst = deque.removeFirst() // Front element dequeue\nlet popLast = deque.removeLast() // Rear element dequeue\n\n/* Get deque length */\nlet size = deque.count\n\n/* Check if deque is empty */\nlet isEmpty = deque.isEmpty\n
deque.js
/* Initialize deque */\n// JavaScript does not have a built-in deque, can only use Array as a deque\nconst deque = [];\n\n/* Enqueue elements */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Please note that since it's an array, unshift() has O(n) time complexity\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Access elements */\nconst peekFirst = deque[0];\nconst peekLast = deque[deque.length - 1];\n\n/* Dequeue elements */\n// Please note that since it's an array, shift() has O(n) time complexity\nconst popFront = deque.shift();\nconst popBack = deque.pop();\n\n/* Get deque length */\nconst size = deque.length;\n\n/* Check if deque is empty */\nconst isEmpty = size === 0;\n
deque.ts
/* Initialize deque */\n// TypeScript does not have a built-in deque, can only use Array as a deque\nconst deque: number[] = [];\n\n/* Enqueue elements */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Please note that since it's an array, unshift() has O(n) time complexity\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Access elements */\nconst peekFirst: number = deque[0];\nconst peekLast: number = deque[deque.length - 1];\n\n/* Dequeue elements */\n// Please note that since it's an array, shift() has O(n) time complexity\nconst popFront: number = deque.shift() as number;\nconst popBack: number = deque.pop() as number;\n\n/* Get deque length */\nconst size: number = deque.length;\n\n/* Check if deque is empty */\nconst isEmpty: boolean = size === 0;\n
deque.dart
/* Initialize deque */\n// In Dart, Queue is defined as a deque\nQueue<int> deque = Queue<int>();\n\n/* Enqueue elements */\ndeque.addLast(2);  // Add to rear\ndeque.addLast(5);\ndeque.addLast(4);\ndeque.addFirst(3); // Add to front\ndeque.addFirst(1);\n\n/* Access elements */\nint peekFirst = deque.first; // Front element\nint peekLast = deque.last;   // Rear element\n\n/* Dequeue elements */\nint popFirst = deque.removeFirst(); // Front element dequeue\nint popLast = deque.removeLast();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.length;\n\n/* Check if deque is empty */\nbool isEmpty = deque.isEmpty;\n
deque.rs
/* Initialize deque */\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Enqueue elements */\ndeque.push_back(2);  // Add to rear\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // Add to front\ndeque.push_front(1);\n\n/* Access elements */\nif let Some(front) = deque.front() { // Front element\n}\nif let Some(rear) = deque.back() {   // Rear element\n}\n\n/* Dequeue elements */\nif let Some(pop_front) = deque.pop_front() { // Front element dequeue\n}\nif let Some(pop_rear) = deque.pop_back() {   // Rear element dequeue\n}\n\n/* Get deque length */\nlet size = deque.len();\n\n/* Check if deque is empty */\nlet is_empty = deque.is_empty();\n
deque.c
// C does not provide a built-in deque\n
deque.kt
/* Initialize deque */\nval deque = LinkedList<Int>()\n\n/* Enqueue elements */\ndeque.offerLast(2)  // Add to rear\ndeque.offerLast(5)\ndeque.offerLast(4)\ndeque.offerFirst(3) // Add to front\ndeque.offerFirst(1)\n\n/* Access elements */\nval peekFirst = deque.peekFirst() // Front element\nval peekLast = deque.peekLast()   // Rear element\n\n/* Dequeue elements */\nval popFirst = deque.pollFirst() // Front element dequeue\nval popLast = deque.pollLast()   // Rear element dequeue\n\n/* Get deque length */\nval size = deque.size\n\n/* Check if deque is empty */\nval isEmpty = deque.isEmpty()\n
deque.rb
# Initialize deque\n# Ruby does not have a built-in deque, can only use Array as a deque\ndeque = []\n\n# Enqueue elements\ndeque << 2\ndeque << 5\ndeque << 4\n# Please note that since it's an array, Array#unshift has O(n) time complexity\ndeque.unshift(3)\ndeque.unshift(1)\n\n# Access elements\npeek_first = deque.first\npeek_last = deque.last\n\n# Dequeue elements\n# Please note that since it's an array, Array#shift has O(n) time complexity\npop_front = deque.shift\npop_back = deque.pop\n\n# Get deque length\nsize = deque.length\n\n# Check if deque is empty\nis_empty = size.zero?\n
Code Visualization

Full Screen >

","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#532-deque-implementation","level":2,"title":"5.3.2   Deque Implementation *","text":"

The implementation of a deque is similar to that of a queue. You can choose either a linked list or an array as the underlying data structure.

","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#1-doubly-linked-list-implementation","level":3,"title":"1.   Doubly Linked List Implementation","text":"

Reviewing the previous section, we used a regular singly linked list to implement a queue because it conveniently allows deleting the head node (corresponding to dequeue) and adding new nodes after the tail node (corresponding to enqueue).

For a deque, both the front and rear can perform enqueue and dequeue operations. In other words, a deque needs to implement operations in the opposite direction as well. For this reason, we use a \"doubly linked list\" as the underlying data structure for the deque.

As shown in Figure 5-8, we treat the head and tail nodes of the doubly linked list as the front and rear of the deque, implementing functionality to add and remove nodes at both ends.

LinkedListDequepush_last()push_first()pop_last()pop_first()

Figure 5-8   Enqueue and dequeue operations in linked list implementation of deque

The implementation code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_deque.py
class ListNode:\n    \"\"\"Doubly linked list node\"\"\"\n\n    def __init__(self, val: int):\n        \"\"\"Constructor\"\"\"\n        self.val: int = val\n        self.next: ListNode | None = None  # Successor node reference\n        self.prev: ListNode | None = None  # Predecessor node reference\n\nclass LinkedListDeque:\n    \"\"\"Double-ended queue based on doubly linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._front: ListNode | None = None  # Head node front\n        self._rear: ListNode | None = None  # Tail node rear\n        self._size: int = 0  # Length of the double-ended queue\n\n    def size(self) -> int:\n        \"\"\"Get the length of the double-ended queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the double-ended queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int, is_front: bool):\n        \"\"\"Enqueue operation\"\"\"\n        node = ListNode(num)\n        # If the linked list is empty, make both front and rear point to node\n        if self.is_empty():\n            self._front = self._rear = node\n        # Front of the queue enqueue operation\n        elif is_front:\n            # Add node to the head of the linked list\n            self._front.prev = node\n            node.next = self._front\n            self._front = node  # Update head node\n        # Rear of the queue enqueue operation\n        else:\n            # Add node to the tail of the linked list\n            self._rear.next = node\n            node.prev = self._rear\n            self._rear = node  # Update tail node\n        self._size += 1  # Update queue length\n\n    def push_first(self, num: int):\n        \"\"\"Front of the queue enqueue\"\"\"\n        self.push(num, True)\n\n    def push_last(self, num: int):\n        \"\"\"Rear of the queue enqueue\"\"\"\n        self.push(num, False)\n\n    def pop(self, is_front: bool) -> int:\n        \"\"\"Dequeue operation\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        # Front of the queue dequeue operation\n        if is_front:\n            val: int = self._front.val  # Temporarily store head node value\n            # Delete head node\n            fnext: ListNode | None = self._front.next\n            if fnext is not None:\n                fnext.prev = None\n                self._front.next = None\n            self._front = fnext  # Update head node\n        # Rear of the queue dequeue operation\n        else:\n            val: int = self._rear.val  # Temporarily store tail node value\n            # Delete tail node\n            rprev: ListNode | None = self._rear.prev\n            if rprev is not None:\n                rprev.next = None\n                self._rear.prev = None\n            self._rear = rprev  # Update tail node\n        self._size -= 1  # Update queue length\n        return val\n\n    def pop_first(self) -> int:\n        \"\"\"Front of the queue dequeue\"\"\"\n        return self.pop(True)\n\n    def pop_last(self) -> int:\n        \"\"\"Rear of the queue dequeue\"\"\"\n        return self.pop(False)\n\n    def peek_first(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._front.val\n\n    def peek_last(self) -> int:\n        \"\"\"Access rear of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._rear.val\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return array for printing\"\"\"\n        node = self._front\n        res = [0] * self.size()\n        for i in range(self.size()):\n            res[i] = node.val\n            node = node.next\n        return res\n
linkedlist_deque.cpp
/* Doubly linked list node */\nstruct DoublyListNode {\n    int val;              // Node value\n    DoublyListNode *next; // Successor node pointer\n    DoublyListNode *prev; // Predecessor node pointer\n    DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {\n    }\n};\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n  private:\n    DoublyListNode *front, *rear; // Head node front, tail node rear\n    int queSize = 0;              // Length of the double-ended queue\n\n  public:\n    /* Constructor */\n    LinkedListDeque() : front(nullptr), rear(nullptr) {\n    }\n\n    /* Destructor */\n    ~LinkedListDeque() {\n        // Traverse linked list to delete nodes and free memory\n        DoublyListNode *pre, *cur = front;\n        while (cur != nullptr) {\n            pre = cur;\n            cur = cur->next;\n            delete pre;\n        }\n    }\n\n    /* Get the length of the double-ended queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue operation */\n    void push(int num, bool isFront) {\n        DoublyListNode *node = new DoublyListNode(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty())\n            front = rear = node;\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front->prev = node;\n            node->next = front;\n            front = node; // Update head node\n        // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear->next = node;\n            node->prev = rear;\n            rear = node; // Update tail node\n        }\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Dequeue operation */\n    int pop(bool isFront) {\n        if (isEmpty())\n            throw out_of_range(\"Queue is empty\");\n        int val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front->val; // Delete head node\n            // Delete head node\n            DoublyListNode *fNext = front->next;\n            if (fNext != nullptr) {\n                fNext->prev = nullptr;\n                front->next = nullptr;\n            }\n            delete front;\n            front = fNext; // Update head node\n        // Temporarily store tail node value\n        } else {\n            val = rear->val; // Delete tail node\n            // Update tail node\n            DoublyListNode *rPrev = rear->prev;\n            if (rPrev != nullptr) {\n                rPrev->next = nullptr;\n                rear->prev = nullptr;\n            }\n            delete rear;\n            rear = rPrev; // Update tail node\n        }\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    int popFirst() {\n        return pop(true);\n    }\n\n    /* Access rear of the queue element */\n    int popLast() {\n        return pop(false);\n    }\n\n    /* Return list for printing */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return front->val;\n    }\n\n    /* Driver Code */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return rear->val;\n    }\n\n    /* Return array for printing */\n    vector<int> toVector() {\n        DoublyListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_deque.java
/* Doubly linked list node */\nclass ListNode {\n    int val; // Node value\n    ListNode next; // Successor node reference\n    ListNode prev; // Predecessor node reference\n\n    ListNode(int val) {\n        this.val = val;\n        prev = next = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private ListNode front, rear; // Head node front, tail node rear\n    private int queSize = 0; // Length of the double-ended queue\n\n    public LinkedListDeque() {\n        front = rear = null;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue operation */\n    private void push(int num, boolean isFront) {\n        ListNode node = new ListNode(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty())\n            front = rear = node;\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front.prev = node;\n            node.next = front;\n            front = node; // Update head node\n        // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear.next = node;\n            node.prev = rear;\n            rear = node; // Update tail node\n        }\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    public void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    public void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Dequeue operation */\n    private int pop(boolean isFront) {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        int val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front.val; // Delete head node\n            // Delete head node\n            ListNode fNext = front.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front.next = null;\n            }\n            front = fNext; // Update head node\n        // Temporarily store tail node value\n        } else {\n            val = rear.val; // Delete tail node\n            // Update tail node\n            ListNode rPrev = rear.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear.prev = null;\n            }\n            rear = rPrev; // Update tail node\n        }\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    public int popFirst() {\n        return pop(true);\n    }\n\n    /* Access rear of the queue element */\n    public int popLast() {\n        return pop(false);\n    }\n\n    /* Return list for printing */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Driver Code */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return rear.val;\n    }\n\n    /* Return array for printing */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_deque.cs
/* Doubly linked list node */\nclass ListNode(int val) {\n    public int val = val;       // Node value\n    public ListNode? next = null; // Successor node reference\n    public ListNode? prev = null; // Predecessor node reference\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    ListNode? front, rear; // Head node front, tail node rear\n    int queSize = 0;      // Length of the double-ended queue\n\n    public LinkedListDeque() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Enqueue operation */\n    void Push(int num, bool isFront) {\n        ListNode node = new(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (IsEmpty()) {\n            front = node;\n            rear = node;\n        }\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front!.prev = node;\n            node.next = front;\n            front = node; // Update head node\n        }\n        // Rear of the queue enqueue operation\n        else {\n            // Add node to the tail of the linked list\n            rear!.next = node;\n            node.prev = rear;\n            rear = node;  // Update tail node\n        }\n\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    public void PushFirst(int num) {\n        Push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    public void PushLast(int num) {\n        Push(num, false);\n    }\n\n    /* Dequeue operation */\n    int? Pop(bool isFront) {\n        if (IsEmpty())\n            throw new Exception();\n        int? val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front?.val; // Delete head node\n            // Delete head node\n            ListNode? fNext = front?.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front!.next = null;\n            }\n            front = fNext;   // Update head node\n        }\n        // Temporarily store tail node value\n        else {\n            val = rear?.val;  // Delete tail node\n            // Update tail node\n            ListNode? rPrev = rear?.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear!.prev = null;\n            }\n            rear = rPrev;    // Update tail node\n        }\n\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    public int? PopFirst() {\n        return Pop(true);\n    }\n\n    /* Access rear of the queue element */\n    public int? PopLast() {\n        return Pop(false);\n    }\n\n    /* Return list for printing */\n    public int? PeekFirst() {\n        if (IsEmpty())\n            throw new Exception();\n        return front?.val;\n    }\n\n    /* Driver Code */\n    public int? PeekLast() {\n        if (IsEmpty())\n            throw new Exception();\n        return rear?.val;\n    }\n\n    /* Return array for printing */\n    public int?[] ToArray() {\n        ListNode? node = front;\n        int?[] res = new int?[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node?.val;\n            node = node?.next;\n        }\n\n        return res;\n    }\n}\n
linkedlist_deque.go
/* Double-ended queue based on doubly linked list implementation */\ntype linkedListDeque struct {\n    // Use built-in package list\n    data *list.List\n}\n\n/* Initialize deque */\nfunc newLinkedListDeque() *linkedListDeque {\n    return &linkedListDeque{\n        data: list.New(),\n    }\n}\n\n/* Front element enqueue */\nfunc (s *linkedListDeque) pushFirst(value any) {\n    s.data.PushFront(value)\n}\n\n/* Rear element enqueue */\nfunc (s *linkedListDeque) pushLast(value any) {\n    s.data.PushBack(value)\n}\n\n/* Check if the double-ended queue is empty */\nfunc (s *linkedListDeque) popFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Rear element dequeue */\nfunc (s *linkedListDeque) popLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Return list for printing */\nfunc (s *linkedListDeque) peekFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* Driver Code */\nfunc (s *linkedListDeque) peekLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* Get the length of the queue */\nfunc (s *linkedListDeque) size() int {\n    return s.data.Len()\n}\n\n/* Check if the queue is empty */\nfunc (s *linkedListDeque) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListDeque) toList() *list.List {\n    return s.data\n}\n
linkedlist_deque.swift
/* Doubly linked list node */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Successor node reference\n    weak var prev: ListNode? // Predecessor node reference\n\n    init(val: Int) {\n        self.val = val\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private var front: ListNode? // Head node front\n    private var rear: ListNode? // Tail node rear\n    private var _size: Int // Length of the double-ended queue\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the double-ended queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the double-ended queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue operation */\n    private func push(num: Int, isFront: Bool) {\n        let node = ListNode(val: num)\n        // If the linked list is empty, make both front and rear point to node\n        if isEmpty() {\n            front = node\n            rear = node\n        }\n        // Front of the queue enqueue operation\n        else if isFront {\n            // Add node to the head of the linked list\n            front?.prev = node\n            node.next = front\n            front = node // Update head node\n        }\n        // Rear of the queue enqueue operation\n        else {\n            // Add node to the tail of the linked list\n            rear?.next = node\n            node.prev = rear\n            rear = node // Update tail node\n        }\n        _size += 1 // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    func pushFirst(num: Int) {\n        push(num: num, isFront: true)\n    }\n\n    /* Rear of the queue enqueue */\n    func pushLast(num: Int) {\n        push(num: num, isFront: false)\n    }\n\n    /* Dequeue operation */\n    private func pop(isFront: Bool) -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        let val: Int\n        // Temporarily store head node value\n        if isFront {\n            val = front!.val // Delete head node\n            // Delete head node\n            let fNext = front?.next\n            if fNext != nil {\n                fNext?.prev = nil\n                front?.next = nil\n            }\n            front = fNext // Update head node\n        }\n        // Temporarily store tail node value\n        else {\n            val = rear!.val // Delete tail node\n            // Update tail node\n            let rPrev = rear?.prev\n            if rPrev != nil {\n                rPrev?.next = nil\n                rear?.prev = nil\n            }\n            rear = rPrev // Update tail node\n        }\n        _size -= 1 // Update queue length\n        return val\n    }\n\n    /* Rear of the queue dequeue */\n    func popFirst() -> Int {\n        pop(isFront: true)\n    }\n\n    /* Access rear of the queue element */\n    func popLast() -> Int {\n        pop(isFront: false)\n    }\n\n    /* Return list for printing */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return front!.val\n    }\n\n    /* Driver Code */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return rear!.val\n    }\n\n    /* Return array for printing */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_deque.js
/* Doubly linked list node */\nclass ListNode {\n    prev; // Predecessor node reference (pointer)\n    next; // Successor node reference (pointer)\n    val; // Node value\n\n    constructor(val) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    #front; // Head node front\n    #rear; // Tail node rear\n    #queSize; // Length of the double-ended queue\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n        this.#queSize = 0;\n    }\n\n    /* Rear of the queue enqueue operation */\n    pushLast(val) {\n        const node = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Add node to the tail of the linked list\n            this.#rear.next = node;\n            node.prev = this.#rear;\n            this.#rear = node; // Update tail node\n        }\n        this.#queSize++;\n    }\n\n    /* Front of the queue enqueue operation */\n    pushFirst(val) {\n        const node = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Add node to the head of the linked list\n            this.#front.prev = node;\n            node.next = this.#front;\n            this.#front = node; // Update head node\n        }\n        this.#queSize++;\n    }\n\n    /* Temporarily store tail node value */\n    popLast() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#rear.val; // Store tail node value\n        // Update tail node\n        let temp = this.#rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.#rear.prev = null;\n        }\n        this.#rear = temp; // Update tail node\n        this.#queSize--;\n        return value;\n    }\n\n    /* Temporarily store head node value */\n    popFirst() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#front.val; // Store tail node value\n        // Delete head node\n        let temp = this.#front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.#front.next = null;\n        }\n        this.#front = temp; // Update head node\n        this.#queSize--;\n        return value;\n    }\n\n    /* Driver Code */\n    peekLast() {\n        return this.#queSize === 0 ? null : this.#rear.val;\n    }\n\n    /* Return list for printing */\n    peekFirst() {\n        return this.#queSize === 0 ? null : this.#front.val;\n    }\n\n    /* Get the length of the double-ended queue */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Print deque */\n    print() {\n        const arr = [];\n        let temp = this.#front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
linkedlist_deque.ts
/* Doubly linked list node */\nclass ListNode {\n    prev: ListNode; // Predecessor node reference (pointer)\n    next: ListNode; // Successor node reference (pointer)\n    val: number; // Node value\n\n    constructor(val: number) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private front: ListNode; // Head node front\n    private rear: ListNode; // Tail node rear\n    private queSize: number; // Length of the double-ended queue\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n        this.queSize = 0;\n    }\n\n    /* Rear of the queue enqueue operation */\n    pushLast(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Add node to the tail of the linked list\n            this.rear.next = node;\n            node.prev = this.rear;\n            this.rear = node; // Update tail node\n        }\n        this.queSize++;\n    }\n\n    /* Front of the queue enqueue operation */\n    pushFirst(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Add node to the head of the linked list\n            this.front.prev = node;\n            node.next = this.front;\n            this.front = node; // Update head node\n        }\n        this.queSize++;\n    }\n\n    /* Temporarily store tail node value */\n    popLast(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.rear.val; // Store tail node value\n        // Update tail node\n        let temp: ListNode = this.rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.rear.prev = null;\n        }\n        this.rear = temp; // Update tail node\n        this.queSize--;\n        return value;\n    }\n\n    /* Temporarily store head node value */\n    popFirst(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.front.val; // Store tail node value\n        // Delete head node\n        let temp: ListNode = this.front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.front.next = null;\n        }\n        this.front = temp; // Update head node\n        this.queSize--;\n        return value;\n    }\n\n    /* Driver Code */\n    peekLast(): number {\n        return this.queSize === 0 ? null : this.rear.val;\n    }\n\n    /* Return list for printing */\n    peekFirst(): number {\n        return this.queSize === 0 ? null : this.front.val;\n    }\n\n    /* Get the length of the double-ended queue */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Print deque */\n    print(): void {\n        const arr: number[] = [];\n        let temp: ListNode = this.front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
linkedlist_deque.dart
/* Doubly linked list node */\nclass ListNode {\n  int val; // Node value\n  ListNode? next; // Successor node reference\n  ListNode? prev; // Predecessor node reference\n\n  ListNode(this.val, {this.next, this.prev});\n}\n\n/* Deque implemented based on doubly linked list */\nclass LinkedListDeque {\n  late ListNode? _front; // Head node _front\n  late ListNode? _rear; // Tail node _rear\n  int _queSize = 0; // Length of the double-ended queue\n\n  LinkedListDeque() {\n    this._front = null;\n    this._rear = null;\n  }\n\n  /* Get deque length */\n  int size() {\n    return this._queSize;\n  }\n\n  /* Check if the double-ended queue is empty */\n  bool isEmpty() {\n    return size() == 0;\n  }\n\n  /* Enqueue operation */\n  void push(int _num, bool isFront) {\n    final ListNode node = ListNode(_num);\n    if (isEmpty()) {\n      // If list is empty, let both _front and _rear point to node\n      _front = _rear = node;\n    } else if (isFront) {\n      // Front of the queue enqueue operation\n      // Add node to the head of the linked list\n      _front!.prev = node;\n      node.next = _front;\n      _front = node; // Update head node\n    } else {\n      // Rear of the queue enqueue operation\n      // Add node to the tail of the linked list\n      _rear!.next = node;\n      node.prev = _rear;\n      _rear = node; // Update tail node\n    }\n    _queSize++; // Update queue length\n  }\n\n  /* Front of the queue enqueue */\n  void pushFirst(int _num) {\n    push(_num, true);\n  }\n\n  /* Rear of the queue enqueue */\n  void pushLast(int _num) {\n    push(_num, false);\n  }\n\n  /* Dequeue operation */\n  int? pop(bool isFront) {\n    // If queue is empty, return null directly\n    if (isEmpty()) {\n      return null;\n    }\n    final int val;\n    if (isFront) {\n      // Temporarily store head node value\n      val = _front!.val; // Delete head node\n      // Delete head node\n      ListNode? fNext = _front!.next;\n      if (fNext != null) {\n        fNext.prev = null;\n        _front!.next = null;\n      }\n      _front = fNext; // Update head node\n    } else {\n      // Temporarily store tail node value\n      val = _rear!.val; // Delete tail node\n      // Update tail node\n      ListNode? rPrev = _rear!.prev;\n      if (rPrev != null) {\n        rPrev.next = null;\n        _rear!.prev = null;\n      }\n      _rear = rPrev; // Update tail node\n    }\n    _queSize--; // Update queue length\n    return val;\n  }\n\n  /* Rear of the queue dequeue */\n  int? popFirst() {\n    return pop(true);\n  }\n\n  /* Access rear of the queue element */\n  int? popLast() {\n    return pop(false);\n  }\n\n  /* Return list for printing */\n  int? peekFirst() {\n    return _front?.val;\n  }\n\n  /* Driver Code */\n  int? peekLast() {\n    return _rear?.val;\n  }\n\n  /* Return array for printing */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> res = [];\n    for (int i = 0; i < _queSize; i++) {\n      res.add(node!.val);\n      node = node.next;\n    }\n    return res;\n  }\n}\n
linkedlist_deque.rs
/* Doubly linked list node */\npub struct ListNode<T> {\n    pub val: T,                                 // Node value\n    pub next: Option<Rc<RefCell<ListNode<T>>>>, // Successor node pointer\n    pub prev: Option<Rc<RefCell<ListNode<T>>>>, // Predecessor node pointer\n}\n\nimpl<T> ListNode<T> {\n    pub fn new(val: T) -> Rc<RefCell<ListNode<T>>> {\n        Rc::new(RefCell::new(ListNode {\n            val,\n            next: None,\n            prev: None,\n        }))\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListDeque<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Head node front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Tail node rear\n    que_size: usize,                         // Length of the double-ended queue\n}\n\nimpl<T: Copy> LinkedListDeque<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* Get the length of the double-ended queue */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Check if the double-ended queue is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Enqueue operation */\n    fn push(&mut self, num: T, is_front: bool) {\n        let node = ListNode::new(num);\n        // Front of the queue enqueue operation\n        if is_front {\n            match self.front.take() {\n                // If the linked list is empty, make both front and rear point to node\n                None => {\n                    self.rear = Some(node.clone());\n                    self.front = Some(node);\n                }\n                // Add node to the head of the linked list\n                Some(old_front) => {\n                    old_front.borrow_mut().prev = Some(node.clone());\n                    node.borrow_mut().next = Some(old_front);\n                    self.front = Some(node); // Update head node\n                }\n            }\n        }\n        // Rear of the queue enqueue operation\n        else {\n            match self.rear.take() {\n                // If the linked list is empty, make both front and rear point to node\n                None => {\n                    self.front = Some(node.clone());\n                    self.rear = Some(node);\n                }\n                // Add node to the tail of the linked list\n                Some(old_rear) => {\n                    old_rear.borrow_mut().next = Some(node.clone());\n                    node.borrow_mut().prev = Some(old_rear);\n                    self.rear = Some(node); // Update tail node\n                }\n            }\n        }\n        self.que_size += 1; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    pub fn push_first(&mut self, num: T) {\n        self.push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    pub fn push_last(&mut self, num: T) {\n        self.push(num, false);\n    }\n\n    /* Dequeue operation */\n    fn pop(&mut self, is_front: bool) -> Option<T> {\n        // If queue is empty, return None directly\n        if self.is_empty() {\n            return None;\n        };\n        // Temporarily store head node value\n        if is_front {\n            self.front.take().map(|old_front| {\n                match old_front.borrow_mut().next.take() {\n                    Some(new_front) => {\n                        new_front.borrow_mut().prev.take();\n                        self.front = Some(new_front); // Update head node\n                    }\n                    None => {\n                        self.rear.take();\n                    }\n                }\n                self.que_size -= 1; // Update queue length\n                old_front.borrow().val\n            })\n        }\n        // Temporarily store tail node value\n        else {\n            self.rear.take().map(|old_rear| {\n                match old_rear.borrow_mut().prev.take() {\n                    Some(new_rear) => {\n                        new_rear.borrow_mut().next.take();\n                        self.rear = Some(new_rear); // Update tail node\n                    }\n                    None => {\n                        self.front.take();\n                    }\n                }\n                self.que_size -= 1; // Update queue length\n                old_rear.borrow().val\n            })\n        }\n    }\n\n    /* Rear of the queue dequeue */\n    pub fn pop_first(&mut self) -> Option<T> {\n        return self.pop(true);\n    }\n\n    /* Access rear of the queue element */\n    pub fn pop_last(&mut self) -> Option<T> {\n        return self.pop(false);\n    }\n\n    /* Return list for printing */\n    pub fn peek_first(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Driver Code */\n    pub fn peek_last(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.rear.as_ref()\n    }\n\n    /* Return array for printing */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n        res\n    }\n}\n
linkedlist_deque.c
/* Doubly linked list node */\ntypedef struct DoublyListNode {\n    int val;                     // Node value\n    struct DoublyListNode *next; // Successor node\n    struct DoublyListNode *prev; // Predecessor node\n} DoublyListNode;\n\n/* Constructor */\nDoublyListNode *newDoublyListNode(int num) {\n    DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode));\n    new->val = num;\n    new->next = NULL;\n    new->prev = NULL;\n    return new;\n}\n\n/* Destructor */\nvoid delDoublyListNode(DoublyListNode *node) {\n    free(node);\n}\n\n/* Double-ended queue based on doubly linked list implementation */\ntypedef struct {\n    DoublyListNode *front, *rear; // Head node front, tail node rear\n    int queSize;                  // Length of the double-ended queue\n} LinkedListDeque;\n\n/* Constructor */\nLinkedListDeque *newLinkedListDeque() {\n    LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque));\n    deque->front = NULL;\n    deque->rear = NULL;\n    deque->queSize = 0;\n    return deque;\n}\n\n/* Destructor */\nvoid delLinkedListdeque(LinkedListDeque *deque) {\n    // Free all nodes\n    for (int i = 0; i < deque->queSize && deque->front != NULL; i++) {\n        DoublyListNode *tmp = deque->front;\n        deque->front = deque->front->next;\n        free(tmp);\n    }\n    // Free deque structure\n    free(deque);\n}\n\n/* Get the length of the queue */\nint size(LinkedListDeque *deque) {\n    return deque->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(LinkedListDeque *deque) {\n    return (size(deque) == 0);\n}\n\n/* Enqueue */\nvoid push(LinkedListDeque *deque, int num, bool isFront) {\n    DoublyListNode *node = newDoublyListNode(num);\n    // If list is empty, set both front and rear to node\n    if (empty(deque)) {\n        deque->front = deque->rear = node;\n    }\n    // Front of the queue enqueue operation\n    else if (isFront) {\n        // Add node to the head of the linked list\n        deque->front->prev = node;\n        node->next = deque->front;\n        deque->front = node; // Update head node\n    }\n    // Rear of the queue enqueue operation\n    else {\n        // Add node to the tail of the linked list\n        deque->rear->next = node;\n        node->prev = deque->rear;\n        deque->rear = node;\n    }\n    deque->queSize++; // Update queue length\n}\n\n/* Front of the queue enqueue */\nvoid pushFirst(LinkedListDeque *deque, int num) {\n    push(deque, num, true);\n}\n\n/* Rear of the queue enqueue */\nvoid pushLast(LinkedListDeque *deque, int num) {\n    push(deque, num, false);\n}\n\n/* Return list for printing */\nint peekFirst(LinkedListDeque *deque) {\n    assert(size(deque) && deque->front);\n    return deque->front->val;\n}\n\n/* Driver Code */\nint peekLast(LinkedListDeque *deque) {\n    assert(size(deque) && deque->rear);\n    return deque->rear->val;\n}\n\n/* Dequeue */\nint pop(LinkedListDeque *deque, bool isFront) {\n    if (empty(deque))\n        return -1;\n    int val;\n    // Temporarily store head node value\n    if (isFront) {\n        val = peekFirst(deque); // Delete head node\n        DoublyListNode *fNext = deque->front->next;\n        if (fNext) {\n            fNext->prev = NULL;\n            deque->front->next = NULL;\n        }\n        delDoublyListNode(deque->front);\n        deque->front = fNext; // Update head node\n    }\n    // Temporarily store tail node value\n    else {\n        val = peekLast(deque); // Delete tail node\n        DoublyListNode *rPrev = deque->rear->prev;\n        if (rPrev) {\n            rPrev->next = NULL;\n            deque->rear->prev = NULL;\n        }\n        delDoublyListNode(deque->rear);\n        deque->rear = rPrev; // Update tail node\n    }\n    deque->queSize--; // Update queue length\n    return val;\n}\n\n/* Rear of the queue dequeue */\nint popFirst(LinkedListDeque *deque) {\n    return pop(deque, true);\n}\n\n/* Access rear of the queue element */\nint popLast(LinkedListDeque *deque) {\n    return pop(deque, false);\n}\n\n/* Print queue */\nvoid printLinkedListDeque(LinkedListDeque *deque) {\n    int *arr = malloc(sizeof(int) * deque->queSize);\n    // Copy data from list to array\n    int i;\n    DoublyListNode *node;\n    for (i = 0, node = deque->front; i < deque->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, deque->queSize);\n    free(arr);\n}\n
linkedlist_deque.kt
/* Doubly linked list node */\nclass ListNode(var _val: Int) {\n    // Node value\n    var next: ListNode? = null // Successor node reference\n    var prev: ListNode? = null // Predecessor node reference\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private var front: ListNode? = null // Head node front\n    private var rear: ListNode? = null // Tail node rear\n    private var queSize: Int = 0 // Length of the double-ended queue\n\n    /* Get the length of the double-ended queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the double-ended queue is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Enqueue operation */\n    fun push(num: Int, isFront: Boolean) {\n        val node = ListNode(num)\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty()) {\n            rear = node\n            front = rear\n            // Front of the queue enqueue operation\n        } else if (isFront) {\n            // Add node to the head of the linked list\n            front?.prev = node\n            node.next = front\n            front = node // Update head node\n            // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear?.next = node\n            node.prev = rear\n            rear = node // Update tail node\n        }\n        queSize++ // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    fun pushFirst(num: Int) {\n        push(num, true)\n    }\n\n    /* Rear of the queue enqueue */\n    fun pushLast(num: Int) {\n        push(num, false)\n    }\n\n    /* Dequeue operation */\n    fun pop(isFront: Boolean): Int {\n        if (isEmpty()) \n            throw IndexOutOfBoundsException()\n        val _val: Int\n        // Temporarily store head node value\n        if (isFront) {\n            _val = front!!._val // Delete head node\n            // Delete head node\n            val fNext = front!!.next\n            if (fNext != null) {\n                fNext.prev = null\n                front!!.next = null\n            }\n            front = fNext // Update head node\n            // Temporarily store tail node value\n        } else {\n            _val = rear!!._val // Delete tail node\n            // Update tail node\n            val rPrev = rear!!.prev\n            if (rPrev != null) {\n                rPrev.next = null\n                rear!!.prev = null\n            }\n            rear = rPrev // Update tail node\n        }\n        queSize-- // Update queue length\n        return _val\n    }\n\n    /* Rear of the queue dequeue */\n    fun popFirst(): Int {\n        return pop(true)\n    }\n\n    /* Access rear of the queue element */\n    fun popLast(): Int {\n        return pop(false)\n    }\n\n    /* Return list for printing */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Driver Code */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return rear!!._val\n    }\n\n    /* Return array for printing */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_deque.rb
=begin\nFile: linkedlist_deque.rb\nCreated Time: 2024-04-06\nAuthor: Xuan Khoa Tu Nguyen (ngxktuzkai2000@gmail.com)\n=end\n\n### Doubly linked list node\nclass ListNode\n  attr_accessor :val\n  attr_accessor :next # Successor node reference\n  attr_accessor :prev # Predecessor node reference\n\n  ### Constructor ###\n  def initialize(val)\n    @val = val\n  end\nend\n\n### Deque based on doubly linked list ###\nclass LinkedListDeque\n  ### Get deque length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @front = nil  # Head node front\n    @rear = nil   # Tail node rear\n    @size = 0     # Length of the double-ended queue\n  end\n\n  ### Check if deque is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue operation ###\n  def push(num, is_front)\n    node = ListNode.new(num)\n    # If list is empty, set both front and rear to node\n    if is_empty?\n      @front = @rear = node\n    # Front of the queue enqueue operation\n    elsif is_front\n      # Add node to the head of the linked list\n      @front.prev = node\n      node.next = @front\n      @front = node # Update head node\n    # Rear of the queue enqueue operation\n    else\n      # Add node to the tail of the linked list\n      @rear.next = node\n      node.prev = @rear\n      @rear = node # Update tail node\n    end\n    @size += 1 # Update queue length\n  end\n\n  ### Enqueue at front ###\n  def push_first(num)\n    push(num, true)\n  end\n\n  ### Enqueue at rear ###\n  def push_last(num)\n    push(num, false)\n  end\n\n  ### Dequeue operation ###\n  def pop(is_front)\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    # Temporarily store head node value\n    if is_front\n      val = @front.val # Delete head node\n      # Delete head node\n      fnext = @front.next\n      unless fnext.nil?\n        fnext.prev = nil\n        @front.next = nil\n      end\n      @front = fnext # Update head node\n    # Temporarily store tail node value\n    else\n      val = @rear.val # Delete tail node\n      # Update tail node\n      rprev = @rear.prev\n      unless rprev.nil?\n        rprev.next = nil\n        @rear.prev = nil\n      end\n      @rear = rprev # Update tail node\n    end\n    @size -= 1 # Update queue length\n\n    val\n  end\n\n  ### Dequeue from front ###\n  def pop_first\n    pop(true)\n  end\n\n  ### Dequeue from front ###\n  def pop_last\n    pop(false)\n  end\n\n  ### Access front element ###\n  def peek_first\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @front.val\n  end\n\n  ### Access rear element ###\n  def peek_last\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @rear.val\n  end\n\n  ### Return array for printing ###\n  def to_array\n    node = @front\n    res = Array.new(size, 0)\n    for i in 0...size\n      res[i] = node.val\n      node = node.next\n    end\n    res\n  end\nend\n
","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

As shown in Figure 5-9, similar to implementing a queue based on an array, we can also use a circular array to implement a deque.

ArrayDequepush_last()push_first()pop_last()pop_first()

Figure 5-9   Enqueue and dequeue operations in array implementation of deque

Based on the queue implementation, we only need to add methods for \"enqueue at front\" and \"dequeue from rear\":

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_deque.py
class ArrayDeque:\n    \"\"\"Double-ended queue based on circular array implementation\"\"\"\n\n    def __init__(self, capacity: int):\n        \"\"\"Constructor\"\"\"\n        self._nums: list[int] = [0] * capacity\n        self._front: int = 0\n        self._size: int = 0\n\n    def capacity(self) -> int:\n        \"\"\"Get the capacity of the double-ended queue\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Get the length of the double-ended queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the double-ended queue is empty\"\"\"\n        return self._size == 0\n\n    def index(self, i: int) -> int:\n        \"\"\"Calculate circular array index\"\"\"\n        # Use modulo operation to wrap the array head and tail together\n        # When i passes the tail of the array, return to the head\n        # When i passes the head of the array, return to the tail\n        return (i + self.capacity()) % self.capacity()\n\n    def push_first(self, num: int):\n        \"\"\"Front of the queue enqueue\"\"\"\n        if self._size == self.capacity():\n            print(\"Double-ended queue is full\")\n            return\n        # Front pointer moves one position to the left\n        # Use modulo operation to wrap front around to the tail after passing the head of the array\n        self._front = self.index(self._front - 1)\n        # Add num to the front of the queue\n        self._nums[self._front] = num\n        self._size += 1\n\n    def push_last(self, num: int):\n        \"\"\"Rear of the queue enqueue\"\"\"\n        if self._size == self.capacity():\n            print(\"Double-ended queue is full\")\n            return\n        # Calculate rear pointer, points to rear index + 1\n        rear = self.index(self._front + self._size)\n        # Add num to the rear of the queue\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop_first(self) -> int:\n        \"\"\"Front of the queue dequeue\"\"\"\n        num = self.peek_first()\n        # Front pointer moves one position backward\n        self._front = self.index(self._front + 1)\n        self._size -= 1\n        return num\n\n    def pop_last(self) -> int:\n        \"\"\"Rear of the queue dequeue\"\"\"\n        num = self.peek_last()\n        self._size -= 1\n        return num\n\n    def peek_first(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._nums[self._front]\n\n    def peek_last(self) -> int:\n        \"\"\"Access rear of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        # Calculate tail element index\n        last = self.index(self._front + self._size - 1)\n        return self._nums[last]\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return array for printing\"\"\"\n        # Only convert list elements within the valid length range\n        res = []\n        for i in range(self._size):\n            res.append(self._nums[self.index(self._front + i)])\n        return res\n
array_deque.cpp
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n  private:\n    vector<int> nums; // Array for storing double-ended queue elements\n    int front;        // Front pointer, points to the front of the queue element\n    int queSize;      // Double-ended queue length\n\n  public:\n    /* Constructor */\n    ArrayDeque(int capacity) {\n        nums.resize(capacity);\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    int capacity() {\n        return nums.size();\n    }\n\n    /* Get the length of the double-ended queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    int index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity();\n    }\n\n    /* Front of the queue enqueue */\n    void pushFirst(int num) {\n        if (queSize == capacity()) {\n            cout << \"Double-ended queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    void pushLast(int num) {\n        if (queSize == capacity()) {\n            cout << \"Double-ended queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    int popFirst() {\n        int num = peekFirst();\n        // Move front pointer backward by one position\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return nums[front];\n    }\n\n    /* Driver Code */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        // Initialize double-ended queue\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    vector<int> toVector() {\n        // Elements enqueue\n        vector<int> res(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n};\n
array_deque.java
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private int[] nums; // Array for storing double-ended queue elements\n    private int front; // Front pointer, points to the front of the queue element\n    private int queSize; // Double-ended queue length\n\n    /* Constructor */\n    public ArrayDeque(int capacity) {\n        this.nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    private int index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity();\n    }\n\n    /* Front of the queue enqueue */\n    public void pushFirst(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    public void pushLast(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    public int popFirst() {\n        int num = peekFirst();\n        // Move front pointer backward by one position\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    public int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Driver Code */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        // Initialize double-ended queue\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    public int[] toArray() {\n        // Elements enqueue\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.cs
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    int[] nums;  // Array for storing double-ended queue elements\n    int front;   // Front pointer, points to the front of the queue element\n    int queSize; // Double-ended queue length\n\n    /* Constructor */\n    public ArrayDeque(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    int Index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + Capacity()) % Capacity();\n    }\n\n    /* Front of the queue enqueue */\n    public void PushFirst(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = Index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    public void PushLast(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = Index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    public int PopFirst() {\n        int num = PeekFirst();\n        // Move front pointer backward by one position\n        front = Index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    public int PopLast() {\n        int num = PeekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int PeekFirst() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        return nums[front];\n    }\n\n    /* Driver Code */\n    public int PeekLast() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        // Initialize double-ended queue\n        int last = Index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    public int[] ToArray() {\n        // Elements enqueue\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[Index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.go
/* Double-ended queue based on circular array implementation */\ntype arrayDeque struct {\n    nums        []int // Array for storing double-ended queue elements\n    front       int   // Front pointer, points to the front of the queue element\n    queSize     int   // Double-ended queue length\n    queCapacity int   // Queue capacity (maximum number of elements)\n}\n\n/* Access front of the queue element */\nfunc newArrayDeque(queCapacity int) *arrayDeque {\n    return &arrayDeque{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* Get the length of the double-ended queue */\nfunc (q *arrayDeque) size() int {\n    return q.queSize\n}\n\n/* Check if the double-ended queue is empty */\nfunc (q *arrayDeque) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Calculate circular array index */\nfunc (q *arrayDeque) index(i int) int {\n    // Use modulo operation to wrap the array head and tail together\n    // When i passes the tail of the array, return to the head\n    // When i passes the head of the array, return to the tail\n    return (i + q.queCapacity) % q.queCapacity\n}\n\n/* Front of the queue enqueue */\nfunc (q *arrayDeque) pushFirst(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Double-ended queue is full\")\n        return\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Add num to the front of the queue\n    q.front = q.index(q.front - 1)\n    // Add num to front of queue\n    q.nums[q.front] = num\n    q.queSize++\n}\n\n/* Rear of the queue enqueue */\nfunc (q *arrayDeque) pushLast(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Double-ended queue is full\")\n        return\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    rear := q.index(q.front + q.queSize)\n    // Front pointer moves one position backward\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Rear of the queue dequeue */\nfunc (q *arrayDeque) popFirst() any {\n    num := q.peekFirst()\n    if num == nil {\n        return nil\n    }\n    // Move front pointer backward by one position\n    q.front = q.index(q.front + 1)\n    q.queSize--\n    return num\n}\n\n/* Access rear of the queue element */\nfunc (q *arrayDeque) popLast() any {\n    num := q.peekLast()\n    if num == nil {\n        return nil\n    }\n    q.queSize--\n    return num\n}\n\n/* Return list for printing */\nfunc (q *arrayDeque) peekFirst() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Driver Code */\nfunc (q *arrayDeque) peekLast() any {\n    if q.isEmpty() {\n        return nil\n    }\n    // Initialize double-ended queue\n    last := q.index(q.front + q.queSize - 1)\n    return q.nums[last]\n}\n\n/* Get Slice for printing */\nfunc (q *arrayDeque) toSlice() []int {\n    // Elements enqueue\n    res := make([]int, q.queSize)\n    for i, j := 0, q.front; i < q.queSize; i++ {\n        res[i] = q.nums[q.index(j)]\n        j++\n    }\n    return res\n}\n
array_deque.swift
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private var nums: [Int] // Array for storing double-ended queue elements\n    private var front: Int // Front pointer, points to the front of the queue element\n    private var _size: Int // Double-ended queue length\n\n    /* Constructor */\n    init(capacity: Int) {\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Get the capacity of the double-ended queue */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Get the length of the double-ended queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the double-ended queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Calculate circular array index */\n    private func index(i: Int) -> Int {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        (i + capacity()) % capacity()\n    }\n\n    /* Front of the queue enqueue */\n    func pushFirst(num: Int) {\n        if size() == capacity() {\n            print(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(i: front - 1)\n        // Add num to front of queue\n        nums[front] = num\n        _size += 1\n    }\n\n    /* Rear of the queue enqueue */\n    func pushLast(num: Int) {\n        if size() == capacity() {\n            print(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        let rear = index(i: front + size())\n        // Front pointer moves one position backward\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Rear of the queue dequeue */\n    func popFirst() -> Int {\n        let num = peekFirst()\n        // Move front pointer backward by one position\n        front = index(i: front + 1)\n        _size -= 1\n        return num\n    }\n\n    /* Access rear of the queue element */\n    func popLast() -> Int {\n        let num = peekLast()\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return nums[front]\n    }\n\n    /* Driver Code */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        // Initialize double-ended queue\n        let last = index(i: front + size() - 1)\n        return nums[last]\n    }\n\n    /* Return array for printing */\n    func toArray() -> [Int] {\n        // Elements enqueue\n        (front ..< front + size()).map { nums[index(i: $0)] }\n    }\n}\n
array_deque.js
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    #nums; // Array for storing double-ended queue elements\n    #front; // Front pointer, points to the front of the queue element\n    #queSize; // Double-ended queue length\n\n    /* Constructor */\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n        this.#front = 0;\n        this.#queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    capacity() {\n        return this.#nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Calculate circular array index */\n    index(i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Front of the queue enqueue */\n    pushFirst(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        this.#front = this.index(this.#front - 1);\n        // Add num to front of queue\n        this.#nums[this.#front] = num;\n        this.#queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    pushLast(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        const rear = this.index(this.#front + this.#queSize);\n        // Front pointer moves one position backward\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    popFirst() {\n        const num = this.peekFirst();\n        // Move front pointer backward by one position\n        this.#front = this.index(this.#front + 1);\n        this.#queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    popLast() {\n        const num = this.peekLast();\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peekFirst() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.#nums[this.#front];\n    }\n\n    /* Driver Code */\n    peekLast() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Initialize double-ended queue\n        const last = this.index(this.#front + this.#queSize - 1);\n        return this.#nums[last];\n    }\n\n    /* Return array for printing */\n    toArray() {\n        // Elements enqueue\n        const res = [];\n        for (let i = 0, j = this.#front; i < this.#queSize; i++, j++) {\n            res[i] = this.#nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.ts
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private nums: number[]; // Array for storing double-ended queue elements\n    private front: number; // Front pointer, points to the front of the queue element\n    private queSize: number; // Double-ended queue length\n\n    /* Constructor */\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = 0;\n        this.queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Calculate circular array index */\n    index(i: number): number {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Front of the queue enqueue */\n    pushFirst(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        this.front = this.index(this.front - 1);\n        // Add num to front of queue\n        this.nums[this.front] = num;\n        this.queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    pushLast(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        const rear: number = this.index(this.front + this.queSize);\n        // Front pointer moves one position backward\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    popFirst(): number {\n        const num: number = this.peekFirst();\n        // Move front pointer backward by one position\n        this.front = this.index(this.front + 1);\n        this.queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    popLast(): number {\n        const num: number = this.peekLast();\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peekFirst(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.nums[this.front];\n    }\n\n    /* Driver Code */\n    peekLast(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Initialize double-ended queue\n        const last = this.index(this.front + this.queSize - 1);\n        return this.nums[last];\n    }\n\n    /* Return array for printing */\n    toArray(): number[] {\n        // Elements enqueue\n        const res: number[] = [];\n        for (let i = 0, j = this.front; i < this.queSize; i++, j++) {\n            res[i] = this.nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.dart
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n  late List<int> _nums; // Array for storing double-ended queue elements\n  late int _front; // Front pointer, points to the front of the queue element\n  late int _queSize; // Double-ended queue length\n\n  /* Constructor */\n  ArrayDeque(int capacity) {\n    this._nums = List.filled(capacity, 0);\n    this._front = this._queSize = 0;\n  }\n\n  /* Get the capacity of the double-ended queue */\n  int capacity() {\n    return _nums.length;\n  }\n\n  /* Get the length of the double-ended queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the double-ended queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Calculate circular array index */\n  int index(int i) {\n    // Use modulo operation to wrap the array head and tail together\n    // When i passes the tail of the array, return to the head\n    // When i passes the head of the array, return to the tail\n    return (i + capacity()) % capacity();\n  }\n\n  /* Front of the queue enqueue */\n  void pushFirst(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Double-ended queue is full\");\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Use modulo operation to wrap _front from array head back to tail\n    _front = index(_front - 1);\n    // Add _num to queue front\n    _nums[_front] = _num;\n    _queSize++;\n  }\n\n  /* Rear of the queue enqueue */\n  void pushLast(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Double-ended queue is full\");\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    int rear = index(_front + _queSize);\n    // Add _num to queue rear\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Rear of the queue dequeue */\n  int popFirst() {\n    int _num = peekFirst();\n    // Move front pointer right by one\n    _front = index(_front + 1);\n    _queSize--;\n    return _num;\n  }\n\n  /* Access rear of the queue element */\n  int popLast() {\n    int _num = peekLast();\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peekFirst() {\n    if (isEmpty()) {\n      throw Exception(\"Deque is empty\");\n    }\n    return _nums[_front];\n  }\n\n  /* Driver Code */\n  int peekLast() {\n    if (isEmpty()) {\n      throw Exception(\"Deque is empty\");\n    }\n    // Initialize double-ended queue\n    int last = index(_front + _queSize - 1);\n    return _nums[last];\n  }\n\n  /* Return array for printing */\n  List<int> toArray() {\n    // Elements enqueue\n    List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[index(j)];\n    }\n    return res;\n  }\n}\n
array_deque.rs
/* Double-ended queue based on circular array implementation */\nstruct ArrayDeque<T> {\n    nums: Vec<T>,    // Array for storing double-ended queue elements\n    front: usize,    // Front pointer, points to the front of the queue element\n    que_size: usize, // Double-ended queue length\n}\n\nimpl<T: Copy + Default> ArrayDeque<T> {\n    /* Constructor */\n    pub fn new(capacity: usize) -> Self {\n        Self {\n            nums: vec![T::default(); capacity],\n            front: 0,\n            que_size: 0,\n        }\n    }\n\n    /* Get the capacity of the double-ended queue */\n    pub fn capacity(&self) -> usize {\n        self.nums.len()\n    }\n\n    /* Get the length of the double-ended queue */\n    pub fn size(&self) -> usize {\n        self.que_size\n    }\n\n    /* Check if the double-ended queue is empty */\n    pub fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Calculate circular array index */\n    fn index(&self, i: i32) -> usize {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        ((i + self.capacity() as i32) % self.capacity() as i32) as usize\n    }\n\n    /* Front of the queue enqueue */\n    pub fn push_first(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        self.front = self.index(self.front as i32 - 1);\n        // Add num to front of queue\n        self.nums[self.front] = num;\n        self.que_size += 1;\n    }\n\n    /* Rear of the queue enqueue */\n    pub fn push_last(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        let rear = self.index(self.front as i32 + self.que_size as i32);\n        // Front pointer moves one position backward\n        self.nums[rear] = num;\n        self.que_size += 1;\n    }\n\n    /* Rear of the queue dequeue */\n    fn pop_first(&mut self) -> T {\n        let num = self.peek_first();\n        // Move front pointer backward by one position\n        self.front = self.index(self.front as i32 + 1);\n        self.que_size -= 1;\n        num\n    }\n\n    /* Access rear of the queue element */\n    fn pop_last(&mut self) -> T {\n        let num = self.peek_last();\n        self.que_size -= 1;\n        num\n    }\n\n    /* Return list for printing */\n    fn peek_first(&self) -> T {\n        if self.is_empty() {\n            panic!(\"Deque is empty\")\n        };\n        self.nums[self.front]\n    }\n\n    /* Driver Code */\n    fn peek_last(&self) -> T {\n        if self.is_empty() {\n            panic!(\"Deque is empty\")\n        };\n        // Initialize double-ended queue\n        let last = self.index(self.front as i32 + self.que_size as i32 - 1);\n        self.nums[last]\n    }\n\n    /* Return array for printing */\n    fn to_array(&self) -> Vec<T> {\n        // Elements enqueue\n        let mut res = vec![T::default(); self.que_size];\n        let mut j = self.front;\n        for i in 0..self.que_size {\n            res[i] = self.nums[self.index(j as i32)];\n            j += 1;\n        }\n        res\n    }\n}\n
array_deque.c
/* Double-ended queue based on circular array implementation */\ntypedef struct {\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Rear pointer, points to rear + 1\n    int queCapacity; // Queue capacity\n} ArrayDeque;\n\n/* Constructor */\nArrayDeque *newArrayDeque(int capacity) {\n    ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque));\n    // Initialize array\n    deque->queCapacity = capacity;\n    deque->nums = (int *)malloc(sizeof(int) * deque->queCapacity);\n    deque->front = deque->queSize = 0;\n    return deque;\n}\n\n/* Destructor */\nvoid delArrayDeque(ArrayDeque *deque) {\n    free(deque->nums);\n    free(deque);\n}\n\n/* Get the capacity of the double-ended queue */\nint capacity(ArrayDeque *deque) {\n    return deque->queCapacity;\n}\n\n/* Get the length of the double-ended queue */\nint size(ArrayDeque *deque) {\n    return deque->queSize;\n}\n\n/* Check if the double-ended queue is empty */\nbool empty(ArrayDeque *deque) {\n    return deque->queSize == 0;\n}\n\n/* Calculate circular array index */\nint dequeIndex(ArrayDeque *deque, int i) {\n    // Use modulo operation to wrap the array head and tail together\n    // When i exceeds array end, wrap to head\n    // When i passes the head of the array, return to the tail\n    return ((i + capacity(deque)) % capacity(deque));\n}\n\n/* Front of the queue enqueue */\nvoid pushFirst(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Deque is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Use modulo to wrap front from array head to rear\n    deque->front = dequeIndex(deque, deque->front - 1);\n    // Add num to queue front\n    deque->nums[deque->front] = num;\n    deque->queSize++;\n}\n\n/* Rear of the queue enqueue */\nvoid pushLast(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Deque is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    int rear = dequeIndex(deque, deque->front + deque->queSize);\n    // Front pointer moves one position backward\n    deque->nums[rear] = num;\n    deque->queSize++;\n}\n\n/* Return list for printing */\nint peekFirst(ArrayDeque *deque) {\n    // Access error: Deque is empty\n    assert(empty(deque) == 0);\n    return deque->nums[deque->front];\n}\n\n/* Driver Code */\nint peekLast(ArrayDeque *deque) {\n    // Access error: Deque is empty\n    assert(empty(deque) == 0);\n    int last = dequeIndex(deque, deque->front + deque->queSize - 1);\n    return deque->nums[last];\n}\n\n/* Rear of the queue dequeue */\nint popFirst(ArrayDeque *deque) {\n    int num = peekFirst(deque);\n    // Move front pointer backward by one position\n    deque->front = dequeIndex(deque, deque->front + 1);\n    deque->queSize--;\n    return num;\n}\n\n/* Access rear of the queue element */\nint popLast(ArrayDeque *deque) {\n    int num = peekLast(deque);\n    deque->queSize--;\n    return num;\n}\n\n/* Return array for printing */\nint *toArray(ArrayDeque *deque, int *queSize) {\n    *queSize = deque->queSize;\n    int *res = (int *)calloc(deque->queSize, sizeof(int));\n    int j = deque->front;\n    for (int i = 0; i < deque->queSize; i++) {\n        res[i] = deque->nums[j % deque->queCapacity];\n        j++;\n    }\n    return res;\n}\n
array_deque.kt
/* Constructor */\nclass ArrayDeque(capacity: Int) {\n    private var nums: IntArray = IntArray(capacity) // Array for storing double-ended queue elements\n    private var front: Int = 0 // Front pointer, points to the front of the queue element\n    private var queSize: Int = 0 // Double-ended queue length\n\n    /* Get the capacity of the double-ended queue */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Get the length of the double-ended queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the double-ended queue is empty */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Calculate circular array index */\n    private fun index(i: Int): Int {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity()\n    }\n\n    /* Front of the queue enqueue */\n    fun pushFirst(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1)\n        // Add num to front of queue\n        nums[front] = num\n        queSize++\n    }\n\n    /* Rear of the queue enqueue */\n    fun pushLast(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        val rear = index(front + queSize)\n        // Front pointer moves one position backward\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Rear of the queue dequeue */\n    fun popFirst(): Int {\n        val num = peekFirst()\n        // Move front pointer backward by one position\n        front = index(front + 1)\n        queSize--\n        return num\n    }\n\n    /* Access rear of the queue element */\n    fun popLast(): Int {\n        val num = peekLast()\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Driver Code */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Initialize double-ended queue\n        val last = index(front + queSize - 1)\n        return nums[last]\n    }\n\n    /* Return array for printing */\n    fun toArray(): IntArray {\n        // Elements enqueue\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[index(j)]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
array_deque.rb
### Deque based on circular array ###\nclass ArrayDeque\n  ### Get deque length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize(capacity)\n    @nums = Array.new(capacity, 0)\n    @front = 0\n    @size = 0\n  end\n\n  ### Get deque capacity ###\n  def capacity\n    @nums.length\n  end\n\n  ### Check if deque is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue at front ###\n  def push_first(num)\n    if size == capacity\n      puts 'Double-ended queue is full'\n      return\n    end\n\n    # Use modulo operation to wrap front around to the tail after passing the head of the array\n    # Add num to the front of the queue\n    @front = index(@front - 1)\n    # Add num to front of queue\n    @nums[@front] = num\n    @size += 1\n  end\n\n  ### Enqueue at rear ###\n  def push_last(num)\n    if size == capacity\n      puts 'Double-ended queue is full'\n      return\n    end\n\n    # Use modulo operation to wrap rear around to the head after passing the tail of the array\n    rear = index(@front + size)\n    # Front pointer moves one position backward\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Dequeue from front ###\n  def pop_first\n    num = peek_first\n    # Move front pointer backward by one position\n    @front = index(@front + 1)\n    @size -= 1\n    num\n  end\n\n  ### Dequeue from rear ###\n  def pop_last\n    num = peek_last\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek_first\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Access rear element ###\n  def peek_last\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    # Initialize double-ended queue\n    last = index(@front + size - 1)\n    @nums[last]\n  end\n\n  ### Return array for printing ###\n  def to_array\n    # Elements enqueue\n    res = []\n    for i in 0...size\n      res << @nums[index(@front + i)]\n    end\n    res\n  end\n\n  private\n\n  ### Calculate circular array index ###\n  def index(i)\n    # Use modulo operation to wrap the array head and tail together\n    # When i passes the tail of the array, return to the head\n    # When i passes the head of the array, return to the tail\n    (i + capacity) % capacity\n  end\nend\n
","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#533-deque-applications","level":2,"title":"5.3.3   Deque Applications","text":"

A deque combines the logic of both stacks and queues. Therefore, it can implement all application scenarios of both, while providing greater flexibility.

We know that the \"undo\" function in software is typically implemented using a stack: the system pushes each change operation onto the stack and then implements undo through pop. However, considering system resource limitations, software usually limits the number of undo steps (for example, only allowing 50 steps to be saved). When the stack length exceeds 50, the software needs to perform a deletion operation at the bottom of the stack (front of the queue). But a stack cannot implement this functionality, so a deque is needed to replace the stack. Note that the core logic of \"undo\" still follows the LIFO principle of a stack; it's just that the deque can more flexibly implement some additional logic.

","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   Queue","text":"

A queue is a linear data structure that follows the First In First Out (FIFO) rule. As the name suggests, a queue simulates the phenomenon of lining up, where newcomers continuously join the end of the queue, while people at the front of the queue leave one by one.

As shown in Figure 5-4, we call the front of the queue the \"front\" and the end the \"rear.\" The operation of adding an element to the rear is called \"enqueue,\" and the operation of removing the front element is called \"dequeue.\"

Figure 5-4   FIFO rule of queue

","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521-common-queue-operations","level":2,"title":"5.2.1   Common Queue Operations","text":"

The common operations on a queue are shown in Table 5-2. Note that method names may vary across different programming languages. We adopt the same naming convention as for stacks here.

Table 5-2   Efficiency of Queue Operations

Method Description Time Complexity push() Enqueue element, add element to rear \\(O(1)\\) pop() Dequeue front element \\(O(1)\\) peek() Access front element \\(O(1)\\)

We can directly use the ready-made queue classes in programming languages:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby queue.py
from collections import deque\n\n# Initialize queue\n# In Python, we generally use the deque class as a queue\n# Although queue.Queue() is a pure queue class, it is not very user-friendly, so it is not recommended\nque: deque[int] = deque()\n\n# Enqueue elements\nque.append(1)\nque.append(3)\nque.append(2)\nque.append(5)\nque.append(4)\n\n# Access front element\nfront: int = que[0]\n\n# Dequeue element\npop: int = que.popleft()\n\n# Get queue length\nsize: int = len(que)\n\n# Check if queue is empty\nis_empty: bool = len(que) == 0\n
queue.cpp
/* Initialize queue */\nqueue<int> queue;\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nint front = queue.front();\n\n/* Dequeue element */\nqueue.pop();\n\n/* Get queue length */\nint size = queue.size();\n\n/* Check if queue is empty */\nbool empty = queue.empty();\n
queue.java
/* Initialize queue */\nQueue<Integer> queue = new LinkedList<>();\n\n/* Enqueue elements */\nqueue.offer(1);\nqueue.offer(3);\nqueue.offer(2);\nqueue.offer(5);\nqueue.offer(4);\n\n/* Access front element */\nint peek = queue.peek();\n\n/* Dequeue element */\nint pop = queue.poll();\n\n/* Get queue length */\nint size = queue.size();\n\n/* Check if queue is empty */\nboolean isEmpty = queue.isEmpty();\n
queue.cs
/* Initialize queue */\nQueue<int> queue = new();\n\n/* Enqueue elements */\nqueue.Enqueue(1);\nqueue.Enqueue(3);\nqueue.Enqueue(2);\nqueue.Enqueue(5);\nqueue.Enqueue(4);\n\n/* Access front element */\nint peek = queue.Peek();\n\n/* Dequeue element */\nint pop = queue.Dequeue();\n\n/* Get queue length */\nint size = queue.Count;\n\n/* Check if queue is empty */\nbool isEmpty = queue.Count == 0;\n
queue_test.go
/* Initialize queue */\n// In Go, use list as a queue\nqueue := list.New()\n\n/* Enqueue elements */\nqueue.PushBack(1)\nqueue.PushBack(3)\nqueue.PushBack(2)\nqueue.PushBack(5)\nqueue.PushBack(4)\n\n/* Access front element */\npeek := queue.Front()\n\n/* Dequeue element */\npop := queue.Front()\nqueue.Remove(pop)\n\n/* Get queue length */\nsize := queue.Len()\n\n/* Check if queue is empty */\nisEmpty := queue.Len() == 0\n
queue.swift
/* Initialize queue */\n// Swift does not have a built-in queue class, can use Array as a queue\nvar queue: [Int] = []\n\n/* Enqueue elements */\nqueue.append(1)\nqueue.append(3)\nqueue.append(2)\nqueue.append(5)\nqueue.append(4)\n\n/* Access front element */\nlet peek = queue.first!\n\n/* Dequeue element */\n// Since it's an array, removeFirst has O(n) complexity\nlet pool = queue.removeFirst()\n\n/* Get queue length */\nlet size = queue.count\n\n/* Check if queue is empty */\nlet isEmpty = queue.isEmpty\n
queue.js
/* Initialize queue */\n// JavaScript does not have a built-in queue, can use Array as a queue\nconst queue = [];\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nconst peek = queue[0];\n\n/* Dequeue element */\n// The underlying structure is an array, so shift() has O(n) time complexity\nconst pop = queue.shift();\n\n/* Get queue length */\nconst size = queue.length;\n\n/* Check if queue is empty */\nconst empty = queue.length === 0;\n
queue.ts
/* Initialize queue */\n// TypeScript does not have a built-in queue, can use Array as a queue\nconst queue: number[] = [];\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nconst peek = queue[0];\n\n/* Dequeue element */\n// The underlying structure is an array, so shift() has O(n) time complexity\nconst pop = queue.shift();\n\n/* Get queue length */\nconst size = queue.length;\n\n/* Check if queue is empty */\nconst empty = queue.length === 0;\n
queue.dart
/* Initialize queue */\n// In Dart, the Queue class is a deque and can also be used as a queue\nQueue<int> queue = Queue();\n\n/* Enqueue elements */\nqueue.add(1);\nqueue.add(3);\nqueue.add(2);\nqueue.add(5);\nqueue.add(4);\n\n/* Access front element */\nint peek = queue.first;\n\n/* Dequeue element */\nint pop = queue.removeFirst();\n\n/* Get queue length */\nint size = queue.length;\n\n/* Check if queue is empty */\nbool isEmpty = queue.isEmpty;\n
queue.rs
/* Initialize deque */\n// In Rust, use deque as a regular queue\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Enqueue elements */\ndeque.push_back(1);\ndeque.push_back(3);\ndeque.push_back(2);\ndeque.push_back(5);\ndeque.push_back(4);\n\n/* Access front element */\nif let Some(front) = deque.front() {\n}\n\n/* Dequeue element */\nif let Some(pop) = deque.pop_front() {\n}\n\n/* Get queue length */\nlet size = deque.len();\n\n/* Check if queue is empty */\nlet is_empty = deque.is_empty();\n
queue.c
// C does not provide a built-in queue\n
queue.kt
/* Initialize queue */\nval queue = LinkedList<Int>()\n\n/* Enqueue elements */\nqueue.offer(1)\nqueue.offer(3)\nqueue.offer(2)\nqueue.offer(5)\nqueue.offer(4)\n\n/* Access front element */\nval peek = queue.peek()\n\n/* Dequeue element */\nval pop = queue.poll()\n\n/* Get queue length */\nval size = queue.size\n\n/* Check if queue is empty */\nval isEmpty = queue.isEmpty()\n
queue.rb
# Initialize queue\n# Ruby's built-in queue (Thread::Queue) does not have peek and traversal methods, can use Array as a queue\nqueue = []\n\n# Enqueue elements\nqueue.push(1)\nqueue.push(3)\nqueue.push(2)\nqueue.push(5)\nqueue.push(4)\n\n# Access front element\npeek = queue.first\n\n# Dequeue element\n# Please note that since it's an array, Array#shift has O(n) time complexity\npop = queue.shift\n\n# Get queue length\nsize = queue.length\n\n# Check if queue is empty\nis_empty = queue.empty?\n
Code Visualization

Full Screen >

","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#522-queue-implementation","level":2,"title":"5.2.2   Queue Implementation","text":"

To implement a queue, we need a data structure that allows adding elements at one end and removing elements at the other end. Both linked lists and arrays meet this requirement.

","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#1-linked-list-implementation","level":3,"title":"1.   Linked List Implementation","text":"

As shown in Figure 5-5, we can treat the \"head node\" and \"tail node\" of a linked list as the \"front\" and \"rear\" of the queue, respectively, with the rule that nodes can only be added at the rear and removed from the front.

LinkedListQueuepush()pop()

Figure 5-5   Enqueue and dequeue operations in linked list implementation of queue

Below is the code for implementing a queue using a linked list:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_queue.py
class LinkedListQueue:\n    \"\"\"Queue based on linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._front: ListNode | None = None  # Head node front\n        self._rear: ListNode | None = None  # Tail node rear\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Get the length of the queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Enqueue\"\"\"\n        # Add num after the tail node\n        node = ListNode(num)\n        # If the queue is empty, make both front and rear point to the node\n        if self._front is None:\n            self._front = node\n            self._rear = node\n        # If the queue is not empty, add the node after the tail node\n        else:\n            self._rear.next = node\n            self._rear = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Dequeue\"\"\"\n        num = self.peek()\n        # Delete head node\n        self._front = self._front.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Queue is empty\")\n        return self._front.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Convert to list for printing\"\"\"\n        queue = []\n        temp = self._front\n        while temp:\n            queue.append(temp.val)\n            temp = temp.next\n        return queue\n
linkedlist_queue.cpp
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n  private:\n    ListNode *front, *rear; // Head node front, tail node rear\n    int queSize;\n\n  public:\n    LinkedListQueue() {\n        front = nullptr;\n        rear = nullptr;\n        queSize = 0;\n    }\n\n    ~LinkedListQueue() {\n        // Traverse linked list to delete nodes and free memory\n        freeMemoryLinkedList(front);\n    }\n\n    /* Get the length of the queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    void push(int num) {\n        // Add num after the tail node\n        ListNode *node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == nullptr) {\n            front = node;\n            rear = node;\n        }\n        // If the queue is not empty, add the node after the tail node\n        else {\n            rear->next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    int pop() {\n        int num = peek();\n        // Delete head node\n        ListNode *tmp = front;\n        front = front->next;\n        // Free memory\n        delete tmp;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peek() {\n        if (size() == 0)\n            throw out_of_range(\"Queue is empty\");\n        return front->val;\n    }\n\n    /* Convert linked list to Vector and return */\n    vector<int> toVector() {\n        ListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_queue.java
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private ListNode front, rear; // Head node front, tail node rear\n    private int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue */\n    public void push(int num) {\n        // Add num after the tail node\n        ListNode node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node;\n            rear = node;\n        // If the queue is not empty, add the node after the tail node\n        } else {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int pop() {\n        int num = peek();\n        // Delete head node\n        front = front.next;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Convert linked list to Array and return */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.cs
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n    ListNode? front, rear;  // Head node front, tail node rear\n    int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Enqueue */\n    public void Push(int num) {\n        // Add num after the tail node\n        ListNode node = new(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node;\n            rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else if (rear != null) {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int Pop() {\n        int num = Peek();\n        // Delete head node\n        front = front?.next;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return front!.val;\n    }\n\n    /* Convert linked list to Array and return */\n    public int[] ToArray() {\n        if (front == null)\n            return [];\n\n        ListNode? node = front;\n        int[] res = new int[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.go
/* Queue based on linked list implementation */\ntype linkedListQueue struct {\n    // Use built-in package list to implement queue\n    data *list.List\n}\n\n/* Access front of the queue element */\nfunc newLinkedListQueue() *linkedListQueue {\n    return &linkedListQueue{\n        data: list.New(),\n    }\n}\n\n/* Enqueue */\nfunc (s *linkedListQueue) push(value any) {\n    s.data.PushBack(value)\n}\n\n/* Dequeue */\nfunc (s *linkedListQueue) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Return list for printing */\nfunc (s *linkedListQueue) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* Get the length of the queue */\nfunc (s *linkedListQueue) size() int {\n    return s.data.Len()\n}\n\n/* Check if the queue is empty */\nfunc (s *linkedListQueue) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListQueue) toList() *list.List {\n    return s.data\n}\n
linkedlist_queue.swift
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private var front: ListNode? // Head node\n    private var rear: ListNode? // Tail node\n    private var _size: Int\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue */\n    func push(num: Int) {\n        // Add num after the tail node\n        let node = ListNode(x: num)\n        // If the queue is empty, make both front and rear point to the node\n        if front == nil {\n            front = node\n            rear = node\n        }\n        // If the queue is not empty, add the node after the tail node\n        else {\n            rear?.next = node\n            rear = node\n        }\n        _size += 1\n    }\n\n    /* Dequeue */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Delete head node\n        front = front?.next\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Queue is empty\")\n        }\n        return front!.val\n    }\n\n    /* Convert linked list to Array and return */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_queue.js
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n    #front; // Front node #front\n    #rear; // Rear node #rear\n    #queSize = 0;\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n    }\n\n    /* Get the length of the queue */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Enqueue */\n    push(num) {\n        // Add num after the tail node\n        const node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (!this.#front) {\n            this.#front = node;\n            this.#rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            this.#rear.next = node;\n            this.#rear = node;\n        }\n        this.#queSize++;\n    }\n\n    /* Dequeue */\n    pop() {\n        const num = this.peek();\n        // Delete head node\n        this.#front = this.#front.next;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (this.size === 0) throw new Error('Queue is empty');\n        return this.#front.val;\n    }\n\n    /* Convert linked list to Array and return */\n    toArray() {\n        let node = this.#front;\n        const res = new Array(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.ts
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private front: ListNode | null; // Head node front\n    private rear: ListNode | null; // Tail node rear\n    private queSize: number = 0;\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n    }\n\n    /* Get the length of the queue */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Enqueue */\n    push(num: number): void {\n        // Add num after the tail node\n        const node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (!this.front) {\n            this.front = node;\n            this.rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            this.rear!.next = node;\n            this.rear = node;\n        }\n        this.queSize++;\n    }\n\n    /* Dequeue */\n    pop(): number {\n        const num = this.peek();\n        if (!this.front) throw new Error('Queue is empty');\n        // Delete head node\n        this.front = this.front.next;\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (this.size === 0) throw new Error('Queue is empty');\n        return this.front!.val;\n    }\n\n    /* Convert linked list to Array and return */\n    toArray(): number[] {\n        let node = this.front;\n        const res = new Array<number>(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.dart
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n  ListNode? _front; // Head node _front\n  ListNode? _rear; // Tail node _rear\n  int _queSize = 0; // Queue length\n\n  LinkedListQueue() {\n    _front = null;\n    _rear = null;\n  }\n\n  /* Get the length of the queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Enqueue */\n  void push(int _num) {\n    // Add _num after tail node\n    final node = ListNode(_num);\n    // If the queue is empty, make both front and rear point to the node\n    if (_front == null) {\n      _front = node;\n      _rear = node;\n    } else {\n      // If the queue is not empty, add the node after the tail node\n      _rear!.next = node;\n      _rear = node;\n    }\n    _queSize++;\n  }\n\n  /* Dequeue */\n  int pop() {\n    final int _num = peek();\n    // Delete head node\n    _front = _front!.next;\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (_queSize == 0) {\n      throw Exception('Queue is empty');\n    }\n    return _front!.val;\n  }\n\n  /* Convert linked list to Array and return */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> queue = [];\n    while (node != null) {\n      queue.add(node.val);\n      node = node.next;\n    }\n    return queue;\n  }\n}\n
linkedlist_queue.rs
/* Queue based on linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListQueue<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Head node front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Tail node rear\n    que_size: usize,                         // Queue length\n}\n\nimpl<T: Copy> LinkedListQueue<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* Get the length of the queue */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Check if the queue is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Enqueue */\n    pub fn push(&mut self, num: T) {\n        // Add num after the tail node\n        let new_rear = ListNode::new(num);\n        match self.rear.take() {\n            // If the queue is not empty, add the node after the tail node\n            Some(old_rear) => {\n                old_rear.borrow_mut().next = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n            // If the queue is empty, make both front and rear point to the node\n            None => {\n                self.front = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n        }\n        self.que_size += 1;\n    }\n\n    /* Dequeue */\n    pub fn pop(&mut self) -> Option<T> {\n        self.front.take().map(|old_front| {\n            match old_front.borrow_mut().next.take() {\n                Some(new_front) => {\n                    self.front = Some(new_front);\n                }\n                None => {\n                    self.rear.take();\n                }\n            }\n            self.que_size -= 1;\n            old_front.borrow().val\n        })\n    }\n\n    /* Return list for printing */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Convert linked list to Array and return */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n\n        res\n    }\n}\n
linkedlist_queue.c
/* Queue based on linked list implementation */\ntypedef struct {\n    ListNode *front, *rear;\n    int queSize;\n} LinkedListQueue;\n\n/* Constructor */\nLinkedListQueue *newLinkedListQueue() {\n    LinkedListQueue *queue = (LinkedListQueue *)malloc(sizeof(LinkedListQueue));\n    queue->front = NULL;\n    queue->rear = NULL;\n    queue->queSize = 0;\n    return queue;\n}\n\n/* Destructor */\nvoid delLinkedListQueue(LinkedListQueue *queue) {\n    // Free all nodes\n    while (queue->front != NULL) {\n        ListNode *tmp = queue->front;\n        queue->front = queue->front->next;\n        free(tmp);\n    }\n    // Free queue structure\n    free(queue);\n}\n\n/* Get the length of the queue */\nint size(LinkedListQueue *queue) {\n    return queue->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(LinkedListQueue *queue) {\n    return (size(queue) == 0);\n}\n\n/* Enqueue */\nvoid push(LinkedListQueue *queue, int num) {\n    // Add node at tail\n    ListNode *node = newListNode(num);\n    // If the queue is empty, make both front and rear point to the node\n    if (queue->front == NULL) {\n        queue->front = node;\n        queue->rear = node;\n    }\n    // If the queue is not empty, add the node after the tail node\n    else {\n        queue->rear->next = node;\n        queue->rear = node;\n    }\n    queue->queSize++;\n}\n\n/* Return list for printing */\nint peek(LinkedListQueue *queue) {\n    assert(size(queue) && queue->front);\n    return queue->front->val;\n}\n\n/* Dequeue */\nint pop(LinkedListQueue *queue) {\n    int num = peek(queue);\n    ListNode *tmp = queue->front;\n    queue->front = queue->front->next;\n    free(tmp);\n    queue->queSize--;\n    return num;\n}\n\n/* Print queue */\nvoid printLinkedListQueue(LinkedListQueue *queue) {\n    int *arr = malloc(sizeof(int) * queue->queSize);\n    // Copy data from list to array\n    int i;\n    ListNode *node;\n    for (i = 0, node = queue->front; i < queue->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, queue->queSize);\n    free(arr);\n}\n
linkedlist_queue.kt
/* Queue based on linked list implementation */\nclass LinkedListQueue(\n    // Head node front, tail node rear\n    private var front: ListNode? = null,\n    private var rear: ListNode? = null,\n    private var queSize: Int = 0\n) {\n\n    /* Get the length of the queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the queue is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Enqueue */\n    fun push(num: Int) {\n        // Add num after the tail node\n        val node = ListNode(num)\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node\n            rear = node\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            rear?.next = node\n            rear = node\n        }\n        queSize++\n    }\n\n    /* Dequeue */\n    fun pop(): Int {\n        val num = peek()\n        // Delete head node\n        front = front?.next\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Convert linked list to Array and return */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_queue.rb
### Queue based on linked list ###\nclass LinkedListQueue\n  ### Get queue length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @front = nil  # Head node front\n    @rear = nil   # Tail node rear\n    @size = 0\n  end\n\n  ### Check if queue is empty ###\n  def is_empty?\n    @front.nil?\n  end\n\n  ### Enqueue ###\n  def push(num)\n    # Add num after the tail node\n    node = ListNode.new(num)\n\n    # If queue is empty, set both front and rear to this node\n    if @front.nil?\n      @front = node\n      @rear = node\n    # If queue is not empty, add this node after rear\n    else\n      @rear.next = node\n      @rear = node\n    end\n\n    @size += 1\n  end\n\n  ### Dequeue ###\n  def pop\n    num = peek\n    # Delete head node\n    @front = @front.next\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek\n    raise IndexError, 'Queue is empty' if is_empty?\n\n    @front.val\n  end\n\n  ### Convert linked list to Array and return ###\n  def to_array\n    queue = []\n    temp = @front\n    while temp\n      queue << temp.val\n      temp = temp.next\n    end\n    queue\n  end\nend\n
","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

Deleting the first element in an array has a time complexity of \\(O(n)\\), which would make the dequeue operation inefficient. However, we can use the following clever method to avoid this problem.

We can use a variable front to point to the index of the front element and maintain a variable size to record the queue length. We define rear = front + size, which calculates the position right after the rear element.

Based on this design, the valid interval containing elements in the array is [front, rear - 1]. The implementation methods for various operations are shown in Figure 5-6:

  • Enqueue operation: Assign the input element to the rear index and increase size by 1.
  • Dequeue operation: Simply increase front by 1 and decrease size by 1.

As you can see, both enqueue and dequeue operations require only one operation, with a time complexity of \\(O(1)\\).

ArrayQueuepush()pop()

Figure 5-6   Enqueue and dequeue operations in array implementation of queue

You may notice a problem: as we continuously enqueue and dequeue, both front and rear move to the right. When they reach the end of the array, they cannot continue moving. To solve this problem, we can treat the array as a \"circular array\" with head and tail connected.

For a circular array, we need to let front or rear wrap around to the beginning of the array when they cross the end. This periodic pattern can be implemented using the \"modulo operation,\" as shown in the code below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_queue.py
class ArrayQueue:\n    \"\"\"Queue based on circular array implementation\"\"\"\n\n    def __init__(self, size: int):\n        \"\"\"Constructor\"\"\"\n        self._nums: list[int] = [0] * size  # Array for storing queue elements\n        self._front: int = 0  # Front pointer, points to the front of the queue element\n        self._size: int = 0  # Queue length\n\n    def capacity(self) -> int:\n        \"\"\"Get the capacity of the queue\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Get the length of the queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Enqueue\"\"\"\n        if self._size == self.capacity():\n            raise IndexError(\"Queue is full\")\n        # Calculate rear pointer, points to rear index + 1\n        # Use modulo operation to wrap rear around to the head after passing the tail of the array\n        rear: int = (self._front + self._size) % self.capacity()\n        # Add num to the rear of the queue\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Dequeue\"\"\"\n        num: int = self.peek()\n        # Front pointer moves one position backward, if it passes the tail, return to the head of the array\n        self._front = (self._front + 1) % self.capacity()\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Queue is empty\")\n        return self._nums[self._front]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Return list for printing\"\"\"\n        res = [0] * self.size()\n        j: int = self._front\n        for i in range(self.size()):\n            res[i] = self._nums[(j % self.capacity())]\n            j += 1\n        return res\n
array_queue.cpp
/* Queue based on circular array implementation */\nclass ArrayQueue {\n  private:\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Queue length\n    int queCapacity; // Queue capacity\n\n  public:\n    ArrayQueue(int capacity) {\n        // Initialize array\n        nums = new int[capacity];\n        queCapacity = capacity;\n        front = queSize = 0;\n    }\n\n    ~ArrayQueue() {\n        delete[] nums;\n    }\n\n    /* Get the capacity of the queue */\n    int capacity() {\n        return queCapacity;\n    }\n\n    /* Get the length of the queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue */\n    void push(int num) {\n        if (queSize == queCapacity) {\n            cout << \"Queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % queCapacity;\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    int pop() {\n        int num = peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % queCapacity;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peek() {\n        if (isEmpty())\n            throw out_of_range(\"Queue is empty\");\n        return nums[front];\n    }\n\n    /* Convert array to Vector and return */\n    vector<int> toVector() {\n        // Elements enqueue\n        vector<int> arr(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            arr[i] = nums[j % queCapacity];\n        }\n        return arr;\n    }\n};\n
array_queue.java
/* Queue based on circular array implementation */\nclass ArrayQueue {\n    private int[] nums; // Array for storing queue elements\n    private int front; // Front pointer, points to the front of the queue element\n    private int queSize; // Queue length\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Get the length of the queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    public void push(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % capacity();\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int pop() {\n        int num = peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Return array */\n    public int[] toArray() {\n        // Elements enqueue\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % capacity()];\n        }\n        return res;\n    }\n}\n
array_queue.cs
/* Queue based on circular array implementation */\nclass ArrayQueue {\n    int[] nums;  // Array for storing queue elements\n    int front;   // Front pointer, points to the front of the queue element\n    int queSize; // Queue length\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Get the length of the queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    public void Push(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % Capacity();\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int Pop() {\n        int num = Peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % Capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return nums[front];\n    }\n\n    /* Return array */\n    public int[] ToArray() {\n        // Elements enqueue\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % this.Capacity()];\n        }\n        return res;\n    }\n}\n
array_queue.go
/* Queue based on circular array implementation */\ntype arrayQueue struct {\n    nums        []int // Array for storing queue elements\n    front       int   // Front pointer, points to the front of the queue element\n    queSize     int   // Queue length\n    queCapacity int   // Queue capacity (maximum number of elements)\n}\n\n/* Access front of the queue element */\nfunc newArrayQueue(queCapacity int) *arrayQueue {\n    return &arrayQueue{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* Get the length of the queue */\nfunc (q *arrayQueue) size() int {\n    return q.queSize\n}\n\n/* Check if the queue is empty */\nfunc (q *arrayQueue) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Enqueue */\nfunc (q *arrayQueue) push(num int) {\n    // When rear == queCapacity, queue is full\n    if q.queSize == q.queCapacity {\n        return\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    rear := (q.front + q.queSize) % q.queCapacity\n    // Front pointer moves one position backward\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Dequeue */\nfunc (q *arrayQueue) pop() any {\n    num := q.peek()\n    if num == nil {\n        return nil\n    }\n\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    q.front = (q.front + 1) % q.queCapacity\n    q.queSize--\n    return num\n}\n\n/* Return list for printing */\nfunc (q *arrayQueue) peek() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Get Slice for printing */\nfunc (q *arrayQueue) toSlice() []int {\n    rear := (q.front + q.queSize)\n    if rear >= q.queCapacity {\n        rear %= q.queCapacity\n        return append(q.nums[q.front:], q.nums[:rear]...)\n    }\n    return q.nums[q.front:rear]\n}\n
array_queue.swift
/* Queue based on circular array implementation */\nclass ArrayQueue {\n    private var nums: [Int] // Array for storing queue elements\n    private var front: Int // Front pointer, points to the front of the queue element\n    private var _size: Int // Queue length\n\n    init(capacity: Int) {\n        // Initialize array\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Get the capacity of the queue */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Get the length of the queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue */\n    func push(num: Int) {\n        if size() == capacity() {\n            print(\"Queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        let rear = (front + size()) % capacity()\n        // Front pointer moves one position backward\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Dequeue */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity()\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Queue is empty\")\n        }\n        return nums[front]\n    }\n\n    /* Return array */\n    func toArray() -> [Int] {\n        // Elements enqueue\n        (front ..< front + size()).map { nums[$0 % capacity()] }\n    }\n}\n
array_queue.js
/* Queue based on circular array implementation */\nclass ArrayQueue {\n    #nums; // Array for storing queue elements\n    #front = 0; // Front pointer, points to the front of the queue element\n    #queSize = 0; // Queue length\n\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n    }\n\n    /* Get the capacity of the queue */\n    get capacity() {\n        return this.#nums.length;\n    }\n\n    /* Get the length of the queue */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Enqueue */\n    push(num) {\n        if (this.size === this.capacity) {\n            console.log('Queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        const rear = (this.#front + this.size) % this.capacity;\n        // Front pointer moves one position backward\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Dequeue */\n    pop() {\n        const num = this.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        this.#front = (this.#front + 1) % this.capacity;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (this.isEmpty()) throw new Error('Queue is empty');\n        return this.#nums[this.#front];\n    }\n\n    /* Return Array */\n    toArray() {\n        // Elements enqueue\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.#front; i < this.size; i++, j++) {\n            arr[i] = this.#nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
array_queue.ts
/* Queue based on circular array implementation */\nclass ArrayQueue {\n    private nums: number[]; // Array for storing queue elements\n    private front: number; // Front pointer, points to the front of the queue element\n    private queSize: number; // Queue length\n\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = this.queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    get capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Get the length of the queue */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Enqueue */\n    push(num: number): void {\n        if (this.size === this.capacity) {\n            console.log('Queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        const rear = (this.front + this.queSize) % this.capacity;\n        // Front pointer moves one position backward\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Dequeue */\n    pop(): number {\n        const num = this.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        this.front = (this.front + 1) % this.capacity;\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (this.isEmpty()) throw new Error('Queue is empty');\n        return this.nums[this.front];\n    }\n\n    /* Return Array */\n    toArray(): number[] {\n        // Elements enqueue\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.front; i < this.size; i++, j++) {\n            arr[i] = this.nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
array_queue.dart
/* Queue based on circular array implementation */\nclass ArrayQueue {\n  late List<int> _nums; // Array for storing queue elements\n  late int _front; // Front pointer, points to the front of the queue element\n  late int _queSize; // Queue length\n\n  ArrayQueue(int capacity) {\n    _nums = List.filled(capacity, 0);\n    _front = _queSize = 0;\n  }\n\n  /* Get the capacity of the queue */\n  int capaCity() {\n    return _nums.length;\n  }\n\n  /* Get the length of the queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Enqueue */\n  void push(int _num) {\n    if (_queSize == capaCity()) {\n      throw Exception(\"Queue is full\");\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    int rear = (_front + _queSize) % capaCity();\n    // Add _num to queue rear\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Dequeue */\n  int pop() {\n    int _num = peek();\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    _front = (_front + 1) % capaCity();\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"Queue is empty\");\n    }\n    return _nums[_front];\n  }\n\n  /* Return Array */\n  List<int> toArray() {\n    // Elements enqueue\n    final List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[j % capaCity()];\n    }\n    return res;\n  }\n}\n
array_queue.rs
/* Queue based on circular array implementation */\nstruct ArrayQueue<T> {\n    nums: Vec<T>,      // Array for storing queue elements\n    front: i32,        // Front pointer, points to the front of the queue element\n    que_size: i32,     // Queue length\n    que_capacity: i32, // Queue capacity\n}\n\nimpl<T: Copy + Default> ArrayQueue<T> {\n    /* Constructor */\n    fn new(capacity: i32) -> ArrayQueue<T> {\n        ArrayQueue {\n            nums: vec![T::default(); capacity as usize],\n            front: 0,\n            que_size: 0,\n            que_capacity: capacity,\n        }\n    }\n\n    /* Get the capacity of the queue */\n    fn capacity(&self) -> i32 {\n        self.que_capacity\n    }\n\n    /* Get the length of the queue */\n    fn size(&self) -> i32 {\n        self.que_size\n    }\n\n    /* Check if the queue is empty */\n    fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Enqueue */\n    fn push(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        let rear = (self.front + self.que_size) % self.que_capacity;\n        // Front pointer moves one position backward\n        self.nums[rear as usize] = num;\n        self.que_size += 1;\n    }\n\n    /* Dequeue */\n    fn pop(&mut self) -> T {\n        let num = self.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        self.front = (self.front + 1) % self.que_capacity;\n        self.que_size -= 1;\n        num\n    }\n\n    /* Return list for printing */\n    fn peek(&self) -> T {\n        if self.is_empty() {\n            panic!(\"index out of bounds\");\n        }\n        self.nums[self.front as usize]\n    }\n\n    /* Return array */\n    fn to_vector(&self) -> Vec<T> {\n        let cap = self.que_capacity;\n        let mut j = self.front;\n        let mut arr = vec![T::default(); cap as usize];\n        for i in 0..self.que_size {\n            arr[i as usize] = self.nums[(j % cap) as usize];\n            j += 1;\n        }\n        arr\n    }\n}\n
array_queue.c
/* Queue based on circular array implementation */\ntypedef struct {\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Rear pointer, points to rear + 1\n    int queCapacity; // Queue capacity\n} ArrayQueue;\n\n/* Constructor */\nArrayQueue *newArrayQueue(int capacity) {\n    ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue));\n    // Initialize array\n    queue->queCapacity = capacity;\n    queue->nums = (int *)malloc(sizeof(int) * queue->queCapacity);\n    queue->front = queue->queSize = 0;\n    return queue;\n}\n\n/* Destructor */\nvoid delArrayQueue(ArrayQueue *queue) {\n    free(queue->nums);\n    free(queue);\n}\n\n/* Get the capacity of the queue */\nint capacity(ArrayQueue *queue) {\n    return queue->queCapacity;\n}\n\n/* Get the length of the queue */\nint size(ArrayQueue *queue) {\n    return queue->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(ArrayQueue *queue) {\n    return queue->queSize == 0;\n}\n\n/* Return list for printing */\nint peek(ArrayQueue *queue) {\n    assert(size(queue) != 0);\n    return queue->nums[queue->front];\n}\n\n/* Enqueue */\nvoid push(ArrayQueue *queue, int num) {\n    if (size(queue) == capacity(queue)) {\n        printf(\"Queue is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    int rear = (queue->front + queue->queSize) % queue->queCapacity;\n    // Front pointer moves one position backward\n    queue->nums[rear] = num;\n    queue->queSize++;\n}\n\n/* Dequeue */\nint pop(ArrayQueue *queue) {\n    int num = peek(queue);\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    queue->front = (queue->front + 1) % queue->queCapacity;\n    queue->queSize--;\n    return num;\n}\n\n/* Return array for printing */\nint *toArray(ArrayQueue *queue, int *queSize) {\n    *queSize = queue->queSize;\n    int *res = (int *)calloc(queue->queSize, sizeof(int));\n    int j = queue->front;\n    for (int i = 0; i < queue->queSize; i++) {\n        res[i] = queue->nums[j % queue->queCapacity];\n        j++;\n    }\n    return res;\n}\n
array_queue.kt
/* Queue based on circular array implementation */\nclass ArrayQueue(capacity: Int) {\n    private val nums: IntArray = IntArray(capacity) // Array for storing queue elements\n    private var front: Int = 0 // Front pointer, points to the front of the queue element\n    private var queSize: Int = 0 // Queue length\n\n    /* Get the capacity of the queue */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Get the length of the queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the queue is empty */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Enqueue */\n    fun push(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        val rear = (front + queSize) % capacity()\n        // Front pointer moves one position backward\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Dequeue */\n    fun pop(): Int {\n        val num = peek()\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity()\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Return array */\n    fun toArray(): IntArray {\n        // Elements enqueue\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[j % capacity()]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
array_queue.rb
### Queue based on circular array ###\nclass ArrayQueue\n  ### Get queue length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize(size)\n    @nums = Array.new(size, 0) # Array for storing queue elements\n    @front = 0 # Front pointer, points to the front of the queue element\n    @size = 0 # Queue length\n  end\n\n  ### Get queue capacity ###\n  def capacity\n    @nums.length\n  end\n\n  ### Check if queue is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue ###\n  def push(num)\n    raise IndexError, 'Queue is full' if size == capacity\n\n    # Use modulo operation to wrap rear around to the head after passing the tail of the array\n    # Add num to the rear of the queue\n    rear = (@front + size) % capacity\n    # Front pointer moves one position backward\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Dequeue ###\n  def pop\n    num = peek\n    # Move front pointer backward by one position, if it passes the tail, return to array head\n    @front = (@front + 1) % capacity\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek\n    raise IndexError, 'Queue is empty' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Return list for printing ###\n  def to_array\n    res = Array.new(size, 0)\n    j = @front\n\n    for i in 0...size\n      res[i] = @nums[j % capacity]\n      j += 1\n    end\n\n    res\n  end\nend\n

The queue implemented above still has limitations: its length is immutable. However, this problem is not difficult to solve. We can replace the array with a dynamic array to introduce an expansion mechanism. Interested readers can try to implement this themselves.

The comparison conclusions for the two implementations are consistent with those for stacks and will not be repeated here.

","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#523-typical-applications-of-queue","level":2,"title":"5.2.3   Typical Applications of Queue","text":"
  • Taobao orders. After shoppers place orders, the orders are added to a queue, and the system subsequently processes the orders in the queue according to their sequence. During Double Eleven, massive orders are generated in a short time, and high concurrency becomes a key challenge that engineers need to tackle.
  • Various to-do tasks. Any scenario that needs to implement \"first come, first served\" functionality, such as a printer's task queue or a restaurant's order queue, can effectively maintain the processing order using queues.
","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/stack/","level":1,"title":"5.1   Stack","text":"

A stack is a linear data structure that follows the Last In First Out (LIFO) logic.

We can compare a stack to a pile of plates on a table. If we specify that only one plate can be moved at a time, then to get the bottom plate, we must first remove the plates above it one by one. If we replace the plates with various types of elements (such as integers, characters, objects, etc.), we get the stack data structure.

As shown in Figure 5-1, we call the top of the stacked elements the \"top\" and the bottom the \"base.\" The operation of adding an element to the top is called \"push,\" and the operation of removing the top element is called \"pop.\"

Figure 5-1   LIFO rule of stack

","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511-common-stack-operations","level":2,"title":"5.1.1   Common Stack Operations","text":"

The common operations on a stack are shown in Table 5-1. The specific method names depend on the programming language used. Here, we use the common naming convention of push(), pop(), and peek().

Table 5-1   Efficiency of Stack Operations

Method Description Time Complexity push() Push element onto stack (add to top) \\(O(1)\\) pop() Pop top element from stack \\(O(1)\\) peek() Access top element \\(O(1)\\)

Typically, we can directly use the built-in stack class provided by the programming language. However, some languages may not provide a dedicated stack class. In these cases, we can use the language's \"array\" or \"linked list\" as a stack and ignore operations unrelated to the stack in the program logic.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby stack.py
# Initialize stack\n# Python does not have a built-in stack class, can use list as a stack\nstack: list[int] = []\n\n# Push elements\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n# Access top element\npeek: int = stack[-1]\n\n# Pop element\npop: int = stack.pop()\n\n# Get stack length\nsize: int = len(stack)\n\n# Check if empty\nis_empty: bool = len(stack) == 0\n
stack.cpp
/* Initialize stack */\nstack<int> stack;\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nint top = stack.top();\n\n/* Pop element */\nstack.pop(); // No return value\n\n/* Get stack length */\nint size = stack.size();\n\n/* Check if empty */\nbool empty = stack.empty();\n
stack.java
/* Initialize stack */\nStack<Integer> stack = new Stack<>();\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nint peek = stack.peek();\n\n/* Pop element */\nint pop = stack.pop();\n\n/* Get stack length */\nint size = stack.size();\n\n/* Check if empty */\nboolean isEmpty = stack.isEmpty();\n
stack.cs
/* Initialize stack */\nStack<int> stack = new();\n\n/* Push elements */\nstack.Push(1);\nstack.Push(3);\nstack.Push(2);\nstack.Push(5);\nstack.Push(4);\n\n/* Access top element */\nint peek = stack.Peek();\n\n/* Pop element */\nint pop = stack.Pop();\n\n/* Get stack length */\nint size = stack.Count;\n\n/* Check if empty */\nbool isEmpty = stack.Count == 0;\n
stack_test.go
/* Initialize stack */\n// In Go, it is recommended to use Slice as a stack\nvar stack []int\n\n/* Push elements */\nstack = append(stack, 1)\nstack = append(stack, 3)\nstack = append(stack, 2)\nstack = append(stack, 5)\nstack = append(stack, 4)\n\n/* Access top element */\npeek := stack[len(stack)-1]\n\n/* Pop element */\npop := stack[len(stack)-1]\nstack = stack[:len(stack)-1]\n\n/* Get stack length */\nsize := len(stack)\n\n/* Check if empty */\nisEmpty := len(stack) == 0\n
stack.swift
/* Initialize stack */\n// Swift does not have a built-in stack class, can use Array as a stack\nvar stack: [Int] = []\n\n/* Push elements */\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n/* Access top element */\nlet peek = stack.last!\n\n/* Pop element */\nlet pop = stack.removeLast()\n\n/* Get stack length */\nlet size = stack.count\n\n/* Check if empty */\nlet isEmpty = stack.isEmpty\n
stack.js
/* Initialize stack */\n// JavaScript does not have a built-in stack class, can use Array as a stack\nconst stack = [];\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nconst peek = stack[stack.length-1];\n\n/* Pop element */\nconst pop = stack.pop();\n\n/* Get stack length */\nconst size = stack.length;\n\n/* Check if empty */\nconst is_empty = stack.length === 0;\n
stack.ts
/* Initialize stack */\n// TypeScript does not have a built-in stack class, can use Array as a stack\nconst stack: number[] = [];\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nconst peek = stack[stack.length - 1];\n\n/* Pop element */\nconst pop = stack.pop();\n\n/* Get stack length */\nconst size = stack.length;\n\n/* Check if empty */\nconst is_empty = stack.length === 0;\n
stack.dart
/* Initialize stack */\n// Dart does not have a built-in stack class, can use List as a stack\nList<int> stack = [];\n\n/* Push elements */\nstack.add(1);\nstack.add(3);\nstack.add(2);\nstack.add(5);\nstack.add(4);\n\n/* Access top element */\nint peek = stack.last;\n\n/* Pop element */\nint pop = stack.removeLast();\n\n/* Get stack length */\nint size = stack.length;\n\n/* Check if empty */\nbool isEmpty = stack.isEmpty;\n
stack.rs
/* Initialize stack */\n// Use Vec as a stack\nlet mut stack: Vec<i32> = Vec::new();\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nlet top = stack.last().unwrap();\n\n/* Pop element */\nlet pop = stack.pop().unwrap();\n\n/* Get stack length */\nlet size = stack.len();\n\n/* Check if empty */\nlet is_empty = stack.is_empty();\n
stack.c
// C does not provide a built-in stack\n
stack.kt
/* Initialize stack */\nval stack = Stack<Int>()\n\n/* Push elements */\nstack.push(1)\nstack.push(3)\nstack.push(2)\nstack.push(5)\nstack.push(4)\n\n/* Access top element */\nval peek = stack.peek()\n\n/* Pop element */\nval pop = stack.pop()\n\n/* Get stack length */\nval size = stack.size\n\n/* Check if empty */\nval isEmpty = stack.isEmpty()\n
stack.rb
# Initialize stack\n# Ruby does not have a built-in stack class, can use Array as a stack\nstack = []\n\n# Push elements\nstack << 1\nstack << 3\nstack << 2\nstack << 5\nstack << 4\n\n# Access top element\npeek = stack.last\n\n# Pop element\npop = stack.pop\n\n# Get stack length\nsize = stack.length\n\n# Check if empty\nis_empty = stack.empty?\n
Code Visualization

Full Screen >

","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512-stack-implementation","level":2,"title":"5.1.2   Stack Implementation","text":"

To gain a deeper understanding of how a stack operates, let's try implementing a stack class ourselves.

A stack follows the LIFO principle, so we can only add or remove elements at the top. However, both arrays and linked lists allow adding and removing elements at any position. Therefore, a stack can be viewed as a restricted array or linked list. In other words, we can \"shield\" some irrelevant operations of arrays or linked lists so that their external logic conforms to the characteristics of a stack.

","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1-linked-list-implementation","level":3,"title":"1.   Linked List Implementation","text":"

When implementing a stack using a linked list, we can treat the head node of the linked list as the top of the stack and the tail node as the base.

As shown in Figure 5-2, for the push operation, we simply insert an element at the head of the linked list. This node insertion method is called the \"head insertion method.\" For the pop operation, we just need to remove the head node from the linked list.

LinkedListStackpush()pop()

Figure 5-2   Push and pop operations in linked list implementation of stack

Below is sample code for implementing a stack based on a linked list:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_stack.py
class LinkedListStack:\n    \"\"\"Stack based on linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._peek: ListNode | None = None\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Get the length of the stack\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the stack is empty\"\"\"\n        return self._size == 0\n\n    def push(self, val: int):\n        \"\"\"Push\"\"\"\n        node = ListNode(val)\n        node.next = self._peek\n        self._peek = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Pop\"\"\"\n        num = self.peek()\n        self._peek = self._peek.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access top of the stack element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._peek.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Convert to list for printing\"\"\"\n        arr = []\n        node = self._peek\n        while node:\n            arr.append(node.val)\n            node = node.next\n        arr.reverse()\n        return arr\n
linkedlist_stack.cpp
/* Stack based on linked list implementation */\nclass LinkedListStack {\n  private:\n    ListNode *stackTop; // Use head node as stack top\n    int stkSize;        // Stack length\n\n  public:\n    LinkedListStack() {\n        stackTop = nullptr;\n        stkSize = 0;\n    }\n\n    ~LinkedListStack() {\n        // Traverse linked list to delete nodes and free memory\n        freeMemoryLinkedList(stackTop);\n    }\n\n    /* Get the length of the stack */\n    int size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    void push(int num) {\n        ListNode *node = new ListNode(num);\n        node->next = stackTop;\n        stackTop = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    int pop() {\n        int num = top();\n        ListNode *tmp = stackTop;\n        stackTop = stackTop->next;\n        // Free memory\n        delete tmp;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"Stack is empty\");\n        return stackTop->val;\n    }\n\n    /* Convert List to Array and return */\n    vector<int> toVector() {\n        ListNode *node = stackTop;\n        vector<int> res(size());\n        for (int i = res.size() - 1; i >= 0; i--) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_stack.java
/* Stack based on linked list implementation */\nclass LinkedListStack {\n    private ListNode stackPeek; // Use head node as stack top\n    private int stkSize = 0; // Stack length\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    public int size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    public void push(int num) {\n        ListNode node = new ListNode(num);\n        node.next = stackPeek;\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    public int pop() {\n        int num = peek();\n        stackPeek = stackPeek.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stackPeek.val;\n    }\n\n    /* Convert List to Array and return */\n    public int[] toArray() {\n        ListNode node = stackPeek;\n        int[] res = new int[size()];\n        for (int i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.cs
/* Stack based on linked list implementation */\nclass LinkedListStack {\n    ListNode? stackPeek;  // Use head node as stack top\n    int stkSize = 0;   // Stack length\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    public int Size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Push */\n    public void Push(int num) {\n        ListNode node = new(num) {\n            next = stackPeek\n        };\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    public int Pop() {\n        int num = Peek();\n        stackPeek = stackPeek!.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stackPeek!.val;\n    }\n\n    /* Convert List to Array and return */\n    public int[] ToArray() {\n        if (stackPeek == null)\n            return [];\n\n        ListNode? node = stackPeek;\n        int[] res = new int[Size()];\n        for (int i = res.Length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.go
/* Stack based on linked list implementation */\ntype linkedListStack struct {\n    // Use built-in package list to implement stack\n    data *list.List\n}\n\n/* Access top of the stack element */\nfunc newLinkedListStack() *linkedListStack {\n    return &linkedListStack{\n        data: list.New(),\n    }\n}\n\n/* Push */\nfunc (s *linkedListStack) push(value int) {\n    s.data.PushBack(value)\n}\n\n/* Pop */\nfunc (s *linkedListStack) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Return list for printing */\nfunc (s *linkedListStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* Get the length of the stack */\nfunc (s *linkedListStack) size() int {\n    return s.data.Len()\n}\n\n/* Check if the stack is empty */\nfunc (s *linkedListStack) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListStack) toList() *list.List {\n    return s.data\n}\n
linkedlist_stack.swift
/* Stack based on linked list implementation */\nclass LinkedListStack {\n    private var _peek: ListNode? // Use head node as stack top\n    private var _size: Int // Stack length\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the stack */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the stack is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Push */\n    func push(num: Int) {\n        let node = ListNode(x: num)\n        node.next = _peek\n        _peek = node\n        _size += 1\n    }\n\n    /* Pop */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        _peek = _peek?.next\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return _peek!.val\n    }\n\n    /* Convert List to Array and return */\n    func toArray() -> [Int] {\n        var node = _peek\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices.reversed() {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_stack.js
/* Stack based on linked list implementation */\nclass LinkedListStack {\n    #stackPeek; // Use head node as stack top\n    #stkSize = 0; // Stack length\n\n    constructor() {\n        this.#stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    get size() {\n        return this.#stkSize;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Push */\n    push(num) {\n        const node = new ListNode(num);\n        node.next = this.#stackPeek;\n        this.#stackPeek = node;\n        this.#stkSize++;\n    }\n\n    /* Pop */\n    pop() {\n        const num = this.peek();\n        this.#stackPeek = this.#stackPeek.next;\n        this.#stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (!this.#stackPeek) throw new Error('Stack is empty');\n        return this.#stackPeek.val;\n    }\n\n    /* Convert linked list to Array and return */\n    toArray() {\n        let node = this.#stackPeek;\n        const res = new Array(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.ts
/* Stack based on linked list implementation */\nclass LinkedListStack {\n    private stackPeek: ListNode | null; // Use head node as stack top\n    private stkSize: number = 0; // Stack length\n\n    constructor() {\n        this.stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    get size(): number {\n        return this.stkSize;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Push */\n    push(num: number): void {\n        const node = new ListNode(num);\n        node.next = this.stackPeek;\n        this.stackPeek = node;\n        this.stkSize++;\n    }\n\n    /* Pop */\n    pop(): number {\n        const num = this.peek();\n        if (!this.stackPeek) throw new Error('Stack is empty');\n        this.stackPeek = this.stackPeek.next;\n        this.stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (!this.stackPeek) throw new Error('Stack is empty');\n        return this.stackPeek.val;\n    }\n\n    /* Convert linked list to Array and return */\n    toArray(): number[] {\n        let node = this.stackPeek;\n        const res = new Array<number>(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.dart
/* Stack implemented based on linked list class */\nclass LinkedListStack {\n  ListNode? _stackPeek; // Use head node as stack top\n  int _stkSize = 0; // Stack length\n\n  LinkedListStack() {\n    _stackPeek = null;\n  }\n\n  /* Get the length of the stack */\n  int size() {\n    return _stkSize;\n  }\n\n  /* Check if the stack is empty */\n  bool isEmpty() {\n    return _stkSize == 0;\n  }\n\n  /* Push */\n  void push(int _num) {\n    final ListNode node = ListNode(_num);\n    node.next = _stackPeek;\n    _stackPeek = node;\n    _stkSize++;\n  }\n\n  /* Pop */\n  int pop() {\n    final int _num = peek();\n    _stackPeek = _stackPeek!.next;\n    _stkSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (_stackPeek == null) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stackPeek!.val;\n  }\n\n  /* Convert linked list to List and return */\n  List<int> toList() {\n    ListNode? node = _stackPeek;\n    List<int> list = [];\n    while (node != null) {\n      list.add(node.val);\n      node = node.next;\n    }\n    list = list.reversed.toList();\n    return list;\n  }\n}\n
linkedlist_stack.rs
/* Stack based on linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListStack<T> {\n    stack_peek: Option<Rc<RefCell<ListNode<T>>>>, // Use head node as stack top\n    stk_size: usize,                              // Stack length\n}\n\nimpl<T: Copy> LinkedListStack<T> {\n    pub fn new() -> Self {\n        Self {\n            stack_peek: None,\n            stk_size: 0,\n        }\n    }\n\n    /* Get the length of the stack */\n    pub fn size(&self) -> usize {\n        return self.stk_size;\n    }\n\n    /* Check if the stack is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.size() == 0;\n    }\n\n    /* Push */\n    pub fn push(&mut self, num: T) {\n        let node = ListNode::new(num);\n        node.borrow_mut().next = self.stack_peek.take();\n        self.stack_peek = Some(node);\n        self.stk_size += 1;\n    }\n\n    /* Pop */\n    pub fn pop(&mut self) -> Option<T> {\n        self.stack_peek.take().map(|old_head| {\n            self.stack_peek = old_head.borrow_mut().next.take();\n            self.stk_size -= 1;\n\n            old_head.borrow().val\n        })\n    }\n\n    /* Return list for printing */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.stack_peek.as_ref()\n    }\n\n    /* Convert List to Array and return */\n    pub fn to_array(&self) -> Vec<T> {\n        fn _to_array<T: Sized + Copy>(head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n            if let Some(node) = head {\n                let mut nums = _to_array(node.borrow().next.as_ref());\n                nums.push(node.borrow().val);\n                return nums;\n            }\n            return Vec::new();\n        }\n\n        _to_array(self.peek())\n    }\n}\n
linkedlist_stack.c
/* Stack based on linked list implementation */\ntypedef struct {\n    ListNode *top; // Use head node as stack top\n    int size;      // Stack length\n} LinkedListStack;\n\n/* Constructor */\nLinkedListStack *newLinkedListStack() {\n    LinkedListStack *s = malloc(sizeof(LinkedListStack));\n    s->top = NULL;\n    s->size = 0;\n    return s;\n}\n\n/* Destructor */\nvoid delLinkedListStack(LinkedListStack *s) {\n    while (s->top) {\n        ListNode *n = s->top->next;\n        free(s->top);\n        s->top = n;\n    }\n    free(s);\n}\n\n/* Get the length of the stack */\nint size(LinkedListStack *s) {\n    return s->size;\n}\n\n/* Check if the stack is empty */\nbool isEmpty(LinkedListStack *s) {\n    return size(s) == 0;\n}\n\n/* Push */\nvoid push(LinkedListStack *s, int num) {\n    ListNode *node = (ListNode *)malloc(sizeof(ListNode));\n    node->next = s->top; // Update new node's pointer field\n    node->val = num;     // Update new node's data field\n    s->top = node;       // Update stack top\n    s->size++;           // Update stack size\n}\n\n/* Return list for printing */\nint peek(LinkedListStack *s) {\n    if (s->size == 0) {\n        printf(\"Stack is empty\\n\");\n        return INT_MAX;\n    }\n    return s->top->val;\n}\n\n/* Pop */\nint pop(LinkedListStack *s) {\n    int val = peek(s);\n    ListNode *tmp = s->top;\n    s->top = s->top->next;\n    // Free memory\n    free(tmp);\n    s->size--;\n    return val;\n}\n
linkedlist_stack.kt
/* Stack based on linked list implementation */\nclass LinkedListStack(\n    private var stackPeek: ListNode? = null, // Use head node as stack top\n    private var stkSize: Int = 0 // Stack length\n) {\n\n    /* Get the length of the stack */\n    fun size(): Int {\n        return stkSize\n    }\n\n    /* Check if the stack is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Push */\n    fun push(num: Int) {\n        val node = ListNode(num)\n        node.next = stackPeek\n        stackPeek = node\n        stkSize++\n    }\n\n    /* Pop */\n    fun pop(): Int? {\n        val num = peek()\n        stackPeek = stackPeek?.next\n        stkSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int? {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stackPeek?._val\n    }\n\n    /* Convert List to Array and return */\n    fun toArray(): IntArray {\n        var node = stackPeek\n        val res = IntArray(size())\n        for (i in res.size - 1 downTo 0) {\n            res[i] = node?._val!!\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_stack.rb
### Stack based on linked list ###\nclass LinkedListStack\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @size = 0\n  end\n\n  ### Check if stack is empty ###\n  def is_empty?\n    @peek.nil?\n  end\n\n  ### Push ###\n  def push(val)\n    node = ListNode.new(val)\n    node.next = @peek\n    @peek = node\n    @size += 1\n  end\n\n  ### Pop ###\n  def pop\n    num = peek\n    @peek = @peek.next\n    @size -= 1\n    num\n  end\n\n  ### Access top element ###\n  def peek\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @peek.val\n  end\n\n  ### Convert linked list to Array and return ###\n  def to_array\n    arr = []\n    node = @peek\n    while node\n      arr << node.val\n      node = node.next\n    end\n    arr.reverse\n  end\nend\n
","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

When implementing a stack using an array, we can treat the end of the array as the top of the stack. As shown in Figure 5-3, push and pop operations correspond to adding and removing elements at the end of the array, both with a time complexity of \\(O(1)\\).

ArrayStackpush()pop()

Figure 5-3   Push and pop operations in array implementation of stack

Since elements pushed onto the stack may increase continuously, we can use a dynamic array, which eliminates the need to handle array expansion ourselves. Here is the sample code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_stack.py
class ArrayStack:\n    \"\"\"Stack based on array implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._stack: list[int] = []\n\n    def size(self) -> int:\n        \"\"\"Get the length of the stack\"\"\"\n        return len(self._stack)\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the stack is empty\"\"\"\n        return self.size() == 0\n\n    def push(self, item: int):\n        \"\"\"Push\"\"\"\n        self._stack.append(item)\n\n    def pop(self) -> int:\n        \"\"\"Pop\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._stack.pop()\n\n    def peek(self) -> int:\n        \"\"\"Access top of the stack element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._stack[-1]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Return list for printing\"\"\"\n        return self._stack\n
array_stack.cpp
/* Stack based on array implementation */\nclass ArrayStack {\n  private:\n    vector<int> stack;\n\n  public:\n    /* Get the length of the stack */\n    int size() {\n        return stack.size();\n    }\n\n    /* Check if the stack is empty */\n    bool isEmpty() {\n        return stack.size() == 0;\n    }\n\n    /* Push */\n    void push(int num) {\n        stack.push_back(num);\n    }\n\n    /* Pop */\n    int pop() {\n        int num = top();\n        stack.pop_back();\n        return num;\n    }\n\n    /* Return list for printing */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"Stack is empty\");\n        return stack.back();\n    }\n\n    /* Return Vector */\n    vector<int> toVector() {\n        return stack;\n    }\n};\n
array_stack.java
/* Stack based on array implementation */\nclass ArrayStack {\n    private ArrayList<Integer> stack;\n\n    public ArrayStack() {\n        // Initialize list (dynamic array)\n        stack = new ArrayList<>();\n    }\n\n    /* Get the length of the stack */\n    public int size() {\n        return stack.size();\n    }\n\n    /* Check if the stack is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    public void push(int num) {\n        stack.add(num);\n    }\n\n    /* Pop */\n    public int pop() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.remove(size() - 1);\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.get(size() - 1);\n    }\n\n    /* Convert List to Array and return */\n    public Object[] toArray() {\n        return stack.toArray();\n    }\n}\n
array_stack.cs
/* Stack based on array implementation */\nclass ArrayStack {\n    List<int> stack;\n    public ArrayStack() {\n        // Initialize list (dynamic array)\n        stack = [];\n    }\n\n    /* Get the length of the stack */\n    public int Size() {\n        return stack.Count;\n    }\n\n    /* Check if the stack is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Push */\n    public void Push(int num) {\n        stack.Add(num);\n    }\n\n    /* Pop */\n    public int Pop() {\n        if (IsEmpty())\n            throw new Exception();\n        var val = Peek();\n        stack.RemoveAt(Size() - 1);\n        return val;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stack[Size() - 1];\n    }\n\n    /* Convert List to Array and return */\n    public int[] ToArray() {\n        return [.. stack];\n    }\n}\n
array_stack.go
/* Stack based on array implementation */\ntype arrayStack struct {\n    data []int // Data\n}\n\n/* Access top of the stack element */\nfunc newArrayStack() *arrayStack {\n    return &arrayStack{\n        // Set stack length to 0, capacity to 16\n        data: make([]int, 0, 16),\n    }\n}\n\n/* Stack length */\nfunc (s *arrayStack) size() int {\n    return len(s.data)\n}\n\n/* Is stack empty */\nfunc (s *arrayStack) isEmpty() bool {\n    return s.size() == 0\n}\n\n/* Push */\nfunc (s *arrayStack) push(v int) {\n    // Slice will automatically expand\n    s.data = append(s.data, v)\n}\n\n/* Pop */\nfunc (s *arrayStack) pop() any {\n    val := s.peek()\n    s.data = s.data[:len(s.data)-1]\n    return val\n}\n\n/* Get stack top element */\nfunc (s *arrayStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    val := s.data[len(s.data)-1]\n    return val\n}\n\n/* Get Slice for printing */\nfunc (s *arrayStack) toSlice() []int {\n    return s.data\n}\n
array_stack.swift
/* Stack based on array implementation */\nclass ArrayStack {\n    private var stack: [Int]\n\n    init() {\n        // Initialize list (dynamic array)\n        stack = []\n    }\n\n    /* Get the length of the stack */\n    func size() -> Int {\n        stack.count\n    }\n\n    /* Check if the stack is empty */\n    func isEmpty() -> Bool {\n        stack.isEmpty\n    }\n\n    /* Push */\n    func push(num: Int) {\n        stack.append(num)\n    }\n\n    /* Pop */\n    @discardableResult\n    func pop() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return stack.removeLast()\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return stack.last!\n    }\n\n    /* Convert List to Array and return */\n    func toArray() -> [Int] {\n        stack\n    }\n}\n
array_stack.js
/* Stack based on array implementation */\nclass ArrayStack {\n    #stack;\n    constructor() {\n        this.#stack = [];\n    }\n\n    /* Get the length of the stack */\n    get size() {\n        return this.#stack.length;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty() {\n        return this.#stack.length === 0;\n    }\n\n    /* Push */\n    push(num) {\n        this.#stack.push(num);\n    }\n\n    /* Pop */\n    pop() {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.#stack.pop();\n    }\n\n    /* Return list for printing */\n    top() {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.#stack[this.#stack.length - 1];\n    }\n\n    /* Return Array */\n    toArray() {\n        return this.#stack;\n    }\n}\n
array_stack.ts
/* Stack based on array implementation */\nclass ArrayStack {\n    private stack: number[];\n    constructor() {\n        this.stack = [];\n    }\n\n    /* Get the length of the stack */\n    get size(): number {\n        return this.stack.length;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty(): boolean {\n        return this.stack.length === 0;\n    }\n\n    /* Push */\n    push(num: number): void {\n        this.stack.push(num);\n    }\n\n    /* Pop */\n    pop(): number | undefined {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.stack.pop();\n    }\n\n    /* Return list for printing */\n    top(): number | undefined {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.stack[this.stack.length - 1];\n    }\n\n    /* Return Array */\n    toArray() {\n        return this.stack;\n    }\n}\n
array_stack.dart
/* Stack based on array implementation */\nclass ArrayStack {\n  late List<int> _stack;\n  ArrayStack() {\n    _stack = [];\n  }\n\n  /* Get the length of the stack */\n  int size() {\n    return _stack.length;\n  }\n\n  /* Check if the stack is empty */\n  bool isEmpty() {\n    return _stack.isEmpty;\n  }\n\n  /* Push */\n  void push(int _num) {\n    _stack.add(_num);\n  }\n\n  /* Pop */\n  int pop() {\n    if (isEmpty()) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stack.removeLast();\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stack.last;\n  }\n\n  /* Convert stack to Array and return */\n  List<int> toArray() => _stack;\n}\n
array_stack.rs
/* Stack based on array implementation */\nstruct ArrayStack<T> {\n    stack: Vec<T>,\n}\n\nimpl<T> ArrayStack<T> {\n    /* Access top of the stack element */\n    fn new() -> ArrayStack<T> {\n        ArrayStack::<T> {\n            stack: Vec::<T>::new(),\n        }\n    }\n\n    /* Get the length of the stack */\n    fn size(&self) -> usize {\n        self.stack.len()\n    }\n\n    /* Check if the stack is empty */\n    fn is_empty(&self) -> bool {\n        self.size() == 0\n    }\n\n    /* Push */\n    fn push(&mut self, num: T) {\n        self.stack.push(num);\n    }\n\n    /* Pop */\n    fn pop(&mut self) -> Option<T> {\n        self.stack.pop()\n    }\n\n    /* Return list for printing */\n    fn peek(&self) -> Option<&T> {\n        if self.is_empty() {\n            panic!(\"Stack is empty\")\n        };\n        self.stack.last()\n    }\n\n    /* Return &Vec */\n    fn to_array(&self) -> &Vec<T> {\n        &self.stack\n    }\n}\n
array_stack.c
/* Stack based on array implementation */\ntypedef struct {\n    int *data;\n    int size;\n} ArrayStack;\n\n/* Constructor */\nArrayStack *newArrayStack() {\n    ArrayStack *stack = malloc(sizeof(ArrayStack));\n    // Initialize with large capacity to avoid expansion\n    stack->data = malloc(sizeof(int) * MAX_SIZE);\n    stack->size = 0;\n    return stack;\n}\n\n/* Destructor */\nvoid delArrayStack(ArrayStack *stack) {\n    free(stack->data);\n    free(stack);\n}\n\n/* Get the length of the stack */\nint size(ArrayStack *stack) {\n    return stack->size;\n}\n\n/* Check if the stack is empty */\nbool isEmpty(ArrayStack *stack) {\n    return stack->size == 0;\n}\n\n/* Push */\nvoid push(ArrayStack *stack, int num) {\n    if (stack->size == MAX_SIZE) {\n        printf(\"Stack is full\\n\");\n        return;\n    }\n    stack->data[stack->size] = num;\n    stack->size++;\n}\n\n/* Return list for printing */\nint peek(ArrayStack *stack) {\n    if (stack->size == 0) {\n        printf(\"Stack is empty\\n\");\n        return INT_MAX;\n    }\n    return stack->data[stack->size - 1];\n}\n\n/* Pop */\nint pop(ArrayStack *stack) {\n    int val = peek(stack);\n    stack->size--;\n    return val;\n}\n
array_stack.kt
/* Stack based on array implementation */\nclass ArrayStack {\n    // Initialize list (dynamic array)\n    private val stack = mutableListOf<Int>()\n\n    /* Get the length of the stack */\n    fun size(): Int {\n        return stack.size\n    }\n\n    /* Check if the stack is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Push */\n    fun push(num: Int) {\n        stack.add(num)\n    }\n\n    /* Pop */\n    fun pop(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack.removeAt(size() - 1)\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack[size() - 1]\n    }\n\n    /* Convert List to Array and return */\n    fun toArray(): Array<Any> {\n        return stack.toTypedArray()\n    }\n}\n
array_stack.rb
### Stack based on array ###\nclass ArrayStack\n  ### Constructor ###\n  def initialize\n    @stack = []\n  end\n\n  ### Get stack length ###\n  def size\n    @stack.length\n  end\n\n  ### Check if stack is empty ###\n  def is_empty?\n    @stack.empty?\n  end\n\n  ### Push ###\n  def push(item)\n    @stack << item\n  end\n\n  ### Pop ###\n  def pop\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @stack.pop\n  end\n\n  ### Access top element ###\n  def peek\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @stack.last\n  end\n\n  ### Return list for printing ###\n  def to_array\n    @stack\n  end\nend\n
","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#513-comparison-of-the-two-implementations","level":2,"title":"5.1.3   Comparison of the Two Implementations","text":"

Supported Operations

Both implementations support all operations defined by the stack. The array implementation additionally supports random access, but this goes beyond the stack definition and is generally not used.

Time Efficiency

In the array-based implementation, both push and pop operations occur in pre-allocated contiguous memory, which has good cache locality and is therefore more efficient. However, if pushing exceeds the array capacity, it triggers an expansion mechanism, causing the time complexity of that particular push operation to become \\(O(n)\\).

In the linked list-based implementation, list expansion is very flexible, and there is no issue of reduced efficiency due to array expansion. However, the push operation requires initializing a node object and modifying pointers, so it is relatively less efficient. Nevertheless, if the pushed elements are already node objects, the initialization step can be omitted, thereby improving efficiency.

In summary, when the elements pushed and popped are basic data types such as int or double, we can draw the following conclusions:

  • The array-based stack implementation has reduced efficiency when expansion is triggered, but since expansion is an infrequent operation, the average efficiency is higher.
  • The linked list-based stack implementation can provide more stable efficiency performance.

Space Efficiency

When initializing a list, the system allocates an \"initial capacity\" that may exceed the actual need. Additionally, the expansion mechanism typically expands at a specific ratio (e.g., 2x), and the capacity after expansion may also exceed actual needs. Therefore, the array-based stack implementation may cause some space wastage.

However, since linked list nodes need to store additional pointers, the space occupied by linked list nodes is relatively large.

In summary, we cannot simply determine which implementation is more memory-efficient and need to analyze the specific situation.

","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514-typical-applications-of-stack","level":2,"title":"5.1.4   Typical Applications of Stack","text":"
  • Back and forward in browsers, undo and redo in software. Every time we open a new webpage, the browser pushes the previous page onto the stack, allowing us to return to the previous page via the back operation. The back operation is essentially performing a pop. To support both back and forward, two stacks are needed to work together.
  • Program memory management. Each time a function is called, the system adds a stack frame to the top of the stack to record the function's context information. During recursion, the downward recursive phase continuously performs push operations, while the upward backtracking phase continuously performs pop operations.
","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/summary/","level":1,"title":"5.4   Summary","text":"","path":["Chapter 5. Stack and Queue","5.4   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • A stack is a data structure that follows the LIFO principle and can be implemented using arrays or linked lists.
  • In terms of time efficiency, the array implementation of a stack has higher average efficiency, but during expansion, the time complexity of a single push operation degrades to \\(O(n)\\). In contrast, the linked list implementation of a stack provides more stable efficiency performance.
  • In terms of space efficiency, the array implementation of a stack may lead to some degree of space wastage. However, it should be noted that the memory space occupied by linked list nodes is larger than that of array elements.
  • A queue is a data structure that follows the FIFO principle and can also be implemented using arrays or linked lists. The conclusions regarding time efficiency and space efficiency comparisons for queues are similar to those for stacks mentioned above.
  • A deque is a queue with greater flexibility that allows adding and removing elements at both ends.
","path":["Chapter 5. Stack and Queue","5.4   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Is the browser's forward and backward functionality implemented with a doubly linked list?

The forward and backward functionality of a browser is essentially a manifestation of a \"stack.\" When a user visits a new page, that page is added to the top of the stack; when the user clicks the back button, that page is popped from the top of the stack. Using a deque can conveniently implement some additional operations, as mentioned in the \"Deque\" section.

Q: After popping from the stack, do we need to free the memory of the popped node?

If the popped node will still be needed later, then memory does not need to be freed. If it won't be used afterward, languages like Java and Python have automatic garbage collection, so manual memory deallocation is not required; in C and C++, manual memory deallocation is necessary.

Q: A deque seems like two stacks joined together. What is its purpose?

A deque is like a combination of a stack and a queue, or two stacks joined together. It exhibits the logic of both stack and queue, so it can implement all applications of stacks and queues, and is more flexible.

Q: How are undo and redo specifically implemented?

Use two stacks: stack A for undo and stack B for redo.

  1. Whenever the user performs an operation, push this operation onto stack A and clear stack B.
  2. When the user performs \"undo,\" pop the most recent operation from stack A and push it onto stack B.
  3. When the user performs \"redo,\" pop the most recent operation from stack B and push it onto stack A.
","path":["Chapter 5. Stack and Queue","5.4   Summary"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"Chapter 7.   Tree","text":"

Abstract

Towering trees are full of vitality, with deep roots and lush leaves, spreading branches and flourishing.

They show us the vivid form of divide and conquer in data.

","path":["Chapter 7. Tree","Chapter 7.   Tree"],"tags":[]},{"location":"chapter_tree/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 7.1   Binary Tree
  • 7.2   Binary Tree Traversal
  • 7.3   Array Representation of Tree
  • 7.4   Binary Search Tree
  • 7.5   AVL Tree *
  • 7.6   Summary
","path":["Chapter 7. Tree","Chapter 7.   Tree"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/","level":1,"title":"7.3   Array Representation of Binary Trees","text":"

Under the linked list representation, the storage unit of a binary tree is a node TreeNode, and nodes are connected by pointers. The previous section introduced the basic operations of binary trees under the linked list representation.

So, can we use an array to represent a binary tree? The answer is yes.

","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#731-representing-perfect-binary-trees","level":2,"title":"7.3.1   Representing Perfect Binary Trees","text":"

Let's analyze a simple case first. Given a perfect binary tree, we store all nodes in an array according to the order of level-order traversal, where each node corresponds to a unique array index.

Based on the characteristics of level-order traversal, we can derive a \"mapping formula\" between parent node index and child node indices: If a node's index is \\(i\\), then its left child index is \\(2i + 1\\) and its right child index is \\(2i + 2\\). Figure 7-12 shows the mapping relationships between various node indices.

Figure 7-12   Array representation of a perfect binary tree

The mapping formula plays a role similar to the node references (pointers) in linked lists. Given any node in the array, we can access its left (right) child node using the mapping formula.

","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732-representing-any-binary-tree","level":2,"title":"7.3.2   Representing Any Binary Tree","text":"

Perfect binary trees are a special case; in the middle levels of a binary tree, there are typically many None values. Since the level-order traversal sequence does not include these None values, we cannot infer the number and distribution of None values based on this sequence alone. This means multiple binary tree structures can correspond to the same level-order traversal sequence.

As shown in Figure 7-13, given a non-perfect binary tree, the above method of array representation fails.

Figure 7-13   Level-order traversal sequence corresponds to multiple binary tree possibilities

To solve this problem, we can consider explicitly writing out all None values in the level-order traversal sequence. As shown in Figure 7-14, after this treatment, the level-order traversal sequence can uniquely represent a binary tree. Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# Array representation of a binary tree\n# Using None to represent empty slots\ntree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]\n
/* Array representation of a binary tree */\n// Using the maximum integer value INT_MAX to mark empty slots\nvector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
/* Array representation of a binary tree */\n// Using the Integer wrapper class allows for using null to mark empty slots\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
/* Array representation of a binary tree */\n// Using nullable int (int?) allows for using null to mark empty slots\nint?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* Array representation of a binary tree */\n// Using an any type slice, allowing for nil to mark empty slots\ntree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}\n
/* Array representation of a binary tree */\n// Using optional Int (Int?) allows for using nil to mark empty slots\nlet tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n
/* Array representation of a binary tree */\n// Using null to represent empty slots\nlet tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* Array representation of a binary tree */\n// Using null to represent empty slots\nlet tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* Array representation of a binary tree */\n// Using nullable int (int?) allows for using null to mark empty slots\nList<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* Array representation of a binary tree */\n// Using None to mark empty slots\nlet tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];\n
/* Array representation of a binary tree */\n// Using the maximum int value to mark empty slots, therefore, node values must not be INT_MAX\nint tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
/* Array representation of a binary tree */\n// Using null to represent empty slots\nval tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )\n
### Array representation of a binary tree ###\n# Using nil to represent empty slots\ntree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n

Figure 7-14   Array representation of any type of binary tree

It's worth noting that complete binary trees are very well-suited for array representation. Recalling the definition of a complete binary tree, None only appears at the bottom level and towards the right, meaning all None values must appear at the end of the level-order traversal sequence.

This means that when using an array to represent a complete binary tree, it's possible to omit storing all None values, which is very convenient. Figure 7-15 gives an example.

Figure 7-15   Array representation of a complete binary tree

The following code implements a binary tree based on array representation, including the following operations:

  • Given a certain node, obtain its value, left (right) child node, and parent node.
  • Obtain the preorder, inorder, postorder, and level-order traversal sequences.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_binary_tree.py
class ArrayBinaryTree:\n    \"\"\"Binary tree class represented by array\"\"\"\n\n    def __init__(self, arr: list[int | None]):\n        \"\"\"Constructor\"\"\"\n        self._tree = list(arr)\n\n    def size(self):\n        \"\"\"List capacity\"\"\"\n        return len(self._tree)\n\n    def val(self, i: int) -> int | None:\n        \"\"\"Get value of node at index i\"\"\"\n        # If index is out of bounds, return None, representing empty position\n        if i < 0 or i >= self.size():\n            return None\n        return self._tree[i]\n\n    def left(self, i: int) -> int | None:\n        \"\"\"Get index of left child node of node at index i\"\"\"\n        return 2 * i + 1\n\n    def right(self, i: int) -> int | None:\n        \"\"\"Get index of right child node of node at index i\"\"\"\n        return 2 * i + 2\n\n    def parent(self, i: int) -> int | None:\n        \"\"\"Get index of parent node of node at index i\"\"\"\n        return (i - 1) // 2\n\n    def level_order(self) -> list[int]:\n        \"\"\"Level-order traversal\"\"\"\n        self.res = []\n        # Traverse array directly\n        for i in range(self.size()):\n            if self.val(i) is not None:\n                self.res.append(self.val(i))\n        return self.res\n\n    def dfs(self, i: int, order: str):\n        \"\"\"Depth-first traversal\"\"\"\n        if self.val(i) is None:\n            return\n        # Preorder traversal\n        if order == \"pre\":\n            self.res.append(self.val(i))\n        self.dfs(self.left(i), order)\n        # Inorder traversal\n        if order == \"in\":\n            self.res.append(self.val(i))\n        self.dfs(self.right(i), order)\n        # Postorder traversal\n        if order == \"post\":\n            self.res.append(self.val(i))\n\n    def pre_order(self) -> list[int]:\n        \"\"\"Preorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"pre\")\n        return self.res\n\n    def in_order(self) -> list[int]:\n        \"\"\"Inorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"in\")\n        return self.res\n\n    def post_order(self) -> list[int]:\n        \"\"\"Postorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"post\")\n        return self.res\n
array_binary_tree.cpp
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n  public:\n    /* Constructor */\n    ArrayBinaryTree(vector<int> arr) {\n        tree = arr;\n    }\n\n    /* List capacity */\n    int size() {\n        return tree.size();\n    }\n\n    /* Get value of node at index i */\n    int val(int i) {\n        // Return INT_MAX if index out of bounds, representing empty position\n        if (i < 0 || i >= size())\n            return INT_MAX;\n        return tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    int left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    int right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    int parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    vector<int> levelOrder() {\n        vector<int> res;\n        // Traverse array directly\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != INT_MAX)\n                res.push_back(val(i));\n        }\n        return res;\n    }\n\n    /* Preorder traversal */\n    vector<int> preOrder() {\n        vector<int> res;\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    vector<int> inOrder() {\n        vector<int> res;\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    vector<int> postOrder() {\n        vector<int> res;\n        dfs(0, \"post\", res);\n        return res;\n    }\n\n  private:\n    vector<int> tree;\n\n    /* Depth-first traversal */\n    void dfs(int i, string order, vector<int> &res) {\n        // If empty position, return\n        if (val(i) == INT_MAX)\n            return;\n        // Preorder traversal\n        if (order == \"pre\")\n            res.push_back(val(i));\n        dfs(left(i), order, res);\n        // Inorder traversal\n        if (order == \"in\")\n            res.push_back(val(i));\n        dfs(right(i), order, res);\n        // Postorder traversal\n        if (order == \"post\")\n            res.push_back(val(i));\n    }\n};\n
array_binary_tree.java
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    private List<Integer> tree;\n\n    /* Constructor */\n    public ArrayBinaryTree(List<Integer> arr) {\n        tree = new ArrayList<>(arr);\n    }\n\n    /* List capacity */\n    public int size() {\n        return tree.size();\n    }\n\n    /* Get value of node at index i */\n    public Integer val(int i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= size())\n            return null;\n        return tree.get(i);\n    }\n\n    /* Get index of left child node of node at index i */\n    public Integer left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    public Integer right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    public Integer parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    public List<Integer> levelOrder() {\n        List<Integer> res = new ArrayList<>();\n        // Traverse array directly\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != null)\n                res.add(val(i));\n        }\n        return res;\n    }\n\n    /* Depth-first traversal */\n    private void dfs(Integer i, String order, List<Integer> res) {\n        // If empty position, return\n        if (val(i) == null)\n            return;\n        // Preorder traversal\n        if (\"pre\".equals(order))\n            res.add(val(i));\n        dfs(left(i), order, res);\n        // Inorder traversal\n        if (\"in\".equals(order))\n            res.add(val(i));\n        dfs(right(i), order, res);\n        // Postorder traversal\n        if (\"post\".equals(order))\n            res.add(val(i));\n    }\n\n    /* Preorder traversal */\n    public List<Integer> preOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    public List<Integer> inOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    public List<Integer> postOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"post\", res);\n        return res;\n    }\n}\n
array_binary_tree.cs
/* Binary tree class represented by array */\nclass ArrayBinaryTree(List<int?> arr) {\n    List<int?> tree = new(arr);\n\n    /* List capacity */\n    public int Size() {\n        return tree.Count;\n    }\n\n    /* Get value of node at index i */\n    public int? Val(int i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= Size())\n            return null;\n        return tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    public int Left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    public int Right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    public int Parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    public List<int> LevelOrder() {\n        List<int> res = [];\n        // Traverse array directly\n        for (int i = 0; i < Size(); i++) {\n            if (Val(i).HasValue)\n                res.Add(Val(i)!.Value);\n        }\n        return res;\n    }\n\n    /* Depth-first traversal */\n    void DFS(int i, string order, List<int> res) {\n        // If empty position, return\n        if (!Val(i).HasValue)\n            return;\n        // Preorder traversal\n        if (order == \"pre\")\n            res.Add(Val(i)!.Value);\n        DFS(Left(i), order, res);\n        // Inorder traversal\n        if (order == \"in\")\n            res.Add(Val(i)!.Value);\n        DFS(Right(i), order, res);\n        // Postorder traversal\n        if (order == \"post\")\n            res.Add(Val(i)!.Value);\n    }\n\n    /* Preorder traversal */\n    public List<int> PreOrder() {\n        List<int> res = [];\n        DFS(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    public List<int> InOrder() {\n        List<int> res = [];\n        DFS(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    public List<int> PostOrder() {\n        List<int> res = [];\n        DFS(0, \"post\", res);\n        return res;\n    }\n}\n
array_binary_tree.go
/* Binary tree class represented by array */\ntype arrayBinaryTree struct {\n    tree []any\n}\n\n/* Constructor */\nfunc newArrayBinaryTree(arr []any) *arrayBinaryTree {\n    return &arrayBinaryTree{\n        tree: arr,\n    }\n}\n\n/* List capacity */\nfunc (abt *arrayBinaryTree) size() int {\n    return len(abt.tree)\n}\n\n/* Get value of node at index i */\nfunc (abt *arrayBinaryTree) val(i int) any {\n    // If index out of bounds, return null to represent empty position\n    if i < 0 || i >= abt.size() {\n        return nil\n    }\n    return abt.tree[i]\n}\n\n/* Get index of left child node of node at index i */\nfunc (abt *arrayBinaryTree) left(i int) int {\n    return 2*i + 1\n}\n\n/* Get index of right child node of node at index i */\nfunc (abt *arrayBinaryTree) right(i int) int {\n    return 2*i + 2\n}\n\n/* Get index of parent node of node at index i */\nfunc (abt *arrayBinaryTree) parent(i int) int {\n    return (i - 1) / 2\n}\n\n/* Level-order traversal */\nfunc (abt *arrayBinaryTree) levelOrder() []any {\n    var res []any\n    // Traverse array directly\n    for i := 0; i < abt.size(); i++ {\n        if abt.val(i) != nil {\n            res = append(res, abt.val(i))\n        }\n    }\n    return res\n}\n\n/* Depth-first traversal */\nfunc (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) {\n    // If empty position, return\n    if abt.val(i) == nil {\n        return\n    }\n    // Preorder traversal\n    if order == \"pre\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.left(i), order, res)\n    // Inorder traversal\n    if order == \"in\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.right(i), order, res)\n    // Postorder traversal\n    if order == \"post\" {\n        *res = append(*res, abt.val(i))\n    }\n}\n\n/* Preorder traversal */\nfunc (abt *arrayBinaryTree) preOrder() []any {\n    var res []any\n    abt.dfs(0, \"pre\", &res)\n    return res\n}\n\n/* Inorder traversal */\nfunc (abt *arrayBinaryTree) inOrder() []any {\n    var res []any\n    abt.dfs(0, \"in\", &res)\n    return res\n}\n\n/* Postorder traversal */\nfunc (abt *arrayBinaryTree) postOrder() []any {\n    var res []any\n    abt.dfs(0, \"post\", &res)\n    return res\n}\n
array_binary_tree.swift
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    private var tree: [Int?]\n\n    /* Constructor */\n    init(arr: [Int?]) {\n        tree = arr\n    }\n\n    /* List capacity */\n    func size() -> Int {\n        tree.count\n    }\n\n    /* Get value of node at index i */\n    func val(i: Int) -> Int? {\n        // If index out of bounds, return null to represent empty position\n        if i < 0 || i >= size() {\n            return nil\n        }\n        return tree[i]\n    }\n\n    /* Get index of left child node of node at index i */\n    func left(i: Int) -> Int {\n        2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    func right(i: Int) -> Int {\n        2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    func parent(i: Int) -> Int {\n        (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    func levelOrder() -> [Int] {\n        var res: [Int] = []\n        // Traverse array directly\n        for i in 0 ..< size() {\n            if let val = val(i: i) {\n                res.append(val)\n            }\n        }\n        return res\n    }\n\n    /* Depth-first traversal */\n    private func dfs(i: Int, order: String, res: inout [Int]) {\n        // If empty position, return\n        guard let val = val(i: i) else {\n            return\n        }\n        // Preorder traversal\n        if order == \"pre\" {\n            res.append(val)\n        }\n        dfs(i: left(i: i), order: order, res: &res)\n        // Inorder traversal\n        if order == \"in\" {\n            res.append(val)\n        }\n        dfs(i: right(i: i), order: order, res: &res)\n        // Postorder traversal\n        if order == \"post\" {\n            res.append(val)\n        }\n    }\n\n    /* Preorder traversal */\n    func preOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"pre\", res: &res)\n        return res\n    }\n\n    /* Inorder traversal */\n    func inOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"in\", res: &res)\n        return res\n    }\n\n    /* Postorder traversal */\n    func postOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"post\", res: &res)\n        return res\n    }\n}\n
array_binary_tree.js
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    #tree;\n\n    /* Constructor */\n    constructor(arr) {\n        this.#tree = arr;\n    }\n\n    /* List capacity */\n    size() {\n        return this.#tree.length;\n    }\n\n    /* Get value of node at index i */\n    val(i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    left(i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    right(i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    parent(i) {\n        return Math.floor((i - 1) / 2); // Floor division\n    }\n\n    /* Level-order traversal */\n    levelOrder() {\n        let res = [];\n        // Traverse array directly\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* Depth-first traversal */\n    #dfs(i, order, res) {\n        // If empty position, return\n        if (this.val(i) === null) return;\n        // Preorder traversal\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Inorder traversal\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Postorder traversal\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Preorder traversal */\n    preOrder() {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    inOrder() {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    postOrder() {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
array_binary_tree.ts
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    #tree: (number | null)[];\n\n    /* Constructor */\n    constructor(arr: (number | null)[]) {\n        this.#tree = arr;\n    }\n\n    /* List capacity */\n    size(): number {\n        return this.#tree.length;\n    }\n\n    /* Get value of node at index i */\n    val(i: number): number | null {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    left(i: number): number {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    right(i: number): number {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    parent(i: number): number {\n        return Math.floor((i - 1) / 2); // Floor division\n    }\n\n    /* Level-order traversal */\n    levelOrder(): number[] {\n        let res = [];\n        // Traverse array directly\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* Depth-first traversal */\n    #dfs(i: number, order: Order, res: (number | null)[]): void {\n        // If empty position, return\n        if (this.val(i) === null) return;\n        // Preorder traversal\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Inorder traversal\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Postorder traversal\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Preorder traversal */\n    preOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    inOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    postOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
array_binary_tree.dart
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n  late List<int?> _tree;\n\n  /* Constructor */\n  ArrayBinaryTree(this._tree);\n\n  /* List capacity */\n  int size() {\n    return _tree.length;\n  }\n\n  /* Get value of node at index i */\n  int? val(int i) {\n    // If index out of bounds, return null to represent empty position\n    if (i < 0 || i >= size()) {\n      return null;\n    }\n    return _tree[i];\n  }\n\n  /* Get index of left child node of node at index i */\n  int? left(int i) {\n    return 2 * i + 1;\n  }\n\n  /* Get index of right child node of node at index i */\n  int? right(int i) {\n    return 2 * i + 2;\n  }\n\n  /* Get index of parent node of node at index i */\n  int? parent(int i) {\n    return (i - 1) ~/ 2;\n  }\n\n  /* Level-order traversal */\n  List<int> levelOrder() {\n    List<int> res = [];\n    for (int i = 0; i < size(); i++) {\n      if (val(i) != null) {\n        res.add(val(i)!);\n      }\n    }\n    return res;\n  }\n\n  /* Depth-first traversal */\n  void dfs(int i, String order, List<int?> res) {\n    // If empty position, return\n    if (val(i) == null) {\n      return;\n    }\n    // Preorder traversal\n    if (order == 'pre') {\n      res.add(val(i));\n    }\n    dfs(left(i)!, order, res);\n    // Inorder traversal\n    if (order == 'in') {\n      res.add(val(i));\n    }\n    dfs(right(i)!, order, res);\n    // Postorder traversal\n    if (order == 'post') {\n      res.add(val(i));\n    }\n  }\n\n  /* Preorder traversal */\n  List<int?> preOrder() {\n    List<int?> res = [];\n    dfs(0, 'pre', res);\n    return res;\n  }\n\n  /* Inorder traversal */\n  List<int?> inOrder() {\n    List<int?> res = [];\n    dfs(0, 'in', res);\n    return res;\n  }\n\n  /* Postorder traversal */\n  List<int?> postOrder() {\n    List<int?> res = [];\n    dfs(0, 'post', res);\n    return res;\n  }\n}\n
array_binary_tree.rs
/* Binary tree class represented by array */\nstruct ArrayBinaryTree {\n    tree: Vec<Option<i32>>,\n}\n\nimpl ArrayBinaryTree {\n    /* Constructor */\n    fn new(arr: Vec<Option<i32>>) -> Self {\n        Self { tree: arr }\n    }\n\n    /* List capacity */\n    fn size(&self) -> i32 {\n        self.tree.len() as i32\n    }\n\n    /* Get value of node at index i */\n    fn val(&self, i: i32) -> Option<i32> {\n        // If index is out of bounds, return None, representing empty position\n        if i < 0 || i >= self.size() {\n            None\n        } else {\n            self.tree[i as usize]\n        }\n    }\n\n    /* Get index of left child node of node at index i */\n    fn left(&self, i: i32) -> i32 {\n        2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    fn right(&self, i: i32) -> i32 {\n        2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    fn parent(&self, i: i32) -> i32 {\n        (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    fn level_order(&self) -> Vec<i32> {\n        self.tree.iter().filter_map(|&x| x).collect()\n    }\n\n    /* Depth-first traversal */\n    fn dfs(&self, i: i32, order: &'static str, res: &mut Vec<i32>) {\n        if self.val(i).is_none() {\n            return;\n        }\n        let val = self.val(i).unwrap();\n        // Preorder traversal\n        if order == \"pre\" {\n            res.push(val);\n        }\n        self.dfs(self.left(i), order, res);\n        // Inorder traversal\n        if order == \"in\" {\n            res.push(val);\n        }\n        self.dfs(self.right(i), order, res);\n        // Postorder traversal\n        if order == \"post\" {\n            res.push(val);\n        }\n    }\n\n    /* Preorder traversal */\n    fn pre_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"pre\", &mut res);\n        res\n    }\n\n    /* Inorder traversal */\n    fn in_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"in\", &mut res);\n        res\n    }\n\n    /* Postorder traversal */\n    fn post_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"post\", &mut res);\n        res\n    }\n}\n
array_binary_tree.c
/* Binary tree structure in array representation */\ntypedef struct {\n    int *tree;\n    int size;\n} ArrayBinaryTree;\n\n/* Constructor */\nArrayBinaryTree *newArrayBinaryTree(int *arr, int arrSize) {\n    ArrayBinaryTree *abt = (ArrayBinaryTree *)malloc(sizeof(ArrayBinaryTree));\n    abt->tree = malloc(sizeof(int) * arrSize);\n    memcpy(abt->tree, arr, sizeof(int) * arrSize);\n    abt->size = arrSize;\n    return abt;\n}\n\n/* Destructor */\nvoid delArrayBinaryTree(ArrayBinaryTree *abt) {\n    free(abt->tree);\n    free(abt);\n}\n\n/* List capacity */\nint size(ArrayBinaryTree *abt) {\n    return abt->size;\n}\n\n/* Get value of node at index i */\nint val(ArrayBinaryTree *abt, int i) {\n    // Return INT_MAX if index out of bounds, representing empty position\n    if (i < 0 || i >= size(abt))\n        return INT_MAX;\n    return abt->tree[i];\n}\n\n/* Level-order traversal */\nint *levelOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    // Traverse array directly\n    for (int i = 0; i < size(abt); i++) {\n        if (val(abt, i) != INT_MAX)\n            res[index++] = val(abt, i);\n    }\n    *returnSize = index;\n    return res;\n}\n\n/* Depth-first traversal */\nvoid dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) {\n    // If empty position, return\n    if (val(abt, i) == INT_MAX)\n        return;\n    // Preorder traversal\n    if (strcmp(order, \"pre\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, left(i), order, res, index);\n    // Inorder traversal\n    if (strcmp(order, \"in\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, right(i), order, res, index);\n    // Postorder traversal\n    if (strcmp(order, \"post\") == 0)\n        res[(*index)++] = val(abt, i);\n}\n\n/* Preorder traversal */\nint *preOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"pre\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* Inorder traversal */\nint *inOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"in\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* Postorder traversal */\nint *postOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"post\", res, &index);\n    *returnSize = index;\n    return res;\n}\n
array_binary_tree.kt
/* Binary tree class represented by array */\nclass ArrayBinaryTree(val tree: MutableList<Int?>) {\n    /* List capacity */\n    fun size(): Int {\n        return tree.size\n    }\n\n    /* Get value of node at index i */\n    fun _val(i: Int): Int? {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= size()) return null\n        return tree[i]\n    }\n\n    /* Get index of left child node of node at index i */\n    fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    fun parent(i: Int): Int {\n        return (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    fun levelOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        // Traverse array directly\n        for (i in 0..<size()) {\n            if (_val(i) != null)\n                res.add(_val(i))\n        }\n        return res\n    }\n\n    /* Depth-first traversal */\n    fun dfs(i: Int, order: String, res: MutableList<Int?>) {\n        // If empty position, return\n        if (_val(i) == null)\n            return\n        // Preorder traversal\n        if (\"pre\" == order)\n            res.add(_val(i))\n        dfs(left(i), order, res)\n        // Inorder traversal\n        if (\"in\" == order)\n            res.add(_val(i))\n        dfs(right(i), order, res)\n        // Postorder traversal\n        if (\"post\" == order)\n            res.add(_val(i))\n    }\n\n    /* Preorder traversal */\n    fun preOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"pre\", res)\n        return res\n    }\n\n    /* Inorder traversal */\n    fun inOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"in\", res)\n        return res\n    }\n\n    /* Postorder traversal */\n    fun postOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"post\", res)\n        return res\n    }\n}\n
array_binary_tree.rb
### Array representation of binary tree class ###\nclass ArrayBinaryTree\n  ### Constructor ###\n  def initialize(arr)\n    @tree = arr.to_a\n  end\n\n  ### List capacity ###\n  def size\n    @tree.length\n  end\n\n  ### Get value of node at index i ###\n  def val(i)\n    # Return nil if index out of bounds, representing empty position\n    return if i < 0 || i >= size\n\n    @tree[i]\n  end\n\n  ### Get left child index of node at index i ###\n  def left(i)\n    2 * i + 1\n  end\n\n  ### Get right child index of node at index i ###\n  def right(i)\n    2 * i + 2\n  end\n\n  ### Get parent node index of node at index i ###\n  def parent(i)\n    (i - 1) / 2\n  end\n\n  ### Level-order traversal ###\n  def level_order\n    @res = []\n\n    # Traverse array directly\n    for i in 0...size\n      @res << val(i) unless val(i).nil?\n    end\n\n    @res\n  end\n\n  ### Depth-first traversal ###\n  def dfs(i, order)\n    return if val(i).nil?\n    # Preorder traversal\n    @res << val(i) if order == :pre\n    dfs(left(i), order)\n    # Inorder traversal\n    @res << val(i) if order == :in\n    dfs(right(i), order)\n    # Postorder traversal\n    @res << val(i) if order == :post\n  end\n\n  ### Pre-order traversal ###\n  def pre_order\n    @res = []\n    dfs(0, :pre)\n    @res\n  end\n\n  ### In-order traversal ###\n  def in_order\n    @res = []\n    dfs(0, :in)\n    @res\n  end\n\n  ### Post-order traversal ###\n  def post_order\n    @res = []\n    dfs(0, :post)\n    @res\n  end\nend\n
","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#733-advantages-and-limitations","level":2,"title":"7.3.3   Advantages and Limitations","text":"

The array representation of binary trees has the following advantages:

  • Arrays are stored in contiguous memory space, which is cache-friendly, allowing faster access and traversal.
  • It does not require storing pointers, which saves space.
  • It allows random access to nodes.

However, the array representation also has some limitations:

  • Array storage requires contiguous memory space, so it is not suitable for storing trees with a large amount of data.
  • Adding or removing nodes requires array insertion and deletion operations, which have lower efficiency.
  • When there are many None values in the binary tree, the proportion of node data contained in the array is low, leading to lower space utilization.
","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/avl_tree/","level":1,"title":"7.5   Avl Tree *","text":"

In the \"Binary Search Tree\" section, we mentioned that after multiple insertion and removal operations, a binary search tree may degenerate into a linked list. In this case, the time complexity of all operations degrades from \\(O(\\log n)\\) to \\(O(n)\\).

As shown in Figure 7-24, after two node removal operations, this binary search tree will degrade into a linked list.

Figure 7-24   Degradation of an AVL tree after removing nodes

For example, in the perfect binary tree shown in Figure 7-25, after inserting two nodes, the tree will lean heavily to the left, and the time complexity of search operations will also degrade.

Figure 7-25   Degradation of an AVL tree after inserting nodes

In 1962, G. M. Adelson-Velsky and E. M. Landis proposed the AVL tree in their paper \"An algorithm for the organization of information\". The paper described in detail a series of operations ensuring that after continuously adding and removing nodes, the AVL tree does not degenerate, thus keeping the time complexity of various operations at the \\(O(\\log n)\\) level. In other words, in scenarios requiring frequent insertions, deletions, searches, and modifications, the AVL tree can always maintain efficient data operation performance, making it very valuable in applications.

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#751-common-terminology-in-avl-trees","level":2,"title":"7.5.1   Common Terminology in Avl Trees","text":"

An AVL tree is both a binary search tree and a balanced binary tree, simultaneously satisfying all the properties of these two types of binary trees, hence it is a balanced binary search tree.

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-node-height","level":3,"title":"1.   Node Height","text":"

Since the operations related to AVL trees require obtaining node heights, we need to add a height variable to the node class:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class TreeNode:\n    \"\"\"AVL tree node\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                 # Node value\n        self.height: int = 0                # Node height\n        self.left: TreeNode | None = None   # Left child reference\n        self.right: TreeNode | None = None  # Right child reference\n
/* AVL tree node */\nstruct TreeNode {\n    int val{};          // Node value\n    int height = 0;     // Node height\n    TreeNode *left{};   // Left child\n    TreeNode *right{};  // Right child\n    TreeNode() = default;\n    explicit TreeNode(int x) : val(x){}\n};\n
/* AVL tree node */\nclass TreeNode {\n    public int val;        // Node value\n    public int height;     // Node height\n    public TreeNode left;  // Left child\n    public TreeNode right; // Right child\n    public TreeNode(int x) { val = x; }\n}\n
/* AVL tree node */\nclass TreeNode(int? x) {\n    public int? val = x;    // Node value\n    public int height;      // Node height\n    public TreeNode? left;  // Left child reference\n    public TreeNode? right; // Right child reference\n}\n
/* AVL tree node */\ntype TreeNode struct {\n    Val    int       // Node value\n    Height int       // Node height\n    Left   *TreeNode // Left child reference\n    Right  *TreeNode // Right child reference\n}\n
/* AVL tree node */\nclass TreeNode {\n    var val: Int // Node value\n    var height: Int // Node height\n    var left: TreeNode? // Left child\n    var right: TreeNode? // Right child\n\n    init(x: Int) {\n        val = x\n        height = 0\n    }\n}\n
/* AVL tree node */\nclass TreeNode {\n    val; // Node value\n    height; // Node height\n    left; // Left child pointer\n    right; // Right child pointer\n    constructor(val, left, right, height) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* AVL tree node */\nclass TreeNode {\n    val: number;            // Node value\n    height: number;         // Node height\n    left: TreeNode | null;  // Left child pointer\n    right: TreeNode | null; // Right child pointer\n    constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height; \n        this.left = left === undefined ? null : left; \n        this.right = right === undefined ? null : right; \n    }\n}\n
/* AVL tree node */\nclass TreeNode {\n  int val;         // Node value\n  int height;      // Node height\n  TreeNode? left;  // Left child\n  TreeNode? right; // Right child\n  TreeNode(this.val, [this.height = 0, this.left, this.right]);\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* AVL tree node */\nstruct TreeNode {\n    val: i32,                               // Node value\n    height: i32,                            // Node height\n    left: Option<Rc<RefCell<TreeNode>>>,    // Left child\n    right: Option<Rc<RefCell<TreeNode>>>,   // Right child\n}\n\nimpl TreeNode {\n    /* Constructor */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            height: 0,\n            left: None,\n            right: None\n        }))\n    }\n}\n
/* AVL tree node */\ntypedef struct TreeNode {\n    int val;\n    int height;\n    struct TreeNode *left;\n    struct TreeNode *right;\n} TreeNode;\n\n/* Constructor */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
/* AVL tree node */\nclass TreeNode(val _val: Int) {  // Node value\n    val height: Int = 0          // Node height\n    val left: TreeNode? = null   // Left child\n    val right: TreeNode? = null  // Right child\n}\n
### AVL tree node class ###\nclass TreeNode\n  attr_accessor :val    # Node value\n  attr_accessor :height # Node height\n  attr_accessor :left   # Left child reference\n  attr_accessor :right  # Right child reference\n\n  def initialize(val)\n    @val = val\n    @height = 0\n  end\nend\n

The \"node height\" refers to the distance from that node to its farthest leaf node, i.e., the number of \"edges\" passed. It is important to note that the height of a leaf node is \\(0\\), and the height of a null node is \\(-1\\). We will create two utility functions for getting and updating the height of a node:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def height(self, node: TreeNode | None) -> int:\n    \"\"\"Get node height\"\"\"\n    # Empty node height is -1, leaf node height is 0\n    if node is not None:\n        return node.height\n    return -1\n\ndef update_height(self, node: TreeNode | None):\n    \"\"\"Update node height\"\"\"\n    # Node height equals the height of the tallest subtree + 1\n    node.height = max([self.height(node.left), self.height(node.right)]) + 1\n
avl_tree.cpp
/* Get node height */\nint height(TreeNode *node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == nullptr ? -1 : node->height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode *node) {\n    // Node height equals the height of the tallest subtree + 1\n    node->height = max(height(node->left), height(node->right)) + 1;\n}\n
avl_tree.java
/* Get node height */\nint height(TreeNode node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height = Math.max(height(node.left), height(node.right)) + 1;\n}\n
avl_tree.cs
/* Get node height */\nint Height(TreeNode? node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid UpdateHeight(TreeNode node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height = Math.Max(Height(node.left), Height(node.right)) + 1;\n}\n
avl_tree.go
/* Get node height */\nfunc (t *aVLTree) height(node *TreeNode) int {\n    // Empty node height is -1, leaf node height is 0\n    if node != nil {\n        return node.Height\n    }\n    return -1\n}\n\n/* Update node height */\nfunc (t *aVLTree) updateHeight(node *TreeNode) {\n    lh := t.height(node.Left)\n    rh := t.height(node.Right)\n    // Node height equals the height of the tallest subtree + 1\n    if lh > rh {\n        node.Height = lh + 1\n    } else {\n        node.Height = rh + 1\n    }\n}\n
avl_tree.swift
/* Get node height */\nfunc height(node: TreeNode?) -> Int {\n    // Empty node height is -1, leaf node height is 0\n    node?.height ?? -1\n}\n\n/* Update node height */\nfunc updateHeight(node: TreeNode?) {\n    // Node height equals the height of the tallest subtree + 1\n    node?.height = max(height(node: node?.left), height(node: node?.right)) + 1\n}\n
avl_tree.js
/* Get node height */\nheight(node) {\n    // Empty node height is -1, leaf node height is 0\n    return node === null ? -1 : node.height;\n}\n\n/* Update node height */\n#updateHeight(node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
avl_tree.ts
/* Get node height */\nheight(node: TreeNode): number {\n    // Empty node height is -1, leaf node height is 0\n    return node === null ? -1 : node.height;\n}\n\n/* Update node height */\nupdateHeight(node: TreeNode): void {\n    // Node height equals the height of the tallest subtree + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
avl_tree.dart
/* Get node height */\nint height(TreeNode? node) {\n  // Empty node height is -1, leaf node height is 0\n  return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode? node) {\n  // Node height equals the height of the tallest subtree + 1\n  node!.height = max(height(node.left), height(node.right)) + 1;\n}\n
avl_tree.rs
/* Get node height */\nfn height(node: OptionTreeNodeRc) -> i32 {\n    // Empty node height is -1, leaf node height is 0\n    match node {\n        Some(node) => node.borrow().height,\n        None => -1,\n    }\n}\n\n/* Update node height */\nfn update_height(node: OptionTreeNodeRc) {\n    if let Some(node) = node {\n        let left = node.borrow().left.clone();\n        let right = node.borrow().right.clone();\n        // Node height equals the height of the tallest subtree + 1\n        node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1;\n    }\n}\n
avl_tree.c
/* Get node height */\nint height(TreeNode *node) {\n    // Empty node height is -1, leaf node height is 0\n    if (node != NULL) {\n        return node->height;\n    }\n    return -1;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode *node) {\n    int lh = height(node->left);\n    int rh = height(node->right);\n    // Node height equals the height of the tallest subtree + 1\n    if (lh > rh) {\n        node->height = lh + 1;\n    } else {\n        node->height = rh + 1;\n    }\n}\n
avl_tree.kt
/* Get node height */\nfun height(node: TreeNode?): Int {\n    // Empty node height is -1, leaf node height is 0\n    return node?.height ?: -1\n}\n\n/* Update node height */\nfun updateHeight(node: TreeNode?) {\n    // Node height equals the height of the tallest subtree + 1\n    node?.height = max(height(node?.left), height(node?.right)) + 1\n}\n
avl_tree.rb
### Get node height ###\ndef height(node)\n  # Empty node height is -1, leaf node height is 0\n  return node.height unless node.nil?\n\n  -1\nend\n\n### Update node height ###\ndef update_height(node)\n  # Node height equals the height of the tallest subtree + 1\n  node.height = [height(node.left), height(node.right)].max + 1\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-node-balance-factor","level":3,"title":"2.   Node Balance Factor","text":"

The balance factor of a node is defined as the height of the node's left subtree minus the height of its right subtree, and the balance factor of a null node is defined as \\(0\\). We also encapsulate the function to obtain the node's balance factor for convenient subsequent use:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def balance_factor(self, node: TreeNode | None) -> int:\n    \"\"\"Get balance factor\"\"\"\n    # Empty node balance factor is 0\n    if node is None:\n        return 0\n    # Node balance factor = left subtree height - right subtree height\n    return self.height(node.left) - self.height(node.right)\n
avl_tree.cpp
/* Get balance factor */\nint balanceFactor(TreeNode *node) {\n    // Empty node balance factor is 0\n    if (node == nullptr)\n        return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return height(node->left) - height(node->right);\n}\n
avl_tree.java
/* Get balance factor */\nint balanceFactor(TreeNode node) {\n    // Empty node balance factor is 0\n    if (node == null)\n        return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return height(node.left) - height(node.right);\n}\n
avl_tree.cs
/* Get balance factor */\nint BalanceFactor(TreeNode? node) {\n    // Empty node balance factor is 0\n    if (node == null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return Height(node.left) - Height(node.right);\n}\n
avl_tree.go
/* Get balance factor */\nfunc (t *aVLTree) balanceFactor(node *TreeNode) int {\n    // Empty node balance factor is 0\n    if node == nil {\n        return 0\n    }\n    // Node balance factor = left subtree height - right subtree height\n    return t.height(node.Left) - t.height(node.Right)\n}\n
avl_tree.swift
/* Get balance factor */\nfunc balanceFactor(node: TreeNode?) -> Int {\n    // Empty node balance factor is 0\n    guard let node = node else { return 0 }\n    // Node balance factor = left subtree height - right subtree height\n    return height(node: node.left) - height(node: node.right)\n}\n
avl_tree.js
/* Get balance factor */\nbalanceFactor(node) {\n    // Empty node balance factor is 0\n    if (node === null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return this.height(node.left) - this.height(node.right);\n}\n
avl_tree.ts
/* Get balance factor */\nbalanceFactor(node: TreeNode): number {\n    // Empty node balance factor is 0\n    if (node === null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return this.height(node.left) - this.height(node.right);\n}\n
avl_tree.dart
/* Get balance factor */\nint balanceFactor(TreeNode? node) {\n  // Empty node balance factor is 0\n  if (node == null) return 0;\n  // Node balance factor = left subtree height - right subtree height\n  return height(node.left) - height(node.right);\n}\n
avl_tree.rs
/* Get balance factor */\nfn balance_factor(node: OptionTreeNodeRc) -> i32 {\n    match node {\n        // Empty node balance factor is 0\n        None => 0,\n        // Node balance factor = left subtree height - right subtree height\n        Some(node) => {\n            Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone())\n        }\n    }\n}\n
avl_tree.c
/* Get balance factor */\nint balanceFactor(TreeNode *node) {\n    // Empty node balance factor is 0\n    if (node == NULL) {\n        return 0;\n    }\n    // Node balance factor = left subtree height - right subtree height\n    return height(node->left) - height(node->right);\n}\n
avl_tree.kt
/* Get balance factor */\nfun balanceFactor(node: TreeNode?): Int {\n    // Empty node balance factor is 0\n    if (node == null) return 0\n    // Node balance factor = left subtree height - right subtree height\n    return height(node.left) - height(node.right)\n}\n
avl_tree.rb
### Get balance factor ###\ndef balance_factor(node)\n  # Empty node balance factor is 0\n  return 0 if node.nil?\n\n  # Node balance factor = left subtree height - right subtree height\n  height(node.left) - height(node.right)\nend\n

Tip

Let the balance factor be \\(f\\), then the balance factor of any node in an AVL tree satisfies \\(-1 \\le f \\le 1\\).

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#752-rotations-in-avl-trees","level":2,"title":"7.5.2   Rotations in Avl Trees","text":"

The characteristic of AVL trees lies in the \"rotation\" operation, which can restore balance to unbalanced nodes without affecting the inorder traversal sequence of the binary tree. In other words, rotation operations can both maintain the property of a \"binary search tree\" and make the tree return to a \"balanced binary tree\".

We call nodes with a balance factor absolute value \\(> 1\\) \"unbalanced nodes\". Depending on the imbalance situation, rotation operations are divided into four types: right rotation, left rotation, left rotation then right rotation, and right rotation then left rotation. Below we describe these rotation operations in detail.

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-right-rotation","level":3,"title":"1.   Right Rotation","text":"

As shown in Figure 7-26, the value below the node is the balance factor. From bottom to top, the first unbalanced node in the binary tree is \"node 3\". We focus on the subtree with this unbalanced node as the root, denoting the node as node and its left child as child, and perform a \"right rotation\" operation. After the right rotation is completed, the subtree regains balance and still maintains the properties of a binary search tree.

<1><2><3><4>

Figure 7-26   Steps of right rotation

As shown in Figure 7-27, when the child node has a right child (denoted as grand_child), a step needs to be added in the right rotation: set grand_child as the left child of node.

Figure 7-27   Right rotation with grand_child

\"Right rotation\" is a figurative term; in practice, it is achieved by modifying node pointers, as shown in the following code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def right_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Right rotation operation\"\"\"\n    child = node.left\n    grand_child = child.right\n    # Using child as pivot, rotate node to the right\n    child.right = node\n    node.left = grand_child\n    # Update node height\n    self.update_height(node)\n    self.update_height(child)\n    # Return root node of subtree after rotation\n    return child\n
avl_tree.cpp
/* Right rotation operation */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child = node->left;\n    TreeNode *grandChild = child->right;\n    // Using child as pivot, rotate node to the right\n    child->right = node;\n    node->left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.java
/* Right rotation operation */\nTreeNode rightRotate(TreeNode node) {\n    TreeNode child = node.left;\n    TreeNode grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.cs
/* Right rotation operation */\nTreeNode? RightRotate(TreeNode? node) {\n    TreeNode? child = node?.left;\n    TreeNode? grandChild = child?.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.go
/* Right rotation operation */\nfunc (t *aVLTree) rightRotate(node *TreeNode) *TreeNode {\n    child := node.Left\n    grandChild := child.Right\n    // Using child as pivot, rotate node to the right\n    child.Right = node\n    node.Left = grandChild\n    // Update node height\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.swift
/* Right rotation operation */\nfunc rightRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.left\n    let grandChild = child?.right\n    // Using child as pivot, rotate node to the right\n    child?.right = node\n    node?.left = grandChild\n    // Update node height\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.js
/* Right rotation operation */\n#rightRotate(node) {\n    const child = node.left;\n    const grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.ts
/* Right rotation operation */\nrightRotate(node: TreeNode): TreeNode {\n    const child = node.left;\n    const grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.dart
/* Right rotation operation */\nTreeNode? rightRotate(TreeNode? node) {\n  TreeNode? child = node!.left;\n  TreeNode? grandChild = child!.right;\n  // Using child as pivot, rotate node to the right\n  child.right = node;\n  node.left = grandChild;\n  // Update node height\n  updateHeight(node);\n  updateHeight(child);\n  // Return root node of subtree after rotation\n  return child;\n}\n
avl_tree.rs
/* Right rotation operation */\nfn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().left.clone().unwrap();\n            let grand_child = child.borrow().right.clone();\n            // Using child as pivot, rotate node to the right\n            child.borrow_mut().right = Some(node.clone());\n            node.borrow_mut().left = grand_child;\n            // Update node height\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Return root node of subtree after rotation\n            Some(child)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* Right rotation operation */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->left;\n    grandChild = child->right;\n    // Using child as pivot, rotate node to the right\n    child->right = node;\n    node->left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.kt
/* Right rotation operation */\nfun rightRotate(node: TreeNode?): TreeNode {\n    val child = node!!.left\n    val grandChild = child!!.right\n    // Using child as pivot, rotate node to the right\n    child.right = node\n    node.left = grandChild\n    // Update node height\n    updateHeight(node)\n    updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.rb
### Right rotation ###\ndef right_rotate(node)\n  child = node.left\n  grand_child = child.right\n  # Using child as pivot, rotate node to the right\n  child.right = node\n  node.left = grand_child\n  # Update node height\n  update_height(node)\n  update_height(child)\n  # Return root node of subtree after rotation\n  child\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-left-rotation","level":3,"title":"2.   Left Rotation","text":"

Correspondingly, if considering the \"mirror\" of the above unbalanced binary tree, the \"left rotation\" operation shown in Figure 7-28 needs to be performed.

Figure 7-28   Left rotation operation

Similarly, as shown in Figure 7-29, when the child node has a left child (denoted as grand_child), a step needs to be added in the left rotation: set grand_child as the right child of node.

Figure 7-29   Left rotation with grand_child

It can be observed that right rotation and left rotation operations are mirror symmetric in logic, and the two imbalance cases they solve are also symmetric. Based on symmetry, we only need to replace all left in the right rotation implementation code with right, and all right with left, to obtain the left rotation implementation code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def left_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Left rotation operation\"\"\"\n    child = node.right\n    grand_child = child.left\n    # Using child as pivot, rotate node to the left\n    child.left = node\n    node.right = grand_child\n    # Update node height\n    self.update_height(node)\n    self.update_height(child)\n    # Return root node of subtree after rotation\n    return child\n
avl_tree.cpp
/* Left rotation operation */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child = node->right;\n    TreeNode *grandChild = child->left;\n    // Using child as pivot, rotate node to the left\n    child->left = node;\n    node->right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.java
/* Left rotation operation */\nTreeNode leftRotate(TreeNode node) {\n    TreeNode child = node.right;\n    TreeNode grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.cs
/* Left rotation operation */\nTreeNode? LeftRotate(TreeNode? node) {\n    TreeNode? child = node?.right;\n    TreeNode? grandChild = child?.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.go
/* Left rotation operation */\nfunc (t *aVLTree) leftRotate(node *TreeNode) *TreeNode {\n    child := node.Right\n    grandChild := child.Left\n    // Using child as pivot, rotate node to the left\n    child.Left = node\n    node.Right = grandChild\n    // Update node height\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.swift
/* Left rotation operation */\nfunc leftRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.right\n    let grandChild = child?.left\n    // Using child as pivot, rotate node to the left\n    child?.left = node\n    node?.right = grandChild\n    // Update node height\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.js
/* Left rotation operation */\n#leftRotate(node) {\n    const child = node.right;\n    const grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.ts
/* Left rotation operation */\nleftRotate(node: TreeNode): TreeNode {\n    const child = node.right;\n    const grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.dart
/* Left rotation operation */\nTreeNode? leftRotate(TreeNode? node) {\n  TreeNode? child = node!.right;\n  TreeNode? grandChild = child!.left;\n  // Using child as pivot, rotate node to the left\n  child.left = node;\n  node.right = grandChild;\n  // Update node height\n  updateHeight(node);\n  updateHeight(child);\n  // Return root node of subtree after rotation\n  return child;\n}\n
avl_tree.rs
/* Left rotation operation */\nfn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().right.clone().unwrap();\n            let grand_child = child.borrow().left.clone();\n            // Using child as pivot, rotate node to the left\n            child.borrow_mut().left = Some(node.clone());\n            node.borrow_mut().right = grand_child;\n            // Update node height\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Return root node of subtree after rotation\n            Some(child)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* Left rotation operation */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->right;\n    grandChild = child->left;\n    // Using child as pivot, rotate node to the left\n    child->left = node;\n    node->right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.kt
/* Left rotation operation */\nfun leftRotate(node: TreeNode?): TreeNode {\n    val child = node!!.right\n    val grandChild = child!!.left\n    // Using child as pivot, rotate node to the left\n    child.left = node\n    node.right = grandChild\n    // Update node height\n    updateHeight(node)\n    updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.rb
### Left rotation ###\ndef left_rotate(node)\n  child = node.right\n  grand_child = child.left\n  # Using child as pivot, rotate node to the left\n  child.left = node\n  node.right = grand_child\n  # Update node height\n  update_height(node)\n  update_height(child)\n  # Return root node of subtree after rotation\n  child\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3-left-rotation-then-right-rotation","level":3,"title":"3.   Left Rotation Then Right Rotation","text":"

For the unbalanced node 3 in Figure 7-30, using either left rotation or right rotation alone cannot restore the subtree to balance. In this case, a \"left rotation\" needs to be performed on child first, followed by a \"right rotation\" on node.

Figure 7-30   Left-right rotation

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4-right-rotation-then-left-rotation","level":3,"title":"4.   Right Rotation Then Left Rotation","text":"

As shown in Figure 7-31, for the mirror case of the above unbalanced binary tree, a \"right rotation\" needs to be performed on child first, then a \"left rotation\" on node.

Figure 7-31   Right-left rotation

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5-choice-of-rotation","level":3,"title":"5.   Choice of Rotation","text":"

The four imbalances shown in Figure 7-32 correspond one-to-one with the above cases, requiring right rotation, left rotation then right rotation, right rotation then left rotation, and left rotation operations respectively.

Figure 7-32   The four rotation cases of AVL tree

As shown in Table 7-3, we determine which case the unbalanced node belongs to by judging the signs of the balance factor of the unbalanced node and the balance factor of its taller-side child node.

Table 7-3   Conditions for Choosing Among the Four Rotation Cases

Balance factor of the unbalanced node Balance factor of the child node Rotation method to apply \\(> 1\\) (left-leaning tree) \\(\\geq 0\\) Right rotation \\(> 1\\) (left-leaning tree) \\(<0\\) Left rotation then right rotation \\(< -1\\) (right-leaning tree) \\(\\leq 0\\) Left rotation \\(< -1\\) (right-leaning tree) \\(>0\\) Right rotation then left rotation

For ease of use, we encapsulate the rotation operations into a function. With this function, we can perform rotations for various imbalance situations, restoring balance to unbalanced nodes. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Perform rotation operation to restore balance to this subtree\"\"\"\n    # Get balance factor of node\n    balance_factor = self.balance_factor(node)\n    # Left-leaning tree\n    if balance_factor > 1:\n        if self.balance_factor(node.left) >= 0:\n            # Right rotation\n            return self.right_rotate(node)\n        else:\n            # First left rotation then right rotation\n            node.left = self.left_rotate(node.left)\n            return self.right_rotate(node)\n    # Right-leaning tree\n    elif balance_factor < -1:\n        if self.balance_factor(node.right) <= 0:\n            # Left rotation\n            return self.left_rotate(node)\n        else:\n            # First right rotation then left rotation\n            node.right = self.right_rotate(node.right)\n            return self.left_rotate(node)\n    # Balanced tree, no rotation needed, return directly\n    return node\n
avl_tree.cpp
/* Perform rotation operation to restore balance to this subtree */\nTreeNode *rotate(TreeNode *node) {\n    // Get balance factor of node\n    int _balanceFactor = balanceFactor(node);\n    // Left-leaning tree\n    if (_balanceFactor > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (_balanceFactor < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.java
/* Perform rotation operation to restore balance to this subtree */\nTreeNode rotate(TreeNode node) {\n    // Get balance factor of node\n    int balanceFactor = balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = leftRotate(node.left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = rightRotate(node.right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.cs
/* Perform rotation operation to restore balance to this subtree */\nTreeNode? Rotate(TreeNode? node) {\n    // Get balance factor of node\n    int balanceFactorInt = BalanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactorInt > 1) {\n        if (BalanceFactor(node?.left) >= 0) {\n            // Right rotation\n            return RightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node!.left = LeftRotate(node!.left);\n            return RightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactorInt < -1) {\n        if (BalanceFactor(node?.right) <= 0) {\n            // Left rotation\n            return LeftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node!.right = RightRotate(node!.right);\n            return LeftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.go
/* Perform rotation operation to restore balance to this subtree */\nfunc (t *aVLTree) rotate(node *TreeNode) *TreeNode {\n    // Get balance factor of node\n    // Go recommends short variables, here bf refers to t.balanceFactor\n    bf := t.balanceFactor(node)\n    // Left-leaning tree\n    if bf > 1 {\n        if t.balanceFactor(node.Left) >= 0 {\n            // Right rotation\n            return t.rightRotate(node)\n        } else {\n            // First left rotation then right rotation\n            node.Left = t.leftRotate(node.Left)\n            return t.rightRotate(node)\n        }\n    }\n    // Right-leaning tree\n    if bf < -1 {\n        if t.balanceFactor(node.Right) <= 0 {\n            // Left rotation\n            return t.leftRotate(node)\n        } else {\n            // First right rotation then left rotation\n            node.Right = t.rightRotate(node.Right)\n            return t.leftRotate(node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
avl_tree.swift
/* Perform rotation operation to restore balance to this subtree */\nfunc rotate(node: TreeNode?) -> TreeNode? {\n    // Get balance factor of node\n    let balanceFactor = balanceFactor(node: node)\n    // Left-leaning tree\n    if balanceFactor > 1 {\n        if self.balanceFactor(node: node?.left) >= 0 {\n            // Right rotation\n            return rightRotate(node: node)\n        } else {\n            // First left rotation then right rotation\n            node?.left = leftRotate(node: node?.left)\n            return rightRotate(node: node)\n        }\n    }\n    // Right-leaning tree\n    if balanceFactor < -1 {\n        if self.balanceFactor(node: node?.right) <= 0 {\n            // Left rotation\n            return leftRotate(node: node)\n        } else {\n            // First right rotation then left rotation\n            node?.right = rightRotate(node: node?.right)\n            return leftRotate(node: node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
avl_tree.js
/* Perform rotation operation to restore balance to this subtree */\n#rotate(node) {\n    // Get balance factor of node\n    const balanceFactor = this.balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return this.#rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = this.#leftRotate(node.left);\n            return this.#rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return this.#leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = this.#rightRotate(node.right);\n            return this.#leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.ts
/* Perform rotation operation to restore balance to this subtree */\nrotate(node: TreeNode): TreeNode {\n    // Get balance factor of node\n    const balanceFactor = this.balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return this.rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = this.leftRotate(node.left);\n            return this.rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return this.leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = this.rightRotate(node.right);\n            return this.leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.dart
/* Perform rotation operation to restore balance to this subtree */\nTreeNode? rotate(TreeNode? node) {\n  // Get balance factor of node\n  int factor = balanceFactor(node);\n  // Left-leaning tree\n  if (factor > 1) {\n    if (balanceFactor(node!.left) >= 0) {\n      // Right rotation\n      return rightRotate(node);\n    } else {\n      // First left rotation then right rotation\n      node.left = leftRotate(node.left);\n      return rightRotate(node);\n    }\n  }\n  // Right-leaning tree\n  if (factor < -1) {\n    if (balanceFactor(node!.right) <= 0) {\n      // Left rotation\n      return leftRotate(node);\n    } else {\n      // First right rotation then left rotation\n      node.right = rightRotate(node.right);\n      return leftRotate(node);\n    }\n  }\n  // Balanced tree, no rotation needed, return directly\n  return node;\n}\n
avl_tree.rs
/* Perform rotation operation to restore balance to this subtree */\nfn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    // Get balance factor of node\n    let balance_factor = Self::balance_factor(node.clone());\n    // Left-leaning tree\n    if balance_factor > 1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().left.clone()) >= 0 {\n            // Right rotation\n            Self::right_rotate(Some(node))\n        } else {\n            // First left rotation then right rotation\n            let left = node.borrow().left.clone();\n            node.borrow_mut().left = Self::left_rotate(left);\n            Self::right_rotate(Some(node))\n        }\n    }\n    // Right-leaning tree\n    else if balance_factor < -1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().right.clone()) <= 0 {\n            // Left rotation\n            Self::left_rotate(Some(node))\n        } else {\n            // First right rotation then left rotation\n            let right = node.borrow().right.clone();\n            node.borrow_mut().right = Self::right_rotate(right);\n            Self::left_rotate(Some(node))\n        }\n    } else {\n        // Balanced tree, no rotation needed, return directly\n        node\n    }\n}\n
avl_tree.c
/* Perform rotation operation to restore balance to this subtree */\nTreeNode *rotate(TreeNode *node) {\n    // Get balance factor of node\n    int bf = balanceFactor(node);\n    // Left-leaning tree\n    if (bf > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (bf < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.kt
/* Perform rotation operation to restore balance to this subtree */\nfun rotate(node: TreeNode): TreeNode {\n    // Get balance factor of node\n    val balanceFactor = balanceFactor(node)\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return rightRotate(node)\n        } else {\n            // First left rotation then right rotation\n            node.left = leftRotate(node.left)\n            return rightRotate(node)\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return leftRotate(node)\n        } else {\n            // First right rotation then left rotation\n            node.right = rightRotate(node.right)\n            return leftRotate(node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
avl_tree.rb
### Perform rotation to rebalance subtree ###\ndef rotate(node)\n  # Get balance factor of node\n  balance_factor = balance_factor(node)\n  # Left-heavy tree\n  if balance_factor > 1\n    if balance_factor(node.left) >= 0\n      # Right rotation\n      return right_rotate(node)\n    else\n      # First left rotation then right rotation\n      node.left = left_rotate(node.left)\n      return right_rotate(node)\n    end\n  # Right-heavy tree\n  elsif balance_factor < -1\n    if balance_factor(node.right) <= 0\n      # Left rotation\n      return left_rotate(node)\n    else\n      # First right rotation then left rotation\n      node.right = right_rotate(node.right)\n      return left_rotate(node)\n    end\n  end\n  # Balanced tree, no rotation needed, return directly\n  node\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#753-common-operations-in-avl-trees","level":2,"title":"7.5.3   Common Operations in Avl Trees","text":"","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-node-insertion","level":3,"title":"1.   Node Insertion","text":"

The node insertion operation in AVL trees is similar in principle to that in binary search trees. The only difference is that after inserting a node in an AVL tree, a series of unbalanced nodes may appear on the path from that node to the root. Therefore, we need to start from this node and perform rotation operations from bottom to top, restoring balance to all unbalanced nodes. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def insert(self, val):\n    \"\"\"Insert node\"\"\"\n    self._root = self.insert_helper(self._root, val)\n\ndef insert_helper(self, node: TreeNode | None, val: int) -> TreeNode:\n    \"\"\"Recursively insert node (helper method)\"\"\"\n    if node is None:\n        return TreeNode(val)\n    # 1. Find insertion position and insert node\n    if val < node.val:\n        node.left = self.insert_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.insert_helper(node.right, val)\n    else:\n        # Duplicate node not inserted, return directly\n        return node\n    # Update node height\n    self.update_height(node)\n    # 2. Perform rotation operation to restore balance to this subtree\n    return self.rotate(node)\n
avl_tree.cpp
/* Insert node */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\n    if (val < node->val)\n        node->left = insertHelper(node->left, val);\n    else if (val > node->val)\n        node->right = insertHelper(node->right, val);\n    else\n        return node;    // Duplicate node not inserted, return directly\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.java
/* Insert node */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode insertHelper(TreeNode node, int val) {\n    if (node == null)\n        return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\n    if (val < node.val)\n        node.left = insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = insertHelper(node.right, val);\n    else\n        return node; // Duplicate node not inserted, return directly\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.cs
/* Insert node */\nvoid Insert(int val) {\n    root = InsertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode? InsertHelper(TreeNode? node, int val) {\n    if (node == null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\n    if (val < node.val)\n        node.left = InsertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = InsertHelper(node.right, val);\n    else\n        return node;     // Duplicate node not inserted, return directly\n    UpdateHeight(node);  // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = Rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.go
/* Insert node */\nfunc (t *aVLTree) insert(val int) {\n    t.root = t.insertHelper(t.root, val)\n}\n\n/* Recursively insert node (helper function) */\nfunc (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return NewTreeNode(val)\n    }\n    /* 1. Find insertion position and insert node */\n    if val < node.Val.(int) {\n        node.Left = t.insertHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.insertHelper(node.Right, val)\n    } else {\n        // Duplicate node not inserted, return directly\n        return node\n    }\n    // Update node height\n    t.updateHeight(node)\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = t.rotate(node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.swift
/* Insert node */\nfunc insert(val: Int) {\n    root = insertHelper(node: root, val: val)\n}\n\n/* Recursively insert node (helper method) */\nfunc insertHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return TreeNode(x: val)\n    }\n    /* 1. Find insertion position and insert node */\n    if val < node!.val {\n        node?.left = insertHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = insertHelper(node: node?.right, val: val)\n    } else {\n        return node // Duplicate node not inserted, return directly\n    }\n    updateHeight(node: node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node: node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.js
/* Insert node */\ninsert(val) {\n    this.root = this.#insertHelper(this.root, val);\n}\n\n/* Recursively insert node (helper method) */\n#insertHelper(node, val) {\n    if (node === null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\n    if (val < node.val) node.left = this.#insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#insertHelper(node.right, val);\n    else return node; // Duplicate node not inserted, return directly\n    this.#updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.#rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.ts
/* Insert node */\ninsert(val: number): void {\n    this.root = this.insertHelper(this.root, val);\n}\n\n/* Recursively insert node (helper method) */\ninsertHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\n    if (val < node.val) {\n        node.left = this.insertHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.insertHelper(node.right, val);\n    } else {\n        return node; // Duplicate node not inserted, return directly\n    }\n    this.updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.dart
/* Insert node */\nvoid insert(int val) {\n  root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode? insertHelper(TreeNode? node, int val) {\n  if (node == null) return TreeNode(val);\n  /* 1. Find insertion position and insert node */\n  if (val < node.val)\n    node.left = insertHelper(node.left, val);\n  else if (val > node.val)\n    node.right = insertHelper(node.right, val);\n  else\n    return node; // Duplicate node not inserted, return directly\n  updateHeight(node); // Update node height\n  /* 2. Perform rotation operation to restore balance to this subtree */\n  node = rotate(node);\n  // Return root node of subtree\n  return node;\n}\n
avl_tree.rs
/* Insert node */\nfn insert(&mut self, val: i32) {\n    self.root = Self::insert_helper(self.root.clone(), val);\n}\n\n/* Recursively insert node (helper method) */\nfn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Find insertion position and insert node */\n            match {\n                let node_val = node.borrow().val;\n                node_val\n            }\n            .cmp(&val)\n            {\n                Ordering::Greater => {\n                    let left = node.borrow().left.clone();\n                    node.borrow_mut().left = Self::insert_helper(left, val);\n                }\n                Ordering::Less => {\n                    let right = node.borrow().right.clone();\n                    node.borrow_mut().right = Self::insert_helper(right, val);\n                }\n                Ordering::Equal => {\n                    return Some(node); // Duplicate node not inserted, return directly\n                }\n            }\n            Self::update_height(Some(node.clone())); // Update node height\n\n            /* 2. Perform rotation operation to restore balance to this subtree */\n            node = Self::rotate(Some(node)).unwrap();\n            // Return root node of subtree\n            Some(node)\n        }\n        None => Some(TreeNode::new(val)),\n    }\n}\n
avl_tree.c
/* Insert node */\nvoid insert(AVLTree *tree, int val) {\n    tree->root = insertHelper(tree->root, val);\n}\n\n/* Recursively insert node (helper function) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == NULL) {\n        return newTreeNode(val);\n    }\n    /* 1. Find insertion position and insert node */\n    if (val < node->val) {\n        node->left = insertHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = insertHelper(node->right, val);\n    } else {\n        // Duplicate node not inserted, return directly\n        return node;\n    }\n    // Update node height\n    updateHeight(node);\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.kt
/* Insert node */\nfun insert(_val: Int) {\n    root = insertHelper(root, _val)\n}\n\n/* Recursively insert node (helper method) */\nfun insertHelper(n: TreeNode?, _val: Int): TreeNode {\n    if (n == null)\n        return TreeNode(_val)\n    var node = n\n    /* 1. Find insertion position and insert node */\n    if (_val < node._val)\n        node.left = insertHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = insertHelper(node.right, _val)\n    else\n        return node // Duplicate node not inserted, return directly\n    updateHeight(node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.rb
### Insert node ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n### Recursively insert node (helper method) ###\ndef insert_helper(node, val)\n  return TreeNode.new(val) if node.nil?\n  # 1. Find insertion position and insert node\n  if val < node.val\n    node.left = insert_helper(node.left, val)\n  elsif val > node.val\n    node.right = insert_helper(node.right, val)\n  else\n    # Duplicate node not inserted, return directly\n    return node\n  end\n  # Update node height\n  update_height(node)\n  # 2. Perform rotation operation to restore balance to this subtree\n  rotate(node)\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-node-removal","level":3,"title":"2.   Node Removal","text":"

Similarly, on the basis of the binary search tree's node removal method, rotation operations need to be performed from bottom to top to restore balance to all unbalanced nodes. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def remove(self, val: int):\n    \"\"\"Delete node\"\"\"\n    self._root = self.remove_helper(self._root, val)\n\ndef remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None:\n    \"\"\"Recursively delete node (helper method)\"\"\"\n    if node is None:\n        return None\n    # 1. Find node and delete\n    if val < node.val:\n        node.left = self.remove_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.remove_helper(node.right, val)\n    else:\n        if node.left is None or node.right is None:\n            child = node.left or node.right\n            # Number of child nodes = 0, delete node directly and return\n            if child is None:\n                return None\n            # Number of child nodes = 1, delete node directly\n            else:\n                node = child\n        else:\n            # Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            temp = node.right\n            while temp.left is not None:\n                temp = temp.left\n            node.right = self.remove_helper(node.right, temp.val)\n            node.val = temp.val\n    # Update node height\n    self.update_height(node)\n    # 2. Perform rotation operation to restore balance to this subtree\n    return self.rotate(node)\n
avl_tree.cpp
/* Remove node */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return nullptr;\n    /* 1. Find node and delete */\n    if (val < node->val)\n        node->left = removeHelper(node->left, val);\n    else if (val > node->val)\n        node->right = removeHelper(node->right, val);\n    else {\n        if (node->left == nullptr || node->right == nullptr) {\n            TreeNode *child = node->left != nullptr ? node->left : node->right;\n            // Number of child nodes = 0, delete node directly and return\n            if (child == nullptr) {\n                delete node;\n                return nullptr;\n            }\n            // Number of child nodes = 1, delete node directly\n            else {\n                delete node;\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            TreeNode *temp = node->right;\n            while (temp->left != nullptr) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.java
/* Remove node */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode removeHelper(TreeNode node, int val) {\n    if (node == null)\n        return null;\n    /* 1. Find node and delete */\n    if (val < node.val)\n        node.left = removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = removeHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode child = node.left != null ? node.left : node.right;\n            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null;\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            TreeNode temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.cs
/* Remove node */\nvoid Remove(int val) {\n    root = RemoveHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode? RemoveHelper(TreeNode? node, int val) {\n    if (node == null) return null;\n    /* 1. Find node and delete */\n    if (val < node.val)\n        node.left = RemoveHelper(node.left, val);\n    else if (val > node.val)\n        node.right = RemoveHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode? child = node.left ?? node.right;\n            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null;\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            TreeNode? temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = RemoveHelper(node.right, temp.val!.Value);\n            node.val = temp.val;\n        }\n    }\n    UpdateHeight(node);  // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = Rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.go
/* Remove node */\nfunc (t *aVLTree) remove(val int) {\n    t.root = t.removeHelper(t.root, val)\n}\n\n/* Recursively remove node (helper function) */\nfunc (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return nil\n    }\n    /* 1. Find node and delete */\n    if val < node.Val.(int) {\n        node.Left = t.removeHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.removeHelper(node.Right, val)\n    } else {\n        if node.Left == nil || node.Right == nil {\n            child := node.Left\n            if node.Right != nil {\n                child = node.Right\n            }\n            if child == nil {\n                // Number of child nodes = 0, delete node directly and return\n                return nil\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            temp := node.Right\n            for temp.Left != nil {\n                temp = temp.Left\n            }\n            node.Right = t.removeHelper(node.Right, temp.Val.(int))\n            node.Val = temp.Val\n        }\n    }\n    // Update node height\n    t.updateHeight(node)\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = t.rotate(node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.swift
/* Remove node */\nfunc remove(val: Int) {\n    root = removeHelper(node: root, val: val)\n}\n\n/* Recursively delete node (helper method) */\nfunc removeHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return nil\n    }\n    /* 1. Find node and delete */\n    if val < node!.val {\n        node?.left = removeHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = removeHelper(node: node?.right, val: val)\n    } else {\n        if node?.left == nil || node?.right == nil {\n            let child = node?.left ?? node?.right\n            // Number of child nodes = 0, delete node directly and return\n            if child == nil {\n                return nil\n            }\n            // Number of child nodes = 1, delete node directly\n            else {\n                node = child\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            var temp = node?.right\n            while temp?.left != nil {\n                temp = temp?.left\n            }\n            node?.right = removeHelper(node: node?.right, val: temp!.val)\n            node?.val = temp!.val\n        }\n    }\n    updateHeight(node: node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node: node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.js
/* Remove node */\nremove(val) {\n    this.root = this.#removeHelper(this.root, val);\n}\n\n/* Recursively delete node (helper method) */\n#removeHelper(node, val) {\n    if (node === null) return null;\n    /* 1. Find node and delete */\n    if (val < node.val) node.left = this.#removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#removeHelper(node.right, val);\n    else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // Number of child nodes = 0, delete node directly and return\n            if (child === null) return null;\n            // Number of child nodes = 1, delete node directly\n            else node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.#removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.#updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.#rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.ts
/* Remove node */\nremove(val: number): void {\n    this.root = this.removeHelper(this.root, val);\n}\n\n/* Recursively delete node (helper method) */\nremoveHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return null;\n    /* 1. Find node and delete */\n    if (val < node.val) {\n        node.left = this.removeHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.removeHelper(node.right, val);\n    } else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // Number of child nodes = 0, delete node directly and return\n            if (child === null) {\n                return null;\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.dart
/* Remove node */\nvoid remove(int val) {\n  root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode? removeHelper(TreeNode? node, int val) {\n  if (node == null) return null;\n  /* 1. Find node and delete */\n  if (val < node.val)\n    node.left = removeHelper(node.left, val);\n  else if (val > node.val)\n    node.right = removeHelper(node.right, val);\n  else {\n    if (node.left == null || node.right == null) {\n      TreeNode? child = node.left ?? node.right;\n      // Number of child nodes = 0, delete node directly and return\n      if (child == null)\n        return null;\n      // Number of child nodes = 1, delete node directly\n      else\n        node = child;\n    } else {\n      // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n      TreeNode? temp = node.right;\n      while (temp!.left != null) {\n        temp = temp.left;\n      }\n      node.right = removeHelper(node.right, temp.val);\n      node.val = temp.val;\n    }\n  }\n  updateHeight(node); // Update node height\n  /* 2. Perform rotation operation to restore balance to this subtree */\n  node = rotate(node);\n  // Return root node of subtree\n  return node;\n}\n
avl_tree.rs
/* Remove node */\nfn remove(&self, val: i32) {\n    Self::remove_helper(self.root.clone(), val);\n}\n\n/* Recursively delete node (helper method) */\nfn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Find node and delete */\n            if val < node.borrow().val {\n                let left = node.borrow().left.clone();\n                node.borrow_mut().left = Self::remove_helper(left, val);\n            } else if val > node.borrow().val {\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, val);\n            } else if node.borrow().left.is_none() || node.borrow().right.is_none() {\n                let child = if node.borrow().left.is_some() {\n                    node.borrow().left.clone()\n                } else {\n                    node.borrow().right.clone()\n                };\n                match child {\n                    // Number of child nodes = 0, delete node directly and return\n                    None => {\n                        return None;\n                    }\n                    // Number of child nodes = 1, delete node directly\n                    Some(child) => node = child,\n                }\n            } else {\n                // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n                let mut temp = node.borrow().right.clone().unwrap();\n                loop {\n                    let temp_left = temp.borrow().left.clone();\n                    if temp_left.is_none() {\n                        break;\n                    }\n                    temp = temp_left.unwrap();\n                }\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val);\n                node.borrow_mut().val = temp.borrow().val;\n            }\n            Self::update_height(Some(node.clone())); // Update node height\n\n            /* 2. Perform rotation operation to restore balance to this subtree */\n            node = Self::rotate(Some(node)).unwrap();\n            // Return root node of subtree\n            Some(node)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* Remove node */\n// Cannot use remove keyword here due to stdio.h inclusion\nvoid removeItem(AVLTree *tree, int val) {\n    TreeNode *root = removeHelper(tree->root, val);\n}\n\n/* Recursively remove node (helper function) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    TreeNode *child, *grandChild;\n    if (node == NULL) {\n        return NULL;\n    }\n    /* 1. Find node and delete */\n    if (val < node->val) {\n        node->left = removeHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = removeHelper(node->right, val);\n    } else {\n        if (node->left == NULL || node->right == NULL) {\n            child = node->left;\n            if (node->right != NULL) {\n                child = node->right;\n            }\n            // Number of child nodes = 0, delete node directly and return\n            if (child == NULL) {\n                return NULL;\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            TreeNode *temp = node->right;\n            while (temp->left != NULL) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    // Update node height\n    updateHeight(node);\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.kt
/* Remove node */\nfun remove(_val: Int) {\n    root = removeHelper(root, _val)\n}\n\n/* Recursively delete node (helper method) */\nfun removeHelper(n: TreeNode?, _val: Int): TreeNode? {\n    var node = n ?: return null\n    /* 1. Find node and delete */\n    if (_val < node._val)\n        node.left = removeHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = removeHelper(node.right, _val)\n    else {\n        if (node.left == null || node.right == null) {\n            val child = if (node.left != null)\n                node.left\n            else\n                node.right\n            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            var temp = node.right\n            while (temp!!.left != null) {\n                temp = temp.left\n            }\n            node.right = removeHelper(node.right, temp._val)\n            node._val = temp._val\n        }\n    }\n    updateHeight(node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.rb
### Delete node ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n### Recursively delete node (helper method) ###\ndef remove_helper(node, val)\n  return if node.nil?\n  # 1. Find node and delete\n  if val < node.val\n    node.left = remove_helper(node.left, val)\n  elsif val > node.val\n    node.right = remove_helper(node.right, val)\n  else\n    if node.left.nil? || node.right.nil?\n      child = node.left || node.right\n      # Number of child nodes = 0, delete node directly and return\n      return if child.nil?\n      # Number of child nodes = 1, delete node directly\n      node = child\n    else\n      # Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n      temp = node.right\n      while !temp.left.nil?\n        temp = temp.left\n      end\n      node.right = remove_helper(node.right, temp.val)\n      node.val = temp.val\n    end\n  end\n  # Update node height\n  update_height(node)\n  # 2. Perform rotation operation to restore balance to this subtree\n  rotate(node)\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3-node-search","level":3,"title":"3.   Node Search","text":"

The node search operation in AVL trees is consistent with that in binary search trees, and will not be elaborated here.

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#754-typical-applications-of-avl-trees","level":2,"title":"7.5.4   Typical Applications of Avl Trees","text":"
  • Organizing and storing large-scale data, suitable for scenarios with high-frequency searches and low-frequency insertions and deletions.
  • Used to build index systems in databases.
  • Red-black trees are also a common type of balanced binary search tree. Compared to AVL trees, red-black trees have more relaxed balance conditions, require fewer rotation operations for node insertion and deletion, and have higher average efficiency for node addition and deletion operations.
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/binary_search_tree/","level":1,"title":"7.4   Binary Search Tree","text":"

As shown in Figure 7-16, a binary search tree satisfies the following conditions.

  1. For the root node, the value of all nodes in the left subtree \\(<\\) the value of the root node \\(<\\) the value of all nodes in the right subtree.
  2. The left and right subtrees of any node are also binary search trees, i.e., they satisfy condition 1. as well.

Figure 7-16   Binary search tree

","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#741-operations-on-a-binary-search-tree","level":2,"title":"7.4.1   Operations on a Binary Search Tree","text":"

We encapsulate the binary search tree as a class BinarySearchTree and declare a member variable root pointing to the tree's root node.

","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#1-searching-for-a-node","level":3,"title":"1.   Searching for a Node","text":"

Given a target node value num, we can search according to the properties of the binary search tree. As shown in Figure 7-17, we declare a node cur and start from the binary tree's root node root, looping to compare the node value cur.val with num.

  • If cur.val < num, it means the target node is in cur's right subtree, thus execute cur = cur.right.
  • If cur.val > num, it means the target node is in cur's left subtree, thus execute cur = cur.left.
  • If cur.val = num, it means the target node is found, exit the loop, and return the node.
<1><2><3><4>

Figure 7-17   Example of searching for a node in a binary search tree

The search operation in a binary search tree works on the same principle as the binary search algorithm, both eliminating half of the cases in each round. The number of loop iterations is at most the height of the binary tree. When the binary tree is balanced, it uses \\(O(\\log n)\\) time. The example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def search(self, num: int) -> TreeNode | None:\n    \"\"\"Search node\"\"\"\n    cur = self._root\n    # Loop search, exit after passing leaf node\n    while cur is not None:\n        # Target node is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Target node is in cur's left subtree\n        elif cur.val > num:\n            cur = cur.left\n        # Found target node, exit loop\n        else:\n            break\n    return cur\n
binary_search_tree.cpp
/* Search node */\nTreeNode *search(int num) {\n    TreeNode *cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Target node is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Target node is in cur's left subtree\n        else if (cur->val > num)\n            cur = cur->left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.java
/* Search node */\nTreeNode search(int num) {\n    TreeNode cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num)\n            cur = cur.left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.cs
/* Search node */\nTreeNode? Search(int num) {\n    TreeNode? cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur =\n            cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num)\n            cur = cur.left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.go
/* Search node */\nfunc (bst *binarySearchTree) search(num int) *TreeNode {\n    node := bst.root\n    // Loop search, exit after passing leaf node\n    for node != nil {\n        if node.Val.(int) < num {\n            // Target node is in cur's right subtree\n            node = node.Right\n        } else if node.Val.(int) > num {\n            // Target node is in cur's left subtree\n            node = node.Left\n        } else {\n            // Found target node, exit loop\n            break\n        }\n    }\n    // Return target node\n    return node\n}\n
binary_search_tree.swift
/* Search node */\nfunc search(num: Int) -> TreeNode? {\n    var cur = root\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Target node is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Target node is in cur's left subtree\n        else if cur!.val > num {\n            cur = cur?.left\n        }\n        // Found target node, exit loop\n        else {\n            break\n        }\n    }\n    // Return target node\n    return cur\n}\n
binary_search_tree.js
/* Search node */\nsearch(num) {\n    let cur = this.root;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num) cur = cur.left;\n        // Found target node, exit loop\n        else break;\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.ts
/* Search node */\nsearch(num: number): TreeNode | null {\n    let cur = this.root;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num) cur = cur.left;\n        // Found target node, exit loop\n        else break;\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.dart
/* Search node */\nTreeNode? search(int _num) {\n  TreeNode? cur = _root;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Target node is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Target node is in cur's left subtree\n    else if (cur.val > _num)\n      cur = cur.left;\n    // Found target node, exit loop\n    else\n      break;\n  }\n  // Return target node\n  return cur;\n}\n
binary_search_tree.rs
/* Search node */\npub fn search(&self, num: i32) -> OptionTreeNodeRc {\n    let mut cur = self.root.clone();\n    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Target node is in cur's right subtree\n            Ordering::Greater => cur = node.borrow().right.clone(),\n            // Target node is in cur's left subtree\n            Ordering::Less => cur = node.borrow().left.clone(),\n            // Found target node, exit loop\n            Ordering::Equal => break,\n        }\n    }\n\n    // Return target node\n    cur\n}\n
binary_search_tree.c
/* Search node */\nTreeNode *search(BinarySearchTree *bst, int num) {\n    TreeNode *cur = bst->root;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        if (cur->val < num) {\n            // Target node is in cur's right subtree\n            cur = cur->right;\n        } else if (cur->val > num) {\n            // Target node is in cur's left subtree\n            cur = cur->left;\n        } else {\n            // Found target node, exit loop\n            break;\n        }\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.kt
/* Search node */\nfun search(num: Int): TreeNode? {\n    var cur = root\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Target node is in cur's left subtree\n        else if (cur._val > num)\n            cur.left\n        // Found target node, exit loop\n        else\n            break\n    }\n    // Return target node\n    return cur\n}\n
binary_search_tree.rb
### Search node ###\ndef search(num)\n  cur = @root\n\n  # Loop search, exit after passing leaf node\n  while !cur.nil?\n    # Target node is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Target node is in cur's left subtree\n    elsif cur.val > num\n      cur = cur.left\n    # Found target node, exit loop\n    else\n      break\n    end\n  end\n\n  cur\nend\n
","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#2-inserting-a-node","level":3,"title":"2.   Inserting a Node","text":"

Given an element num to be inserted, in order to maintain the property of the binary search tree \"left subtree < root node < right subtree,\" the insertion process is as shown in Figure 7-18.

  1. Finding the insertion position: Similar to the search operation, start from the root node and loop downward searching according to the size relationship between the current node value and num, until passing the leaf node (traversing to None) and then exit the loop.
  2. Insert the node at that position: Initialize node num and place it at the None position.

Figure 7-18   Inserting a node into a binary search tree

In the code implementation, note the following two points:

  • Binary search trees do not allow duplicate nodes; otherwise, it would violate its definition. Therefore, if the node to be inserted already exists in the tree, the insertion is not performed and it returns directly.
  • To implement the node insertion, we need to use node pre to save the node from the previous loop iteration. This way, when traversing to None, we can obtain its parent node, thereby completing the node insertion operation.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def insert(self, num: int):\n    \"\"\"Insert node\"\"\"\n    # If tree is empty, initialize root node\n    if self._root is None:\n        self._root = TreeNode(num)\n        return\n    # Loop search, exit after passing leaf node\n    cur, pre = self._root, None\n    while cur is not None:\n        # Found duplicate node, return directly\n        if cur.val == num:\n            return\n        pre = cur\n        # Insertion position is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Insertion position is in cur's left subtree\n        else:\n            cur = cur.left\n    # Insert node\n    node = TreeNode(num)\n    if pre.val < num:\n        pre.right = node\n    else:\n        pre.left = node\n
binary_search_tree.cpp
/* Insert node */\nvoid insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == nullptr) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode *cur = root, *pre = nullptr;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Found duplicate node, return directly\n        if (cur->val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur->left;\n    }\n    // Insert node\n    TreeNode *node = new TreeNode(num);\n    if (pre->val < num)\n        pre->right = node;\n    else\n        pre->left = node;\n}\n
binary_search_tree.java
/* Insert node */\nvoid insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // Insert node\n    TreeNode node = new TreeNode(num);\n    if (pre.val < num)\n        pre.right = node;\n    else\n        pre.left = node;\n}\n
binary_search_tree.cs
/* Insert node */\nvoid Insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode? cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n\n    // Insert node\n    TreeNode node = new(num);\n    if (pre != null) {\n        if (pre.val < num)\n            pre.right = node;\n        else\n            pre.left = node;\n    }\n}\n
binary_search_tree.go
/* Insert node */\nfunc (bst *binarySearchTree) insert(num int) {\n    cur := bst.root\n    // If tree is empty, initialize root node\n    if cur == nil {\n        bst.root = NewTreeNode(num)\n        return\n    }\n    // Node position before the node to be inserted\n    var pre *TreeNode = nil\n    // Loop search, exit after passing leaf node\n    for cur != nil {\n        if cur.Val == num {\n            return\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            cur = cur.Right\n        } else {\n            cur = cur.Left\n        }\n    }\n    // Insert node\n    node := NewTreeNode(num)\n    if pre.Val.(int) < num {\n        pre.Right = node\n    } else {\n        pre.Left = node\n    }\n}\n
binary_search_tree.swift
/* Insert node */\nfunc insert(num: Int) {\n    // If tree is empty, initialize root node\n    if root == nil {\n        root = TreeNode(x: num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Found duplicate node, return directly\n        if cur!.val == num {\n            return\n        }\n        pre = cur\n        // Insertion position is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Insertion position is in cur's left subtree\n        else {\n            cur = cur?.left\n        }\n    }\n    // Insert node\n    let node = TreeNode(x: num)\n    if pre!.val < num {\n        pre?.right = node\n    } else {\n        pre?.left = node\n    }\n}\n
binary_search_tree.js
/* Insert node */\ninsert(num) {\n    // If tree is empty, initialize root node\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur = this.root,\n        pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found duplicate node, return directly\n        if (cur.val === num) return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else cur = cur.left;\n    }\n    // Insert node\n    const node = new TreeNode(num);\n    if (pre.val < num) pre.right = node;\n    else pre.left = node;\n}\n
binary_search_tree.ts
/* Insert node */\ninsert(num: number): void {\n    // If tree is empty, initialize root node\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found duplicate node, return directly\n        if (cur.val === num) return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else cur = cur.left;\n    }\n    // Insert node\n    const node = new TreeNode(num);\n    if (pre!.val < num) pre!.right = node;\n    else pre!.left = node;\n}\n
binary_search_tree.dart
/* Insert node */\nvoid insert(int _num) {\n  // If tree is empty, initialize root node\n  if (_root == null) {\n    _root = TreeNode(_num);\n    return;\n  }\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Found duplicate node, return directly\n    if (cur.val == _num) return;\n    pre = cur;\n    // Insertion position is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Insertion position is in cur's left subtree\n    else\n      cur = cur.left;\n  }\n  // Insert node\n  TreeNode? node = TreeNode(_num);\n  if (pre!.val < _num)\n    pre.right = node;\n  else\n    pre.left = node;\n}\n
binary_search_tree.rs
/* Insert node */\npub fn insert(&mut self, num: i32) {\n    // If tree is empty, initialize root node\n    if self.root.is_none() {\n        self.root = Some(TreeNode::new(num));\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Found duplicate node, return directly\n            Ordering::Equal => return,\n            // Insertion position is in cur's right subtree\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Insertion position is in cur's left subtree\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // Insert node\n    let pre = pre.unwrap();\n    let node = Some(TreeNode::new(num));\n    if num > pre.borrow().val {\n        pre.borrow_mut().right = node;\n    } else {\n        pre.borrow_mut().left = node;\n    }\n}\n
binary_search_tree.c
/* Insert node */\nvoid insert(BinarySearchTree *bst, int num) {\n    // If tree is empty, initialize root node\n    if (bst->root == NULL) {\n        bst->root = newTreeNode(num);\n        return;\n    }\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        // Found duplicate node, return directly\n        if (cur->val == num) {\n            return;\n        }\n        pre = cur;\n        if (cur->val < num) {\n            // Insertion position is in cur's right subtree\n            cur = cur->right;\n        } else {\n            // Insertion position is in cur's left subtree\n            cur = cur->left;\n        }\n    }\n    // Insert node\n    TreeNode *node = newTreeNode(num);\n    if (pre->val < num) {\n        pre->right = node;\n    } else {\n        pre->left = node;\n    }\n}\n
binary_search_tree.kt
/* Insert node */\nfun insert(num: Int) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = TreeNode(num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode? = null\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur._val == num)\n            return\n        pre = cur\n        // Insertion position is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Insertion position is in cur's left subtree\n        else\n            cur.left\n    }\n    // Insert node\n    val node = TreeNode(num)\n    if (pre?._val!! < num)\n        pre.right = node\n    else\n        pre.left = node\n}\n
binary_search_tree.rb
### Insert node ###\ndef insert(num)\n  # If tree is empty, initialize root node\n  if @root.nil?\n    @root = TreeNode.new(num)\n    return\n  end\n\n  # Loop search, exit after passing leaf node\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Found duplicate node, return directly\n    return if cur.val == num\n\n    pre = cur\n    # Insertion position is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Insertion position is in cur's left subtree\n    else\n      cur = cur.left\n    end\n  end\n\n  # Insert node\n  node = TreeNode.new(num)\n  if pre.val < num\n    pre.right = node\n  else\n    pre.left = node\n  end\nend\n

Similar to searching for a node, inserting a node uses \\(O(\\log n)\\) time.

","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#3-removing-a-node","level":3,"title":"3.   Removing a Node","text":"

First, find the target node in the binary tree, then remove it. Similar to node insertion, we need to ensure that after the removal operation is completed, the binary search tree's property of \"left subtree \\(<\\) root node \\(<\\) right subtree\" is still maintained. Therefore, depending on the number of child nodes the target node has, we divide it into 0, 1, and 2 three cases, and execute the corresponding node removal operations.

As shown in Figure 7-19, when the degree of the node to be removed is \\(0\\), it means the node is a leaf node and can be directly removed.

Figure 7-19   Removing a node in a binary search tree (degree 0)

As shown in Figure 7-20, when the degree of the node to be removed is \\(1\\), replacing the node to be removed with its child node is sufficient.

Figure 7-20   Removing a node in a binary search tree (degree 1)

When the degree of the node to be removed is \\(2\\), we cannot directly remove it; instead, we need to use a node to replace it. To maintain the binary search tree's property of \"left subtree \\(<\\) root node \\(<\\) right subtree,\" this node can be either the smallest node in the right subtree or the largest node in the left subtree.

Assuming we choose the smallest node in the right subtree (the next node in the inorder traversal), the removal process is as shown in Figure 7-21.

  1. Find the next node of the node to be removed in the \"inorder traversal sequence,\" denoted as tmp.
  2. Replace the value of the node to be removed with the value of tmp, and recursively remove node tmp in the tree.
<1><2><3><4>

Figure 7-21   Removing a node in a binary search tree (degree 2)

The node removal operation also uses \\(O(\\log n)\\) time, where finding the node to be removed requires \\(O(\\log n)\\) time, and obtaining the inorder successor node requires \\(O(\\log n)\\) time. Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def remove(self, num: int):\n    \"\"\"Delete node\"\"\"\n    # If tree is empty, return directly\n    if self._root is None:\n        return\n    # Loop search, exit after passing leaf node\n    cur, pre = self._root, None\n    while cur is not None:\n        # Found node to delete, exit loop\n        if cur.val == num:\n            break\n        pre = cur\n        # Node to delete is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Node to delete is in cur's left subtree\n        else:\n            cur = cur.left\n    # If no node to delete, return directly\n    if cur is None:\n        return\n\n    # Number of child nodes = 0 or 1\n    if cur.left is None or cur.right is None:\n        # When number of child nodes = 0 / 1, child = null / that child node\n        child = cur.left or cur.right\n        # Delete node cur\n        if cur != self._root:\n            if pre.left == cur:\n                pre.left = child\n            else:\n                pre.right = child\n        else:\n            # If deleted node is root node, reassign root node\n            self._root = child\n    # Number of child nodes = 2\n    else:\n        # Get next node of cur in inorder traversal\n        tmp: TreeNode = cur.right\n        while tmp.left is not None:\n            tmp = tmp.left\n        # Recursively delete node tmp\n        self.remove(tmp.val)\n        # Replace cur with tmp\n        cur.val = tmp.val\n
binary_search_tree.cpp
/* Remove node */\nvoid remove(int num) {\n    // If tree is empty, return directly\n    if (root == nullptr)\n        return;\n    TreeNode *cur = root, *pre = nullptr;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Found node to delete, exit loop\n        if (cur->val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur->left;\n    }\n    // If no node to delete, return directly\n    if (cur == nullptr)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur->left == nullptr || cur->right == nullptr) {\n        // When number of child nodes = 0 / 1, child = nullptr / that child node\n        TreeNode *child = cur->left != nullptr ? cur->left : cur->right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre->left == cur)\n                pre->left = child;\n            else\n                pre->right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n        // Free memory\n        delete cur;\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode *tmp = cur->right;\n        while (tmp->left != nullptr) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Recursively delete node tmp\n        remove(tmp->val);\n        // Replace cur with tmp\n        cur->val = tmpVal;\n    }\n}\n
binary_search_tree.java
/* Remove node */\nvoid remove(int num) {\n    // If tree is empty, return directly\n    if (root == null)\n        return;\n    TreeNode cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        TreeNode child = cur.left != null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        remove(tmp.val);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.cs
/* Remove node */\nvoid Remove(int num) {\n    // If tree is empty, return directly\n    if (root == null)\n        return;\n    TreeNode? cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        TreeNode? child = cur.left ?? cur.right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre!.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode? tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        Remove(tmp.val!.Value);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.go
/* Remove node */\nfunc (bst *binarySearchTree) remove(num int) {\n    cur := bst.root\n    // If tree is empty, return directly\n    if cur == nil {\n        return\n    }\n    // Node position before the node to be removed\n    var pre *TreeNode = nil\n    // Loop search, exit after passing leaf node\n    for cur != nil {\n        if cur.Val == num {\n            break\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            // Node to be removed is in right subtree\n            cur = cur.Right\n        } else {\n            // Node to be removed is in left subtree\n            cur = cur.Left\n        }\n    }\n    // If no node to delete, return directly\n    if cur == nil {\n        return\n    }\n    // Number of child nodes is 0 or 1\n    if cur.Left == nil || cur.Right == nil {\n        var child *TreeNode = nil\n        // Get child node of node to be removed\n        if cur.Left != nil {\n            child = cur.Left\n        } else {\n            child = cur.Right\n        }\n        // Delete node cur\n        if cur != bst.root {\n            if pre.Left == cur {\n                pre.Left = child\n            } else {\n                pre.Right = child\n            }\n        } else {\n            // If deleted node is root node, reassign root node\n            bst.root = child\n        }\n        // Number of child nodes is 2\n    } else {\n        // Get next node of node cur to be removed in in-order traversal\n        tmp := cur.Right\n        for tmp.Left != nil {\n            tmp = tmp.Left\n        }\n        // Recursively delete node tmp\n        bst.remove(tmp.Val.(int))\n        // Replace cur with tmp\n        cur.Val = tmp.Val\n    }\n}\n
binary_search_tree.swift
/* Remove node */\nfunc remove(num: Int) {\n    // If tree is empty, return directly\n    if root == nil {\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Found node to delete, exit loop\n        if cur!.val == num {\n            break\n        }\n        pre = cur\n        // Node to delete is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Node to delete is in cur's left subtree\n        else {\n            cur = cur?.left\n        }\n    }\n    // If no node to delete, return directly\n    if cur == nil {\n        return\n    }\n    // Number of child nodes = 0 or 1\n    if cur?.left == nil || cur?.right == nil {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        let child = cur?.left ?? cur?.right\n        // Delete node cur\n        if cur !== root {\n            if pre?.left === cur {\n                pre?.left = child\n            } else {\n                pre?.right = child\n            }\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        var tmp = cur?.right\n        while tmp?.left != nil {\n            tmp = tmp?.left\n        }\n        // Recursively delete node tmp\n        remove(num: tmp!.val)\n        // Replace cur with tmp\n        cur?.val = tmp!.val\n    }\n}\n
binary_search_tree.js
/* Remove node */\nremove(num) {\n    // If tree is empty, return directly\n    if (this.root === null) return;\n    let cur = this.root,\n        pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found node to delete, exit loop\n        if (cur.val === num) break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur === null) return;\n    // Number of child nodes = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        const child = cur.left !== null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur !== this.root) {\n            if (pre.left === cur) pre.left = child;\n            else pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            this.root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        let tmp = cur.right;\n        while (tmp.left !== null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        this.remove(tmp.val);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.ts
/* Remove node */\nremove(num: number): void {\n    // If tree is empty, return directly\n    if (this.root === null) return;\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found node to delete, exit loop\n        if (cur.val === num) break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur === null) return;\n    // Number of child nodes = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        const child: TreeNode | null =\n            cur.left !== null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur !== this.root) {\n            if (pre!.left === cur) pre!.left = child;\n            else pre!.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            this.root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        let tmp: TreeNode | null = cur.right;\n        while (tmp!.left !== null) {\n            tmp = tmp!.left;\n        }\n        // Recursively delete node tmp\n        this.remove(tmp!.val);\n        // Replace cur with tmp\n        cur.val = tmp!.val;\n    }\n}\n
binary_search_tree.dart
/* Remove node */\nvoid remove(int _num) {\n  // If tree is empty, return directly\n  if (_root == null) return;\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Found node to delete, exit loop\n    if (cur.val == _num) break;\n    pre = cur;\n    // Node to delete is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Node to delete is in cur's left subtree\n    else\n      cur = cur.left;\n  }\n  // If no node to delete, return directly\n  if (cur == null) return;\n  // Number of child nodes = 0 or 1\n  if (cur.left == null || cur.right == null) {\n    // When number of child nodes = 0 / 1, child = null / that child node\n    TreeNode? child = cur.left ?? cur.right;\n    // Delete node cur\n    if (cur != _root) {\n      if (pre!.left == cur)\n        pre.left = child;\n      else\n        pre.right = child;\n    } else {\n      // If deleted node is root node, reassign root node\n      _root = child;\n    }\n  } else {\n    // Number of child nodes = 2\n    // Get next node of cur in inorder traversal\n    TreeNode? tmp = cur.right;\n    while (tmp!.left != null) {\n      tmp = tmp.left;\n    }\n    // Recursively delete node tmp\n    remove(tmp.val);\n    // Replace cur with tmp\n    cur.val = tmp.val;\n  }\n}\n
binary_search_tree.rs
/* Remove node */\npub fn remove(&mut self, num: i32) {\n    // If tree is empty, return directly\n    if self.root.is_none() {\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Found node to delete, exit loop\n            Ordering::Equal => break,\n            // Node to delete is in cur's right subtree\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Node to delete is in cur's left subtree\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // If no node to delete, return directly\n    if cur.is_none() {\n        return;\n    }\n    let cur = cur.unwrap();\n    let (left_child, right_child) = (cur.borrow().left.clone(), cur.borrow().right.clone());\n    match (left_child.clone(), right_child.clone()) {\n        // Number of child nodes = 0 or 1\n        (None, None) | (Some(_), None) | (None, Some(_)) => {\n            // When number of child nodes = 0 / 1, child = nullptr / that child node\n            let child = left_child.or(right_child);\n            let pre = pre.unwrap();\n            // Delete node cur\n            if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) {\n                let left = pre.borrow().left.clone();\n                if left.is_some() && Rc::ptr_eq(left.as_ref().unwrap(), &cur) {\n                    pre.borrow_mut().left = child;\n                } else {\n                    pre.borrow_mut().right = child;\n                }\n            } else {\n                // If deleted node is root node, reassign root node\n                self.root = child;\n            }\n        }\n        // Number of child nodes = 2\n        (Some(_), Some(_)) => {\n            // Get next node of cur in inorder traversal\n            let mut tmp = cur.borrow().right.clone();\n            while let Some(node) = tmp.clone() {\n                if node.borrow().left.is_some() {\n                    tmp = node.borrow().left.clone();\n                } else {\n                    break;\n                }\n            }\n            let tmp_val = tmp.unwrap().borrow().val;\n            // Recursively delete node tmp\n            self.remove(tmp_val);\n            // Replace cur with tmp\n            cur.borrow_mut().val = tmp_val;\n        }\n    }\n}\n
binary_search_tree.c
/* Remove node */\n// Cannot use remove keyword here due to stdio.h inclusion\nvoid removeItem(BinarySearchTree *bst, int num) {\n    // If tree is empty, return directly\n    if (bst->root == NULL)\n        return;\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        // Found node to delete, exit loop\n        if (cur->val == num)\n            break;\n        pre = cur;\n        if (cur->val < num) {\n            // Node to delete is in right subtree of root\n            cur = cur->right;\n        } else {\n            // Node to delete is in left subtree of root\n            cur = cur->left;\n        }\n    }\n    // If no node to delete, return directly\n    if (cur == NULL)\n        return;\n    // Check if node to delete has children\n    if (cur->left == NULL || cur->right == NULL) {\n        /* Number of child nodes = 0 or 1 */\n        // When number of child nodes = 0 / 1, child = nullptr / that child node\n        TreeNode *child = cur->left != NULL ? cur->left : cur->right;\n        // Delete node cur\n        if (pre->left == cur) {\n            pre->left = child;\n        } else {\n            pre->right = child;\n        }\n        // Free memory\n        free(cur);\n    } else {\n        /* Number of child nodes = 2 */\n        // Get next node of cur in inorder traversal\n        TreeNode *tmp = cur->right;\n        while (tmp->left != NULL) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Recursively delete node tmp\n        removeItem(bst, tmp->val);\n        // Replace cur with tmp\n        cur->val = tmpVal;\n    }\n}\n
binary_search_tree.kt
/* Remove node */\nfun remove(num: Int) {\n    // If tree is empty, return directly\n    if (root == null)\n        return\n    var cur = root\n    var pre: TreeNode? = null\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur._val == num)\n            break\n        pre = cur\n        // Node to delete is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Node to delete is in cur's left subtree\n        else\n            cur.left\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        val child = if (cur.left != null)\n            cur.left\n        else\n            cur.right\n        // Delete node cur\n        if (cur != root) {\n            if (pre!!.left == cur)\n                pre.left = child\n            else\n                pre.right = child\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child\n        }\n        // Number of child nodes = 2\n    } else {\n        // Get next node of cur in inorder traversal\n        var tmp = cur.right\n        while (tmp!!.left != null) {\n            tmp = tmp.left\n        }\n        // Recursively delete node tmp\n        remove(tmp._val)\n        // Replace cur with tmp\n        cur._val = tmp._val\n    }\n}\n
binary_search_tree.rb
### Delete node ###\ndef remove(num)\n  # If tree is empty, return directly\n  return if @root.nil?\n\n  # Loop search, exit after passing leaf node\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Found node to delete, exit loop\n    break if cur.val == num\n\n    pre = cur\n    # Node to delete is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Node to delete is in cur's left subtree\n    else\n      cur = cur.left\n    end\n  end\n  # If no node to delete, return directly\n  return if cur.nil?\n\n  # Number of child nodes = 0 or 1\n  if cur.left.nil? || cur.right.nil?\n    # When number of child nodes = 0 / 1, child = null / that child node\n    child = cur.left || cur.right\n    # Delete node cur\n    if cur != @root\n      if pre.left == cur\n        pre.left = child\n      else\n        pre.right = child\n      end\n    else\n      # If deleted node is root node, reassign root node\n      @root = child\n    end\n  # Number of child nodes = 2\n  else\n    # Get next node of cur in inorder traversal\n    tmp = cur.right\n    while !tmp.left.nil?\n      tmp = tmp.left\n    end\n    # Recursively delete node tmp\n    remove(tmp.val)\n    # Replace cur with tmp\n    cur.val = tmp.val\n  end\nend\n
","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#4-inorder-traversal-is-ordered","level":3,"title":"4.   Inorder Traversal Is Ordered","text":"

As shown in Figure 7-22, the inorder traversal of a binary tree follows the \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\" traversal order, while the binary search tree satisfies the \"left child node \\(<\\) root node \\(<\\) right child node\" size relationship.

This means that when performing an inorder traversal in a binary search tree, the next smallest node is always traversed first, thus yielding an important property: The inorder traversal sequence of a binary search tree is ascending.

Using the property of inorder traversal being ascending, we can obtain ordered data in a binary search tree in only \\(O(n)\\) time, without the need for additional sorting operations, which is very efficient.

Figure 7-22   Inorder traversal sequence of a binary search tree

","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#742-efficiency-of-binary-search-trees","level":2,"title":"7.4.2   Efficiency of Binary Search Trees","text":"

Given a set of data, we consider using an array or a binary search tree for storage. Observing Table 7-2, all operations in a binary search tree have logarithmic time complexity, providing stable and efficient performance. Arrays are more efficient than binary search trees only in scenarios with high-frequency additions and low-frequency searches and deletions.

Table 7-2   Efficiency comparison between arrays and search trees

Unsorted array Binary search tree Search element \\(O(n)\\) \\(O(\\log n)\\) Insert element \\(O(1)\\) \\(O(\\log n)\\) Remove element \\(O(n)\\) \\(O(\\log n)\\)

In the ideal case, a binary search tree is \"balanced,\" such that any node can be found within \\(\\log n\\) loop iterations.

However, if we continuously insert and remove nodes in a binary search tree, it may degenerate into a linked list as shown in Figure 7-23, where the time complexity of various operations also degrades to \\(O(n)\\).

Figure 7-23   Degradation of a binary search tree

","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#743-common-applications-of-binary-search-trees","level":2,"title":"7.4.3   Common Applications of Binary Search Trees","text":"
  • Used as multi-level indexes in systems to implement efficient search, insertion, and removal operations.
  • Serves as the underlying data structure for certain search algorithms.
  • Used to store data streams to maintain their ordered state.
","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/","level":1,"title":"7.1   Binary Tree","text":"

A binary tree is a non-linear data structure that represents the derivation relationship between \"ancestors\" and \"descendants\" and embodies the divide-and-conquer logic of \"one divides into two\". Similar to a linked list, the basic unit of a binary tree is a node, and each node contains a value, a reference to its left child node, and a reference to its right child node.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class TreeNode:\n    \"\"\"Binary tree node\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Node value\n        self.left: TreeNode | None = None  # Reference to left child node\n        self.right: TreeNode | None = None # Reference to right child node\n
/* Binary tree node */\nstruct TreeNode {\n    int val;          // Node value\n    TreeNode *left;   // Pointer to left child node\n    TreeNode *right;  // Pointer to right child node\n    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}\n};\n
/* Binary tree node */\nclass TreeNode {\n    int val;         // Node value\n    TreeNode left;   // Reference to left child node\n    TreeNode right;  // Reference to right child node\n    TreeNode(int x) { val = x; }\n}\n
/* Binary tree node */\nclass TreeNode(int? x) {\n    public int? val = x;    // Node value\n    public TreeNode? left;  // Reference to left child node\n    public TreeNode? right; // Reference to right child node\n}\n
/* Binary tree node */\ntype TreeNode struct {\n    Val   int\n    Left  *TreeNode\n    Right *TreeNode\n}\n/* Constructor */\nfunc NewTreeNode(v int) *TreeNode {\n    return &TreeNode{\n        Left:  nil, // Pointer to left child node\n        Right: nil, // Pointer to right child node\n        Val:   v,   // Node value\n    }\n}\n
/* Binary tree node */\nclass TreeNode {\n    var val: Int // Node value\n    var left: TreeNode? // Reference to left child node\n    var right: TreeNode? // Reference to right child node\n\n    init(x: Int) {\n        val = x\n    }\n}\n
/* Binary tree node */\nclass TreeNode {\n    val; // Node value\n    left; // Pointer to left child node\n    right; // Pointer to right child node\n    constructor(val, left, right) {\n        this.val = val === undefined ? 0 : val;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* Binary tree node */\nclass TreeNode {\n    val: number;\n    left: TreeNode | null;\n    right: TreeNode | null;\n\n    constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val; // Node value\n        this.left = left === undefined ? null : left; // Reference to left child node\n        this.right = right === undefined ? null : right; // Reference to right child node\n    }\n}\n
/* Binary tree node */\nclass TreeNode {\n  int val;         // Node value\n  TreeNode? left;  // Reference to left child node\n  TreeNode? right; // Reference to right child node\n  TreeNode(this.val, [this.left, this.right]);\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Binary tree node */\nstruct TreeNode {\n    val: i32,                               // Node value\n    left: Option<Rc<RefCell<TreeNode>>>,    // Reference to left child node\n    right: Option<Rc<RefCell<TreeNode>>>,   // Reference to right child node\n}\n\nimpl TreeNode {\n    /* Constructor */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            left: None,\n            right: None\n        }))\n    }\n}\n
/* Binary tree node */\ntypedef struct TreeNode {\n    int val;                // Node value\n    int height;             // Node height\n    struct TreeNode *left;  // Pointer to left child node\n    struct TreeNode *right; // Pointer to right child node\n} TreeNode;\n\n/* Constructor */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
/* Binary tree node */\nclass TreeNode(val _val: Int) {  // Node value\n    val left: TreeNode? = null   // Reference to left child node\n    val right: TreeNode? = null  // Reference to right child node\n}\n
### Binary tree node class ###\nclass TreeNode\n  attr_accessor :val    # Node value\n  attr_accessor :left   # Reference to left child node\n  attr_accessor :right  # Reference to right child node\n\n  def initialize(val)\n    @val = val\n  end\nend\n

Each node has two references (pointers), pointing respectively to the left-child node and right-child node. This node is called the parent node of these two child nodes. When given a node of a binary tree, we call the tree formed by this node's left child and all nodes below it the left subtree of this node. Similarly, the right subtree can be defined.

In a binary tree, except leaf nodes, all other nodes contain child nodes and non-empty subtrees. As shown in Figure 7-1, if \"Node 2\" is regarded as a parent node, its left and right child nodes are \"Node 4\" and \"Node 5\" respectively. The left subtree is formed by \"Node 4\" and all nodes beneath it, while the right subtree is formed by \"Node 5\" and all nodes beneath it.

Figure 7-1   Parent Node, child Node, subtree

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#711-common-terminology-of-binary-trees","level":2,"title":"7.1.1   Common Terminology of Binary Trees","text":"

The commonly used terminology of binary trees is shown in Figure 7-2.

  • Root node: The node at the top level of a binary tree, which does not have a parent node.
  • Leaf node: A node that does not have any child nodes, with both of its pointers pointing to None.
  • Edge: A line segment that connects two nodes, representing a reference (pointer) between the nodes.
  • The level of a node: It increases from top to bottom, with the root node being at level 1.
  • The degree of a node: The number of child nodes that a node has. In a binary tree, the degree can be 0, 1, or 2.
  • The height of a binary tree: The number of edges from the root node to the farthest leaf node.
  • The depth of a node: The number of edges from the root node to the node.
  • The height of a node: The number of edges from the farthest leaf node to the node.

Figure 7-2   Common Terminology of Binary Trees

Tip

Please note that we usually define \"height\" and \"depth\" as \"the number of edges traversed\", but some questions or textbooks may define them as \"the number of nodes traversed\". In this case, both height and depth need to be incremented by 1.

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#712-basic-operations-of-binary-trees","level":2,"title":"7.1.2   Basic Operations of Binary Trees","text":"","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#1-initializing-a-binary-tree","level":3,"title":"1.   Initializing a Binary Tree","text":"

Similar to a linked list, the initialization of a binary tree involves first creating the nodes and then establishing the references (pointers) between them.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
# Initializing a binary tree\n# Initializing nodes\nn1 = TreeNode(val=1)\nn2 = TreeNode(val=2)\nn3 = TreeNode(val=3)\nn4 = TreeNode(val=4)\nn5 = TreeNode(val=5)\n# Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.cpp
/* Initializing a binary tree */\n// Initializing nodes\nTreeNode* n1 = new TreeNode(1);\nTreeNode* n2 = new TreeNode(2);\nTreeNode* n3 = new TreeNode(3);\nTreeNode* n4 = new TreeNode(4);\nTreeNode* n5 = new TreeNode(5);\n// Linking references (pointers) between nodes\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
binary_tree.java
// Initializing nodes\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.cs
/* Initializing a binary tree */\n// Initializing nodes\nTreeNode n1 = new(1);\nTreeNode n2 = new(2);\nTreeNode n3 = new(3);\nTreeNode n4 = new(4);\nTreeNode n5 = new(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.go
/* Initializing a binary tree */\n// Initializing nodes\nn1 := NewTreeNode(1)\nn2 := NewTreeNode(2)\nn3 := NewTreeNode(3)\nn4 := NewTreeNode(4)\nn5 := NewTreeNode(5)\n// Linking references (pointers) between nodes\nn1.Left = n2\nn1.Right = n3\nn2.Left = n4\nn2.Right = n5\n
binary_tree.swift
// Initializing nodes\nlet n1 = TreeNode(x: 1)\nlet n2 = TreeNode(x: 2)\nlet n3 = TreeNode(x: 3)\nlet n4 = TreeNode(x: 4)\nlet n5 = TreeNode(x: 5)\n// Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.js
/* Initializing a binary tree */\n// Initializing nodes\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.ts
/* Initializing a binary tree */\n// Initializing nodes\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.dart
/* Initializing a binary tree */\n// Initializing nodes\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.rs
// Initializing nodes\nlet n1 = TreeNode::new(1);\nlet n2 = TreeNode::new(2);\nlet n3 = TreeNode::new(3);\nlet n4 = TreeNode::new(4);\nlet n5 = TreeNode::new(5);\n// Linking references (pointers) between nodes\nn1.borrow_mut().left = Some(n2.clone());\nn1.borrow_mut().right = Some(n3);\nn2.borrow_mut().left = Some(n4);\nn2.borrow_mut().right = Some(n5);\n
binary_tree.c
/* Initializing a binary tree */\n// Initializing nodes\nTreeNode *n1 = newTreeNode(1);\nTreeNode *n2 = newTreeNode(2);\nTreeNode *n3 = newTreeNode(3);\nTreeNode *n4 = newTreeNode(4);\nTreeNode *n5 = newTreeNode(5);\n// Linking references (pointers) between nodes\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
binary_tree.kt
// Initializing nodes\nval n1 = TreeNode(1)\nval n2 = TreeNode(2)\nval n3 = TreeNode(3)\nval n4 = TreeNode(4)\nval n5 = TreeNode(5)\n// Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.rb
# Initializing a binary tree\n# Initializing nodes\nn1 = TreeNode.new(1)\nn2 = TreeNode.new(2)\nn3 = TreeNode.new(3)\nn4 = TreeNode.new(4)\nn5 = TreeNode.new(5)\n# Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
Code Visualization

Full Screen >

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#2-inserting-and-removing-nodes","level":3,"title":"2.   Inserting and Removing Nodes","text":"

Similar to a linked list, inserting and removing nodes in a binary tree can be achieved by modifying pointers. Figure 7-3 provides an example.

Figure 7-3   Inserting and removing nodes in a binary tree

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
# Inserting and removing nodes\np = TreeNode(0)\n# Inserting node P between n1 -> n2\nn1.left = p\np.left = n2\n# Removing node P\nn1.left = n2\n
binary_tree.cpp
/* Inserting and removing nodes */\nTreeNode* P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1->left = P;\nP->left = n2;\n// Removing node P\nn1->left = n2;\n
binary_tree.java
TreeNode P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
binary_tree.cs
/* Inserting and removing nodes */\nTreeNode P = new(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
binary_tree.go
/* Inserting and removing nodes */\n// Inserting node P between n1 and n2\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// Removing node P\nn1.Left = n2\n
binary_tree.swift
let P = TreeNode(x: 0)\n// Inserting node P between n1 and n2\nn1.left = P\nP.left = n2\n// Removing node P\nn1.left = n2\n
binary_tree.js
/* Inserting and removing nodes */\nlet P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
binary_tree.ts
/* Inserting and removing nodes */\nconst P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
binary_tree.dart
/* Inserting and removing nodes */\nTreeNode P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
binary_tree.rs
let p = TreeNode::new(0);\n// Inserting node P between n1 and n2\nn1.borrow_mut().left = Some(p.clone());\np.borrow_mut().left = Some(n2.clone());\n// Removing node P\nn1.borrow_mut().left = Some(n2);\n
binary_tree.c
/* Inserting and removing nodes */\nTreeNode *P = newTreeNode(0);\n// Inserting node P between n1 and n2\nn1->left = P;\nP->left = n2;\n// Removing node P\nn1->left = n2;\n
binary_tree.kt
val P = TreeNode(0)\n// Inserting node P between n1 and n2\nn1.left = P\nP.left = n2\n// Removing node P\nn1.left = n2\n
binary_tree.rb
# Inserting and removing nodes\n_p = TreeNode.new(0)\n# Inserting node _p between n1 and n2\nn1.left = _p\n_p.left = n2\n# Removing node _p\nn1.left = n2\n
Code Visualization

Full Screen >

Tip

It should be noted that inserting nodes may change the original logical structure of the binary tree, while removing nodes typically involves removing the node and all its subtrees. Therefore, in a binary tree, insertion and removal are usually performed through a set of operations to achieve meaningful outcomes.

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#713-common-types-of-binary-trees","level":2,"title":"7.1.3   Common Types of Binary Trees","text":"","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#1-perfect-binary-tree","level":3,"title":"1.   Perfect Binary Tree","text":"

As shown in Figure 7-4, a perfect binary tree has all levels completely filled with nodes. In a perfect binary tree, leaf nodes have a degree of \\(0\\), while all other nodes have a degree of \\(2\\). If the tree height is \\(h\\), the total number of nodes is \\(2^{h+1} - 1\\), exhibiting a standard exponential relationship that reflects the common phenomenon of cell division in nature.

Tip

Please note that in the Chinese community, a perfect binary tree is often referred to as a full binary tree.

Figure 7-4   Perfect binary tree

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#2-complete-binary-tree","level":3,"title":"2.   Complete Binary Tree","text":"

As shown in Figure 7-5, a complete binary tree only allows the bottom level to be incompletely filled, and the nodes at the bottom level must be filled continuously from left to right. Note that a perfect binary tree is also a complete binary tree.

Figure 7-5   Complete binary tree

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#3-full-binary-tree","level":3,"title":"3.   Full Binary Tree","text":"

As shown in Figure 7-6, in a full binary tree, all nodes except leaf nodes have two child nodes.

Figure 7-6   Full binary tree

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#4-balanced-binary-tree","level":3,"title":"4.   Balanced Binary Tree","text":"

As shown in Figure 7-7, in a balanced binary tree, the absolute difference between the height of the left and right subtrees of any node does not exceed 1.

Figure 7-7   Balanced binary tree

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#714-degeneration-of-binary-trees","level":2,"title":"7.1.4   Degeneration of Binary Trees","text":"

Figure 7-8 shows the ideal and degenerate structures of binary trees. When every level of a binary tree is filled, it reaches the \"perfect binary tree\" state; when all nodes are biased toward one side, the binary tree degenerates into a \"linked list\".

  • A perfect binary tree is the ideal case, fully leveraging the \"divide and conquer\" advantage of binary trees.
  • A linked list represents the other extreme, where all operations become linear operations with time complexity degrading to \\(O(n)\\).

Figure 7-8   The Best and Worst Structures of Binary Trees

As shown in Table 7-1, in the best and worst structures, the binary tree achieves either maximum or minimum values for leaf node count, total number of nodes, and height.

Table 7-1   The Best and Worst Structures of Binary Trees

Perfect binary tree Linked list Number of nodes at level \\(i\\) \\(2^{i-1}\\) \\(1\\) Number of leaf nodes in a tree with height \\(h\\) \\(2^h\\) \\(1\\) Total number of nodes in a tree with height \\(h\\) \\(2^{h+1} - 1\\) \\(h + 1\\) Height of a tree with \\(n\\) total nodes \\(\\log_2 (n+1) - 1\\) \\(n - 1\\)","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/","level":1,"title":"7.2   Binary Tree Traversal","text":"

From a physical structure perspective, a tree is a data structure based on linked lists. Hence, its traversal method involves accessing nodes one by one through pointers. However, a tree is a non-linear data structure, which makes traversing a tree more complex than traversing a linked list, requiring the assistance of search algorithms.

The common traversal methods for binary trees include level-order traversal, pre-order traversal, in-order traversal, and post-order traversal.

","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#721-level-order-traversal","level":2,"title":"7.2.1   Level-Order Traversal","text":"

As shown in Figure 7-9, level-order traversal traverses the binary tree from top to bottom, layer by layer. Within each level, it visits nodes from left to right.

Level-order traversal is essentially breadth-first traversal, also known as breadth-first search (BFS), which embodies a \"expanding outward circle by circle\" layer-by-layer traversal method.

Figure 7-9   Level-order traversal of a binary tree

","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1-code-implementation","level":3,"title":"1.   Code Implementation","text":"

Breadth-first traversal is typically implemented with the help of a \"queue\". The queue follows the \"first in, first out\" rule, while breadth-first traversal follows the \"layer-by-layer progression\" rule; the underlying ideas of the two are consistent. The implementation code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_bfs.py
def level_order(root: TreeNode | None) -> list[int]:\n    \"\"\"Level-order traversal\"\"\"\n    # Initialize queue, add root node\n    queue: deque[TreeNode] = deque()\n    queue.append(root)\n    # Initialize a list to save the traversal sequence\n    res = []\n    while queue:\n        node: TreeNode = queue.popleft()  # Dequeue\n        res.append(node.val)  # Save node value\n        if node.left is not None:\n            queue.append(node.left)  # Left child node enqueue\n        if node.right is not None:\n            queue.append(node.right)  # Right child node enqueue\n    return res\n
binary_tree_bfs.cpp
/* Level-order traversal */\nvector<int> levelOrder(TreeNode *root) {\n    // Initialize queue, add root node\n    queue<TreeNode *> queue;\n    queue.push(root);\n    // Initialize a list to save the traversal sequence\n    vector<int> vec;\n    while (!queue.empty()) {\n        TreeNode *node = queue.front();\n        queue.pop();              // Dequeue\n        vec.push_back(node->val); // Save node value\n        if (node->left != nullptr)\n            queue.push(node->left); // Left child node enqueue\n        if (node->right != nullptr)\n            queue.push(node->right); // Right child node enqueue\n    }\n    return vec;\n}\n
binary_tree_bfs.java
/* Level-order traversal */\nList<Integer> levelOrder(TreeNode root) {\n    // Initialize queue, add root node\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.add(root);\n    // Initialize a list to save the traversal sequence\n    List<Integer> list = new ArrayList<>();\n    while (!queue.isEmpty()) {\n        TreeNode node = queue.poll(); // Dequeue\n        list.add(node.val);           // Save node value\n        if (node.left != null)\n            queue.offer(node.left);   // Left child node enqueue\n        if (node.right != null)\n            queue.offer(node.right);  // Right child node enqueue\n    }\n    return list;\n}\n
binary_tree_bfs.cs
/* Level-order traversal */\nList<int> LevelOrder(TreeNode root) {\n    // Initialize queue, add root node\n    Queue<TreeNode> queue = new();\n    queue.Enqueue(root);\n    // Initialize a list to save the traversal sequence\n    List<int> list = [];\n    while (queue.Count != 0) {\n        TreeNode node = queue.Dequeue(); // Dequeue\n        list.Add(node.val!.Value);       // Save node value\n        if (node.left != null)\n            queue.Enqueue(node.left);    // Left child node enqueue\n        if (node.right != null)\n            queue.Enqueue(node.right);   // Right child node enqueue\n    }\n    return list;\n}\n
binary_tree_bfs.go
/* Level-order traversal */\nfunc levelOrder(root *TreeNode) []any {\n    // Initialize queue, add root node\n    queue := list.New()\n    queue.PushBack(root)\n    // Initialize a slice to save traversal sequence\n    nums := make([]any, 0)\n    for queue.Len() > 0 {\n        // Dequeue\n        node := queue.Remove(queue.Front()).(*TreeNode)\n        // Save node value\n        nums = append(nums, node.Val)\n        if node.Left != nil {\n            // Left child node enqueue\n            queue.PushBack(node.Left)\n        }\n        if node.Right != nil {\n            // Right child node enqueue\n            queue.PushBack(node.Right)\n        }\n    }\n    return nums\n}\n
binary_tree_bfs.swift
/* Level-order traversal */\nfunc levelOrder(root: TreeNode) -> [Int] {\n    // Initialize queue, add root node\n    var queue: [TreeNode] = [root]\n    // Initialize a list to save the traversal sequence\n    var list: [Int] = []\n    while !queue.isEmpty {\n        let node = queue.removeFirst() // Dequeue\n        list.append(node.val) // Save node value\n        if let left = node.left {\n            queue.append(left) // Left child node enqueue\n        }\n        if let right = node.right {\n            queue.append(right) // Right child node enqueue\n        }\n    }\n    return list\n}\n
binary_tree_bfs.js
/* Level-order traversal */\nfunction levelOrder(root) {\n    // Initialize queue, add root node\n    const queue = [root];\n    // Initialize a list to save the traversal sequence\n    const list = [];\n    while (queue.length) {\n        let node = queue.shift(); // Dequeue\n        list.push(node.val); // Save node value\n        if (node.left) queue.push(node.left); // Left child node enqueue\n        if (node.right) queue.push(node.right); // Right child node enqueue\n    }\n    return list;\n}\n
binary_tree_bfs.ts
/* Level-order traversal */\nfunction levelOrder(root: TreeNode | null): number[] {\n    // Initialize queue, add root node\n    const queue = [root];\n    // Initialize a list to save the traversal sequence\n    const list: number[] = [];\n    while (queue.length) {\n        let node = queue.shift() as TreeNode; // Dequeue\n        list.push(node.val); // Save node value\n        if (node.left) {\n            queue.push(node.left); // Left child node enqueue\n        }\n        if (node.right) {\n            queue.push(node.right); // Right child node enqueue\n        }\n    }\n    return list;\n}\n
binary_tree_bfs.dart
/* Level-order traversal */\nList<int> levelOrder(TreeNode? root) {\n  // Initialize queue, add root node\n  Queue<TreeNode?> queue = Queue();\n  queue.add(root);\n  // Initialize a list to save the traversal sequence\n  List<int> res = [];\n  while (queue.isNotEmpty) {\n    TreeNode? node = queue.removeFirst(); // Dequeue\n    res.add(node!.val); // Save node value\n    if (node.left != null) queue.add(node.left); // Left child node enqueue\n    if (node.right != null) queue.add(node.right); // Right child node enqueue\n  }\n  return res;\n}\n
binary_tree_bfs.rs
/* Level-order traversal */\nfn level_order(root: &Rc<RefCell<TreeNode>>) -> Vec<i32> {\n    // Initialize queue, add root node\n    let mut que = VecDeque::new();\n    que.push_back(root.clone());\n    // Initialize a list to save the traversal sequence\n    let mut vec = Vec::new();\n\n    while let Some(node) = que.pop_front() {\n        // Dequeue\n        vec.push(node.borrow().val); // Save node value\n        if let Some(left) = node.borrow().left.as_ref() {\n            que.push_back(left.clone()); // Left child node enqueue\n        }\n        if let Some(right) = node.borrow().right.as_ref() {\n            que.push_back(right.clone()); // Right child node enqueue\n        };\n    }\n    vec\n}\n
binary_tree_bfs.c
/* Level-order traversal */\nint *levelOrder(TreeNode *root, int *size) {\n    /* Auxiliary queue */\n    int front, rear;\n    int index, *arr;\n    TreeNode *node;\n    TreeNode **queue;\n\n    /* Auxiliary queue */\n    queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE);\n    // Queue pointer\n    front = 0, rear = 0;\n    // Add root node\n    queue[rear++] = root;\n    // Initialize a list to save the traversal sequence\n    /* Auxiliary array */\n    arr = (int *)malloc(sizeof(int) * MAX_SIZE);\n    // Array pointer\n    index = 0;\n    while (front < rear) {\n        // Dequeue\n        node = queue[front++];\n        // Save node value\n        arr[index++] = node->val;\n        if (node->left != NULL) {\n            // Left child node enqueue\n            queue[rear++] = node->left;\n        }\n        if (node->right != NULL) {\n            // Right child node enqueue\n            queue[rear++] = node->right;\n        }\n    }\n    // Update array length value\n    *size = index;\n    arr = realloc(arr, sizeof(int) * (*size));\n\n    // Free auxiliary array space\n    free(queue);\n    return arr;\n}\n
binary_tree_bfs.kt
/* Level-order traversal */\nfun levelOrder(root: TreeNode?): MutableList<Int> {\n    // Initialize queue, add root node\n    val queue = LinkedList<TreeNode?>()\n    queue.add(root)\n    // Initialize a list to save the traversal sequence\n    val list = mutableListOf<Int>()\n    while (queue.isNotEmpty()) {\n        val node = queue.poll()      // Dequeue\n        list.add(node?._val!!)       // Save node value\n        if (node.left != null)\n            queue.offer(node.left)   // Left child node enqueue\n        if (node.right != null)\n            queue.offer(node.right)  // Right child node enqueue\n    }\n    return list\n}\n
binary_tree_bfs.rb
### Level-order traversal ###\ndef level_order(root)\n  # Initialize queue, add root node\n  queue = [root]\n  # Initialize a list to save the traversal sequence\n  res = []\n  while !queue.empty?\n    node = queue.shift # Dequeue\n    res << node.val # Save node value\n    queue << node.left unless node.left.nil? # Left child node enqueue\n    queue << node.right unless node.right.nil? # Right child node enqueue\n  end\n  res\nend\n
","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2-complexity-analysis","level":3,"title":"2.   Complexity Analysis","text":"
  • Time complexity is \\(O(n)\\): All nodes are visited once, using \\(O(n)\\) time, where \\(n\\) is the number of nodes.
  • Space complexity is \\(O(n)\\): In the worst case, i.e., a full binary tree, before traversing to the bottom level, the queue contains at most \\((n + 1) / 2\\) nodes simultaneously, occupying \\(O(n)\\) space.
","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#722-preorder-inorder-and-postorder-traversal","level":2,"title":"7.2.2   Preorder, Inorder, and Postorder Traversal","text":"

Correspondingly, preorder, inorder, and postorder traversals all belong to depth-first traversal, also known as depth-first search (DFS), which embodies a \"first go to the end, then backtrack and continue\" traversal method.

Figure 7-10 shows how depth-first traversal works on a binary tree. Depth-first traversal is like \"walking\" around the perimeter of the entire binary tree, encountering three positions at each node, corresponding to preorder, inorder, and postorder traversal.

Figure 7-10   Preorder, inorder, and postorder traversal of a binary tree

","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1-code-implementation_1","level":3,"title":"1.   Code Implementation","text":"

Depth-first search is usually implemented based on recursion:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_dfs.py
def pre_order(root: TreeNode | None):\n    \"\"\"Preorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: root node -> left subtree -> right subtree\n    res.append(root.val)\n    pre_order(root=root.left)\n    pre_order(root=root.right)\n\ndef in_order(root: TreeNode | None):\n    \"\"\"Inorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: left subtree -> root node -> right subtree\n    in_order(root=root.left)\n    res.append(root.val)\n    in_order(root=root.right)\n\ndef post_order(root: TreeNode | None):\n    \"\"\"Postorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: left subtree -> right subtree -> root node\n    post_order(root=root.left)\n    post_order(root=root.right)\n    res.append(root.val)\n
binary_tree_dfs.cpp
/* Preorder traversal */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    vec.push_back(root->val);\n    preOrder(root->left);\n    preOrder(root->right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root->left);\n    vec.push_back(root->val);\n    inOrder(root->right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root->left);\n    postOrder(root->right);\n    vec.push_back(root->val);\n}\n
binary_tree_dfs.java
/* Preorder traversal */\nvoid preOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.add(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.add(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.add(root.val);\n}\n
binary_tree_dfs.cs
/* Preorder traversal */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.Add(root.val!.Value);\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n\n/* Inorder traversal */\nvoid InOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: left subtree -> root node -> right subtree\n    InOrder(root.left);\n    list.Add(root.val!.Value);\n    InOrder(root.right);\n}\n\n/* Postorder traversal */\nvoid PostOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: left subtree -> right subtree -> root node\n    PostOrder(root.left);\n    PostOrder(root.right);\n    list.Add(root.val!.Value);\n}\n
binary_tree_dfs.go
/* Preorder traversal */\nfunc preOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    nums = append(nums, node.Val)\n    preOrder(node.Left)\n    preOrder(node.Right)\n}\n\n/* Inorder traversal */\nfunc inOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(node.Left)\n    nums = append(nums, node.Val)\n    inOrder(node.Right)\n}\n\n/* Postorder traversal */\nfunc postOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(node.Left)\n    postOrder(node.Right)\n    nums = append(nums, node.Val)\n}\n
binary_tree_dfs.swift
/* Preorder traversal */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    list.append(root.val)\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n\n/* Inorder traversal */\nfunc inOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root: root.left)\n    list.append(root.val)\n    inOrder(root: root.right)\n}\n\n/* Postorder traversal */\nfunc postOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root: root.left)\n    postOrder(root: root.right)\n    list.append(root.val)\n}\n
binary_tree_dfs.js
/* Preorder traversal */\nfunction preOrder(root) {\n    if (root === null) return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nfunction inOrder(root) {\n    if (root === null) return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nfunction postOrder(root) {\n    if (root === null) return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
binary_tree_dfs.ts
/* Preorder traversal */\nfunction preOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nfunction inOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nfunction postOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
binary_tree_dfs.dart
/* Preorder traversal */\nvoid preOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: root node -> left subtree -> right subtree\n  list.add(node.val);\n  preOrder(node.left);\n  preOrder(node.right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: left subtree -> root node -> right subtree\n  inOrder(node.left);\n  list.add(node.val);\n  inOrder(node.right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: left subtree -> right subtree -> root node\n  postOrder(node.left);\n  postOrder(node.right);\n  list.add(node.val);\n}\n
binary_tree_dfs.rs
/* Preorder traversal */\nfn pre_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Visit priority: root node -> left subtree -> right subtree\n            let node = node.borrow();\n            res.push(node.val);\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* Inorder traversal */\nfn in_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Visit priority: left subtree -> root node -> right subtree\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            res.push(node.val);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* Postorder traversal */\nfn post_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Visit priority: left subtree -> right subtree -> root node\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n            res.push(node.val);\n        }\n    }\n\n    dfs(root, &mut result);\n\n    result\n}\n
binary_tree_dfs.c
/* Preorder traversal */\nvoid preOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    arr[(*size)++] = root->val;\n    preOrder(root->left, size);\n    preOrder(root->right, size);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root->left, size);\n    arr[(*size)++] = root->val;\n    inOrder(root->right, size);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root->left, size);\n    postOrder(root->right, size);\n    arr[(*size)++] = root->val;\n}\n
binary_tree_dfs.kt
/* Preorder traversal */\nfun preOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: root node -> left subtree -> right subtree\n    list.add(root._val)\n    preOrder(root.left)\n    preOrder(root.right)\n}\n\n/* Inorder traversal */\nfun inOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left)\n    list.add(root._val)\n    inOrder(root.right)\n}\n\n/* Postorder traversal */\nfun postOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left)\n    postOrder(root.right)\n    list.add(root._val)\n}\n
binary_tree_dfs.rb
### Pre-order traversal ###\ndef pre_order(root)\n  return if root.nil?\n\n  # Visit priority: root node -> left subtree -> right subtree\n  $res << root.val\n  pre_order(root.left)\n  pre_order(root.right)\nend\n\n### In-order traversal ###\ndef in_order(root)\n  return if root.nil?\n\n  # Visit priority: left subtree -> root node -> right subtree\n  in_order(root.left)\n  $res << root.val\n  in_order(root.right)\nend\n\n### Post-order traversal ###\ndef post_order(root)\n  return if root.nil?\n\n  # Visit priority: left subtree -> right subtree -> root node\n  post_order(root.left)\n  post_order(root.right)\n  $res << root.val\nend\n

Tip

Depth-first search can also be implemented based on iteration, interested readers can study this on their own.

Figure 7-11 shows the recursive process of preorder traversal of a binary tree, which can be divided into two opposite parts: \"recursion\" and \"return\".

  1. \"Recursion\" means opening a new method, where the program accesses the next node in this process.
  2. \"Return\" means the function returns, indicating that the current node has been fully visited.
<1><2><3><4><5><6><7><8><9><10><11>

Figure 7-11   The recursive process of preorder traversal

","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2-complexity-analysis_1","level":3,"title":"2.   Complexity Analysis","text":"
  • Time complexity is \\(O(n)\\): All nodes are visited once, using \\(O(n)\\) time.
  • Space complexity is \\(O(n)\\): In the worst case, i.e., the tree degenerates into a linked list, the recursion depth reaches \\(n\\), and the system occupies \\(O(n)\\) stack frame space.
","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/summary/","level":1,"title":"7.6   Summary","text":"","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]},{"location":"chapter_tree/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • A binary tree is a non-linear data structure that embodies the divide-and-conquer logic of \"one divides into two\". Each binary tree node contains a value and two pointers, which respectively point to its left and right child nodes.
  • For a certain node in a binary tree, the tree formed by its left (right) child node and all nodes below is called the left (right) subtree of that node.
  • Related terminology of binary trees includes root node, leaf node, level, degree, edge, height, and depth.
  • The initialization, node insertion, and node removal operations of binary trees are similar to those of linked lists.
  • Common types of binary trees include perfect binary trees, complete binary trees, full binary trees, and balanced binary trees. The perfect binary tree is the ideal state, while the linked list is the worst state after degradation.
  • A binary tree can be represented using an array by arranging node values and empty slots in level-order traversal sequence, and implementing pointers based on the index mapping relationship between parent and child nodes.
  • Level-order traversal of a binary tree is a breadth-first search method, embodying a layer-by-layer traversal approach of \"expanding outward circle by circle\", typically implemented using a queue.
  • Preorder, inorder, and postorder traversals all belong to depth-first search, embodying a traversal approach of \"first go to the end, then backtrack and continue\", typically implemented using recursion.
  • A binary search tree is an efficient data structure for element searching, with search, insertion, and removal operations all having time complexity of \\(O(\\log n)\\). When a binary search tree degenerates into a linked list, all time complexities degrade to \\(O(n)\\).
  • An AVL tree, also known as a balanced binary search tree, ensures the tree remains balanced after continuous node insertions and removals through rotation operations.
  • Rotation operations in AVL trees include right rotation, left rotation, left rotation then right rotation, and right rotation then left rotation. After inserting or removing nodes, AVL trees perform rotation operations from bottom to top to restore the tree to balance.
","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]},{"location":"chapter_tree/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: For a binary tree with only one node, are both the height of the tree and the depth of the root node \\(0\\)?

Yes, because height and depth are typically defined as \"the number of edges passed.\"

Q: The insertion and removal in a binary tree are generally accomplished by a set of operations. What does \"a set of operations\" refer to here? Does it imply releasing the resources of the child nodes?

Taking the binary search tree as an example, the operation of removing a node needs to be handled in three different scenarios, each requiring multiple steps of node operations.

Q: Why does DFS traversal of binary trees have three orders: preorder, inorder, and postorder, and what are their uses?

Similar to forward and reverse traversal of arrays, preorder, inorder, and postorder traversals are three methods of binary tree traversal that allow us to obtain a traversal result in a specific order. For example, in a binary search tree, since nodes satisfy the relationship left child node value < root node value < right child node value, we only need to traverse the tree with the priority of \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\" to obtain an ordered node sequence.

Q: In a right rotation operation handling the relationship between unbalanced nodes node, child, and grand_child, doesn't the connection between node and its parent node get lost after the right rotation?

We need to view this problem from a recursive perspective. The right rotation operation right_rotate(root) passes in the root node of the subtree and eventually returns the root node of the subtree after rotation with return child. The connection between the subtree's root node and its parent node is completed after the function returns, which is not within the maintenance scope of the right rotation operation.

Q: In C++, functions are divided into private and public sections. What considerations are there for this? Why are the height() function and the updateHeight() function placed in public and private, respectively?

It mainly depends on the method's usage scope. If a method is only used within the class, then it is designed as private. For example, calling updateHeight() alone by the user makes no sense, as it is only a step in insertion or removal operations. However, height() is used to access node height, similar to vector.size(), so it is set to public for ease of use.

Q: How do you build a binary search tree from a set of input data? Is the choice of root node very important?

Yes, the method for building a tree is provided in the build_tree() method in the binary search tree code. As for the choice of root node, we typically sort the input data, then select the middle element as the root node, and recursively build the left and right subtrees. This approach maximizes the tree's balance.

Q: In Java, do you always have to use the equals() method for string comparison?

In Java, for primitive data types, == is used to compare whether the values of two variables are equal. For reference types, the working principles of the two symbols are different.

  • ==: Used to compare whether two variables point to the same object, i.e., whether their positions in memory are the same.
  • equals(): Used to compare whether the values of two objects are equal.

Therefore, if we want to compare values, we should use equals(). However, strings initialized via String a = \"hi\"; String b = \"hi\"; are stored in the string constant pool and point to the same object, so a == b can also be used to compare the contents of the two strings.

Q: Before reaching the bottom level, is the number of nodes in the queue \\(2^h\\) in breadth-first traversal?

Yes, for example, a full binary tree with height \\(h = 2\\) has a total of \\(n = 7\\) nodes, then the bottom level has \\(4 = 2^h = (n + 1) / 2\\) nodes.

","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]}]} \ No newline at end of file +{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"Chapter 16.   Appendix","text":"","path":["Chapter 16. Appendix","Chapter 16.   Appendix"],"tags":[]},{"location":"chapter_appendix/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 16.1   Programming Environment Installation
  • 16.2   Contributing Together
  • 16.3   Terminology Table
","path":["Chapter 16. Appendix","Chapter 16.   Appendix"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   Contributing Together","text":"

Due to limited capacity, there may be inevitable omissions and errors in this book. We appreciate your understanding and are grateful for your help in correcting them. If you discover typos, broken links, missing content, ambiguous wording, unclear explanations, or structural issues, please help us make corrections to provide readers with higher-quality learning resources.

The GitHub IDs of all contributors will be displayed on the homepage of the book repository, the web version, and the PDF version to acknowledge their selfless contributions to the open source community.

The Charm of Open Source

The interval between two printings of a physical book is often quite long, making content updates very inconvenient.

In this open source book, the time for content updates has been shortened to just days or even hours.

","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#1-minor-content-adjustments","level":3,"title":"1.   Minor Content Adjustments","text":"

As shown in Figure 16-3, there is an \"edit icon\" in the top-right corner of each page. You can modify text or code by following these steps.

  1. Click the \"edit icon\". If you encounter a prompt asking you to \"Fork this repository\", please approve the operation.
  2. Modify the content of the Markdown source file, verify the correctness of the content, and maintain consistent formatting as much as possible.
  3. Fill in a description of your changes at the bottom of the page, then click the \"Propose file change\" button. After the page transitions, click the \"Create pull request\" button to submit your pull request.

Figure 16-3   Page edit button

Images cannot be directly modified. Please describe the issue by creating a new Issue or leaving a comment. We will promptly redraw and replace the images.

","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#2-content-creation","level":3,"title":"2.   Content Creation","text":"

If you are interested in contributing to this open source project, including translating code into other programming languages or expanding article content, you will need to follow the Pull Request workflow below.

  1. Log in to GitHub and Fork the book's code repository to your personal account.
  2. Enter your forked repository webpage and use the git clone command to clone the repository to your local machine.
  3. Create content locally and conduct comprehensive tests to verify code correctness.
  4. Commit your local changes and push them to the remote repository.
  5. Refresh the repository webpage and click the \"Create pull request\" button to submit your pull request.
","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/contribution/#3-docker-deployment","level":3,"title":"3.   Docker Deployment","text":"

From the root directory of hello-algo, run the following Docker script to access the project at http://localhost:8000:

docker-compose up -d\n

Use the following command to remove the deployment:

docker-compose down\n
","path":["Chapter 16. Appendix","16.2   Contributing Together"],"tags":[]},{"location":"chapter_appendix/installation/","level":1,"title":"16.1   Programming Environment Installation","text":"","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1611-installing-ide","level":2,"title":"16.1.1   Installing Ide","text":"

We recommend using the open-source and lightweight VS Code as the local integrated development environment (IDE). Visit the VS Code official website, and download and install the appropriate version of VS Code according to your operating system.

Figure 16-1   Download VS Code from the Official Website

VS Code has a powerful ecosystem of extensions that supports running and debugging most programming languages. For example, after installing the \"Python Extension Pack\" extension, you can debug Python code. The installation steps are shown in the following figure.

Figure 16-2   Install VS Code Extensions

","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1612-installing-language-environments","level":2,"title":"16.1.2   Installing Language Environments","text":"","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#1-python-environment","level":3,"title":"1.   Python Environment","text":"
  1. Download and install Miniconda3, which requires Python 3.10 or newer.
  2. Search for python in the VS Code extension marketplace and install the Python Extension Pack.
  3. (Optional) Enter pip install black on the command line to install the code formatter.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#2-cc-environment","level":3,"title":"2.   C/c++ Environment","text":"
  1. Windows systems need to install MinGW (configuration tutorial); macOS comes with Clang built-in and does not require installation.
  2. Search for c++ in the VS Code extension marketplace and install the C/C++ Extension Pack.
  3. (Optional) Open the Settings page, search for the Clang_format_fallback Style code formatting option, and set it to { BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#3-java-environment","level":3,"title":"3.   Java Environment","text":"
  1. Download and install OpenJDK (version must be > JDK 9).
  2. Search for java in the VS Code extension marketplace and install the Extension Pack for Java.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#4-c-environment","level":3,"title":"4.   C# Environment","text":"
  1. Download and install .Net 8.0.
  2. Search for C# Dev Kit in the VS Code extension marketplace and install C# Dev Kit (configuration tutorial).
  3. You can also use Visual Studio (installation tutorial).
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#5-go-environment","level":3,"title":"5.   Go Environment","text":"
  1. Download and install Go.
  2. Search for go in the VS Code extension marketplace and install Go.
  3. Press Ctrl + Shift + P to open the command palette, type go, select Go: Install/Update Tools, check all options and install.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#6-swift-environment","level":3,"title":"6.   Swift Environment","text":"
  1. Download and install Swift.
  2. Search for swift in the VS Code extension marketplace and install Swift for Visual Studio Code.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#7-javascript-environment","level":3,"title":"7.   Javascript Environment","text":"
  1. Download and install Node.js.
  2. (Optional) Search for Prettier in the VS Code extension marketplace and install the code formatter.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#8-typescript-environment","level":3,"title":"8.   Typescript Environment","text":"
  1. Follow the same installation steps as the JavaScript environment.
  2. Install TypeScript Execute (tsx).
  3. Search for typescript in the VS Code extension marketplace and install Pretty TypeScript Errors.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#9-dart-environment","level":3,"title":"9.   Dart Environment","text":"
  1. Download and install Dart.
  2. Search for dart in the VS Code extension marketplace and install Dart.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/installation/#10-rust-environment","level":3,"title":"10.   Rust Environment","text":"
  1. Download and install Rust.
  2. Search for rust in the VS Code extension marketplace and install rust-analyzer.
","path":["Chapter 16. Appendix","16.1   Programming Environment Installation"],"tags":[]},{"location":"chapter_appendix/terminology/","level":1,"title":"16.3   Terminology Table","text":"

The following table lists important terms that appear in this book. It is worth noting the following points:

  • We recommend remembering the English names of terms to help with reading English literature.
  • Some terms have different names in Simplified Chinese and Traditional Chinese.

Table 16-1   Important Terms in Data Structures and Algorithms

English Simplified Chinese Traditional Chinese algorithm 算法 演算法 data structure 数据结构 資料結構 code 代码 程式碼 file 文件 檔案 function 函数 函式 method 方法 方法 variable 变量 變數 asymptotic complexity analysis 渐近复杂度分析 漸近複雜度分析 time complexity 时间复杂度 時間複雜度 space complexity 空间复杂度 空間複雜度 loop 循环 迴圈 iteration 迭代 迭代 recursion 递归 遞迴 tail recursion 尾递归 尾遞迴 recursion tree 递归树 遞迴樹 big-\\(O\\) notation 大 \\(O\\) 记号 大 \\(O\\) 記號 asymptotic upper bound 渐近上界 漸近上界 sign-magnitude 原码 原碼 1’s complement 反码 一補數 2’s complement 补码 二補數 array 数组 陣列 index 索引 索引 linked list 链表 鏈結串列 linked list node, list node 链表节点 鏈結串列節點 head node 头节点 頭節點 tail node 尾节点 尾節點 list 列表 串列 dynamic array 动态数组 動態陣列 hard disk 硬盘 硬碟 random-access memory (RAM) 内存 記憶體 cache memory 缓存 快取 cache miss 缓存未命中 快取未命中 cache hit rate 缓存命中率 快取命中率 stack 栈 堆疊 top of the stack 栈顶 堆疊頂 bottom of the stack 栈底 堆疊底 queue 队列 佇列 double-ended queue 双向队列 雙向佇列 front of the queue 队首 佇列首 rear of the queue 队尾 佇列尾 hash table 哈希表 雜湊表 hash set 哈希集合 雜湊集合 bucket 桶 桶 hash function 哈希函数 雜湊函式 hash collision 哈希冲突 雜湊衝突 load factor 负载因子 負載因子 separate chaining 链式地址 鏈結位址 open addressing 开放寻址 開放定址 linear probing 线性探测 線性探查 lazy deletion 懒删除 懶刪除 binary tree 二叉树 二元樹 tree node 树节点 樹節點 left-child node 左子节点 左子節點 right-child node 右子节点 右子節點 parent node 父节点 父節點 left subtree 左子树 左子樹 right subtree 右子树 右子樹 root node 根节点 根節點 leaf node 叶节点 葉節點 edge 边 邊 level 层 層 degree 度 度 height 高度 高度 depth 深度 深度 perfect binary tree 完美二叉树 完美二元樹 complete binary tree 完全二叉树 完全二元樹 full binary tree 完满二叉树 完滿二元樹 balanced binary tree 平衡二叉树 平衡二元樹 binary search tree 二叉搜索树 二元搜尋樹 AVL tree AVL 树 AVL 樹 red-black tree 红黑树 紅黑樹 level-order traversal 层序遍历 層序走訪 breadth-first traversal 广度优先遍历 廣度優先走訪 depth-first traversal 深度优先遍历 深度優先走訪 binary search tree 二叉搜索树 二元搜尋樹 balanced binary search tree 平衡二叉搜索树 平衡二元搜尋樹 balance factor 平衡因子 平衡因子 heap 堆 堆積 max heap 大顶堆 大頂堆積 min heap 小顶堆 小頂堆積 priority queue 优先队列 優先佇列 heapify 堆化 堆積化 top-\\(k\\) problem Top-\\(k\\) 问题 Top-\\(k\\) 問題 graph 图 圖 vertex 顶点 頂點 undirected graph 无向图 無向圖 directed graph 有向图 有向圖 connected graph 连通图 連通圖 disconnected graph 非连通图 非連通圖 weighted graph 有权图 有權圖 adjacency 邻接 鄰接 path 路径 路徑 in-degree 入度 入度 out-degree 出度 出度 adjacency matrix 邻接矩阵 鄰接矩陣 adjacency list 邻接表 鄰接表 breadth-first search 广度优先搜索 廣度優先搜尋 depth-first search 深度优先搜索 深度優先搜尋 binary search 二分查找 二分搜尋 searching algorithm 搜索算法 搜尋演算法 sorting algorithm 排序算法 排序演算法 selection sort 选择排序 選擇排序 bubble sort 冒泡排序 泡沫排序 insertion sort 插入排序 插入排序 quick sort 快速排序 快速排序 merge sort 归并排序 合併排序 heap sort 堆排序 堆積排序 bucket sort 桶排序 桶排序 counting sort 计数排序 計數排序 radix sort 基数排序 基數排序 divide and conquer 分治 分治 hanota problem 汉诺塔问题 河內塔問題 backtracking algorithm 回溯算法 回溯演算法 constraint 约束 約束 solution 解 解 state 状态 狀態 pruning 剪枝 剪枝 permutations problem 全排列问题 全排列問題 subset-sum problem 子集和问题 子集合問題 \\(n\\)-queens problem \\(n\\) 皇后问题 \\(n\\) 皇后問題 dynamic programming 动态规划 動態規劃 initial state 初始状态 初始狀態 state-transition equation 状态转移方程 狀態轉移方程 knapsack problem 背包问题 背包問題 edit distance problem 编辑距离问题 編輯距離問題 greedy algorithm 贪心算法 貪婪演算法","path":["Chapter 16. Appendix","16.3   Terminology Table"],"tags":[]},{"location":"chapter_array_and_linkedlist/","level":1,"title":"Chapter 4.   Array and Linked List","text":"

Abstract

The world of data structures is like a solid brick wall.

Array bricks are neatly arranged, tightly packed one by one. Linked list bricks are scattered everywhere, with connecting vines freely weaving through the gaps between bricks.

","path":["Chapter 4. Array and Linked List","Chapter 4.   Array and Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 4.1   Array
  • 4.2   Linked List
  • 4.3   List
  • 4.4   Memory and Cache *
  • 4.5   Summary
","path":["Chapter 4. Array and Linked List","Chapter 4.   Array and Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   Array","text":"

An array is a linear data structure that stores elements of the same type in contiguous memory space. The position of an element in the array is called the element's index. Figure 4-1 illustrates the main concepts and storage method of arrays.

Figure 4-1   Array definition and storage method

","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#411-common-array-operations","level":2,"title":"4.1.1   Common Array Operations","text":"","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#1-initializing-arrays","level":3,"title":"1.   Initializing Arrays","text":"

We can choose between two array initialization methods based on our needs: without initial values or with given initial values. When no initial values are specified, most programming languages will initialize array elements to \\(0\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
# Initialize array\narr: list[int] = [0] * 5  # [ 0, 0, 0, 0, 0 ]\nnums: list[int] = [1, 3, 2, 5, 4]\n
array.cpp
/* Initialize array */\n// Stored on stack\nint arr[5];\nint nums[5] = { 1, 3, 2, 5, 4 };\n// Stored on heap (requires manual memory release)\nint* arr1 = new int[5];\nint* nums1 = new int[5] { 1, 3, 2, 5, 4 };\n
array.java
/* Initialize array */\nint[] arr = new int[5]; // { 0, 0, 0, 0, 0 }\nint[] nums = { 1, 3, 2, 5, 4 };\n
array.cs
/* Initialize array */\nint[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]\nint[] nums = [1, 3, 2, 5, 4];\n
array.go
/* Initialize array */\nvar arr [5]int\n// In Go, specifying length ([5]int) creates an array; not specifying length ([]int) creates a slice\n// Since Go's arrays are designed to have their length determined at compile time, only constants can be used to specify the length\n// For convenience in implementing the extend() method, slices are treated as arrays below\nnums := []int{1, 3, 2, 5, 4}\n
array.swift
/* Initialize array */\nlet arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]\nlet nums = [1, 3, 2, 5, 4]\n
array.js
/* Initialize array */\nvar arr = new Array(5).fill(0);\nvar nums = [1, 3, 2, 5, 4];\n
array.ts
/* Initialize array */\nlet arr: number[] = new Array(5).fill(0);\nlet nums: number[] = [1, 3, 2, 5, 4];\n
array.dart
/* Initialize array */\nList<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]\nList<int> nums = [1, 3, 2, 5, 4];\n
array.rs
/* Initialize array */\nlet arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]\nlet slice: &[i32] = &[0; 5];\n// In Rust, specifying length ([i32; 5]) creates an array; not specifying length (&[i32]) creates a slice\n// Since Rust's arrays are designed to have their length determined at compile time, only constants can be used to specify the length\n// Vector is the type generally used as a dynamic array in Rust\n// For convenience in implementing the extend() method, vectors are treated as arrays below\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
array.c
/* Initialize array */\nint arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }\nint nums[5] = { 1, 3, 2, 5, 4 };\n
array.kt
/* Initialize array */\nvar arr = IntArray(5) // { 0, 0, 0, 0, 0 }\nvar nums = intArrayOf(1, 3, 2, 5, 4)\n
array.rb
# Initialize array\narr = Array.new(5, 0)\nnums = [1, 3, 2, 5, 4]\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#2-accessing-elements","level":3,"title":"2.   Accessing Elements","text":"

Array elements are stored in contiguous memory space, which means calculating the memory address of array elements is very easy. Given the array's memory address (the memory address of the first element) and an element's index, we can use the formula shown in Figure 4-2 to calculate the element's memory address and directly access that element.

Figure 4-2   Memory address calculation for array elements

Observing Figure 4-2, we find that the first element of an array has an index of \\(0\\), which may seem counterintuitive since counting from \\(1\\) would be more natural. However, from the perspective of the address calculation formula, an index is essentially an offset from the memory address. The address offset of the first element is \\(0\\), so it is reasonable for its index to be \\(0\\).

Accessing elements in an array is highly efficient; we can randomly access any element in the array in \\(O(1)\\) time.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def random_access(nums: list[int]) -> int:\n    \"\"\"Random access to element\"\"\"\n    # Randomly select a number from the interval [0, len(nums)-1]\n    random_index = random.randint(0, len(nums) - 1)\n    # Retrieve and return the random element\n    random_num = nums[random_index]\n    return random_num\n
array.cpp
/* Random access to element */\nint randomAccess(int *nums, int size) {\n    // Randomly select a number from interval [0, size)\n    int randomIndex = rand() % size;\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.java
/* Random access to element */\nint randomAccess(int[] nums) {\n    // Randomly select a number in the interval [0, nums.length)\n    int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.cs
/* Random access to element */\nint RandomAccess(int[] nums) {\n    Random random = new();\n    // Randomly select a number in interval [0, nums.Length)\n    int randomIndex = random.Next(nums.Length);\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.go
/* Random access to element */\nfunc randomAccess(nums []int) (randomNum int) {\n    // Randomly select a number in the interval [0, nums.length)\n    randomIndex := rand.Intn(len(nums))\n    // Retrieve and return the random element\n    randomNum = nums[randomIndex]\n    return\n}\n
array.swift
/* Random access to element */\nfunc randomAccess(nums: [Int]) -> Int {\n    // Randomly select a number in interval [0, nums.count)\n    let randomIndex = nums.indices.randomElement()!\n    // Retrieve and return the random element\n    let randomNum = nums[randomIndex]\n    return randomNum\n}\n
array.js
/* Random access to element */\nfunction randomAccess(nums) {\n    // Randomly select a number in the interval [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Retrieve and return the random element\n    const random_num = nums[random_index];\n    return random_num;\n}\n
array.ts
/* Random access to element */\nfunction randomAccess(nums: number[]): number {\n    // Randomly select a number in the interval [0, nums.length)\n    const random_index = Math.floor(Math.random() * nums.length);\n    // Retrieve and return the random element\n    const random_num = nums[random_index];\n    return random_num;\n}\n
array.dart
/* Random access to element */\nint randomAccess(List<int> nums) {\n  // Randomly select a number in the interval [0, nums.length)\n  int randomIndex = Random().nextInt(nums.length);\n  // Retrieve and return the random element\n  int randomNum = nums[randomIndex];\n  return randomNum;\n}\n
array.rs
/* Random access to element */\nfn random_access(nums: &[i32]) -> i32 {\n    // Randomly select a number in interval [0, nums.len())\n    let random_index = rand::thread_rng().gen_range(0..nums.len());\n    // Retrieve and return the random element\n    let random_num = nums[random_index];\n    random_num\n}\n
array.c
/* Random access to element */\nint randomAccess(int *nums, int size) {\n    // Randomly select a number from interval [0, size)\n    int randomIndex = rand() % size;\n    // Retrieve and return the random element\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.kt
/* Random access to element */\nfun randomAccess(nums: IntArray): Int {\n    // Randomly select a number in interval [0, nums.size)\n    val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size)\n    // Retrieve and return the random element\n    val randomNum = nums[randomIndex]\n    return randomNum\n}\n
array.rb
### Random access element ###\ndef random_access(nums)\n  # Randomly select a number in the interval [0, nums.length)\n  random_index = Random.rand(0...nums.length)\n\n  # Retrieve and return the random element\n  nums[random_index]\nend\n
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#3-inserting-elements","level":3,"title":"3.   Inserting Elements","text":"

Array elements are stored \"tightly adjacent\" in memory, with no space between them to store any additional data. As shown in Figure 4-3, if we want to insert an element in the middle of an array, we need to shift all elements after that position backward by one position, and then assign the value to that index.

Figure 4-3   Example of inserting an element into an array

It is worth noting that since the length of an array is fixed, inserting an element will inevitably cause the element at the end of the array to be \"lost\". We will leave the solution to this problem for discussion in the \"List\" chapter.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def insert(nums: list[int], num: int, index: int):\n    \"\"\"Insert element num at index index in the array\"\"\"\n    # Move all elements at and after index index backward by one position\n    for i in range(len(nums) - 1, index, -1):\n        nums[i] = nums[i - 1]\n    # Assign num to the element at index index\n    nums[index] = num\n
array.cpp
/* Insert element num at index index in the array */\nvoid insert(int *nums, int size, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.java
/* Insert element num at index index in the array */\nvoid insert(int[] nums, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.cs
/* Insert element num at index index in the array */\nvoid Insert(int[] nums, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = nums.Length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.go
/* Insert element num at index index in the array */\nfunc insert(nums []int, num int, index int) {\n    // Move all elements at and after index index backward by one position\n    for i := len(nums) - 1; i > index; i-- {\n        nums[i] = nums[i-1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
array.swift
/* Insert element num at index index in the array */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n    // Move all elements at and after index index backward by one position\n    for i in nums.indices.dropFirst(index).reversed() {\n        nums[i] = nums[i - 1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
array.js
/* Insert element num at index index in the array */\nfunction insert(nums, num, index) {\n    // Move all elements at and after index index backward by one position\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.ts
/* Insert element num at index index in the array */\nfunction insert(nums: number[], num: number, index: number): void {\n    // Move all elements at and after index index backward by one position\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.dart
/* Insert element _num at array index index */\nvoid insert(List<int> nums, int _num, int index) {\n  // Move all elements at and after index index backward by one position\n  for (var i = nums.length - 1; i > index; i--) {\n    nums[i] = nums[i - 1];\n  }\n  // Assign _num to element at index\n  nums[index] = _num;\n}\n
array.rs
/* Insert element num at index index in the array */\nfn insert(nums: &mut [i32], num: i32, index: usize) {\n    // Move all elements at and after index index backward by one position\n    for i in (index + 1..nums.len()).rev() {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.c
/* Insert element num at index index in the array */\nvoid insert(int *nums, int size, int num, int index) {\n    // Move all elements at and after index index backward by one position\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // Assign num to the element at index index\n    nums[index] = num;\n}\n
array.kt
/* Insert element num at index index in the array */\nfun insert(nums: IntArray, num: Int, index: Int) {\n    // Move all elements at and after index index backward by one position\n    for (i in nums.size - 1 downTo index + 1) {\n        nums[i] = nums[i - 1]\n    }\n    // Assign num to the element at index index\n    nums[index] = num\n}\n
array.rb
### Insert element num at index in array ###\ndef insert(nums, num, index)\n  # Move all elements at and after index index backward by one position\n  for i in (nums.length - 1).downto(index + 1)\n    nums[i] = nums[i - 1]\n  end\n\n  # Assign num to the element at index index\n  nums[index] = num\nend\n
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4-removing-elements","level":3,"title":"4.   Removing Elements","text":"

Similarly, as shown in Figure 4-4, to delete the element at index \\(i\\), we need to shift all elements after index \\(i\\) forward by one position.

Figure 4-4   Example of removing an element from an array

Note that after the deletion is complete, the original last element becomes \"meaningless\", so we do not need to specifically modify it.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def remove(nums: list[int], index: int):\n    \"\"\"Remove the element at index index\"\"\"\n    # Move all elements after index index forward by one position\n    for i in range(index, len(nums) - 1):\n        nums[i] = nums[i + 1]\n
array.cpp
/* Remove the element at index index */\nvoid remove(int *nums, int size, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.java
/* Remove the element at index index */\nvoid remove(int[] nums, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.cs
/* Remove the element at index index */\nvoid Remove(int[] nums, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < nums.Length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.go
/* Remove the element at index index */\nfunc remove(nums []int, index int) {\n    // Move all elements after index index forward by one position\n    for i := index; i < len(nums)-1; i++ {\n        nums[i] = nums[i+1]\n    }\n}\n
array.swift
/* Remove the element at index index */\nfunc remove(nums: inout [Int], index: Int) {\n    // Move all elements after index index forward by one position\n    for i in nums.indices.dropFirst(index).dropLast() {\n        nums[i] = nums[i + 1]\n    }\n}\n
array.js
/* Remove the element at index index */\nfunction remove(nums, index) {\n    // Move all elements after index index forward by one position\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.ts
/* Remove the element at index index */\nfunction remove(nums: number[], index: number): void {\n    // Move all elements after index index forward by one position\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.dart
/* Remove the element at index index */\nvoid remove(List<int> nums, int index) {\n  // Move all elements after index index forward by one position\n  for (var i = index; i < nums.length - 1; i++) {\n    nums[i] = nums[i + 1];\n  }\n}\n
array.rs
/* Remove the element at index index */\nfn remove(nums: &mut [i32], index: usize) {\n    // Move all elements after index index forward by one position\n    for i in index..nums.len() - 1 {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.c
/* Remove the element at index index */\n// Note: stdio.h occupies the remove keyword\nvoid removeItem(int *nums, int size, int index) {\n    // Move all elements after index index forward by one position\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.kt
/* Remove the element at index index */\nfun remove(nums: IntArray, index: Int) {\n    // Move all elements after index index forward by one position\n    for (i in index..<nums.size - 1) {\n        nums[i] = nums[i + 1]\n    }\n}\n
array.rb
### Delete element at index ###\ndef remove(nums, index)\n  # Move all elements after index index forward by one position\n  for i in index...(nums.length - 1)\n    nums[i] = nums[i + 1]\n  end\nend\n

Overall, array insertion and deletion operations have the following drawbacks:

  • High time complexity: The average time complexity for both insertion and deletion in arrays is \\(O(n)\\), where \\(n\\) is the length of the array.
  • Loss of elements: Since the length of an array is immutable, after inserting an element, elements that exceed the array's length will be lost.
  • Memory waste: We can initialize a relatively long array and only use the front portion, so that when inserting data, the lost elements at the end are \"meaningless\", but this causes some memory space to be wasted.
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#5-traversing-arrays","level":3,"title":"5.   Traversing Arrays","text":"

In most programming languages, we can traverse an array either by index or by directly iterating through each element in the array:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def traverse(nums: list[int]):\n    \"\"\"Traverse array\"\"\"\n    count = 0\n    # Traverse array by index\n    for i in range(len(nums)):\n        count += nums[i]\n    # Direct traversal of array elements\n    for num in nums:\n        count += num\n    # Traverse simultaneously data index and elements\n    for i, num in enumerate(nums):\n        count += nums[i]\n        count += num\n
array.cpp
/* Traverse array */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
array.java
/* Traverse array */\nvoid traverse(int[] nums) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (int num : nums) {\n        count += num;\n    }\n}\n
array.cs
/* Traverse array */\nvoid Traverse(int[] nums) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < nums.Length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    foreach (int num in nums) {\n        count += num;\n    }\n}\n
array.go
/* Traverse array */\nfunc traverse(nums []int) {\n    count := 0\n    // Traverse array by index\n    for i := 0; i < len(nums); i++ {\n        count += nums[i]\n    }\n    count = 0\n    // Direct traversal of array elements\n    for _, num := range nums {\n        count += num\n    }\n    // Traverse simultaneously data index and elements\n    for i, num := range nums {\n        count += nums[i]\n        count += num\n    }\n}\n
array.swift
/* Traverse array */\nfunc traverse(nums: [Int]) {\n    var count = 0\n    // Traverse array by index\n    for i in nums.indices {\n        count += nums[i]\n    }\n    // Direct traversal of array elements\n    for num in nums {\n        count += num\n    }\n    // Traverse simultaneously data index and elements\n    for (i, num) in nums.enumerated() {\n        count += nums[i]\n        count += num\n    }\n}\n
array.js
/* Traverse array */\nfunction traverse(nums) {\n    let count = 0;\n    // Traverse array by index\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (const num of nums) {\n        count += num;\n    }\n}\n
array.ts
/* Traverse array */\nfunction traverse(nums: number[]): void {\n    let count = 0;\n    // Traverse array by index\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // Direct traversal of array elements\n    for (const num of nums) {\n        count += num;\n    }\n}\n
array.dart
/* Traverse array elements */\nvoid traverse(List<int> nums) {\n  int count = 0;\n  // Traverse array by index\n  for (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n  }\n  // Direct traversal of array elements\n  for (int _num in nums) {\n    count += _num;\n  }\n  // Traverse array using forEach method\n  nums.forEach((_num) {\n    count += _num;\n  });\n}\n
array.rs
/* Traverse array */\nfn traverse(nums: &[i32]) {\n    let mut _count = 0;\n    // Traverse array by index\n    for i in 0..nums.len() {\n        _count += nums[i];\n    }\n    // Direct traversal of array elements\n    _count = 0;\n    for &num in nums {\n        _count += num;\n    }\n}\n
array.c
/* Traverse array */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // Traverse array by index\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
array.kt
/* Traverse array */\nfun traverse(nums: IntArray) {\n    var count = 0\n    // Traverse array by index\n    for (i in nums.indices) {\n        count += nums[i]\n    }\n    // Direct traversal of array elements\n    for (j in nums) {\n        count += j\n    }\n}\n
array.rb
### Traverse array ###\ndef traverse(nums)\n  count = 0\n\n  # Traverse array by index\n  for i in 0...nums.length\n    count += nums[i]\n  end\n\n  # Direct traversal of array elements\n  for num in nums\n    count += num\n  end\nend\n
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#6-finding-elements","level":3,"title":"6.   Finding Elements","text":"

Finding a specified element in an array requires traversing the array and checking whether the element value matches in each iteration; if it matches, output the corresponding index.

Since an array is a linear data structure, the above search operation is called a \"linear search\".

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def find(nums: list[int], target: int) -> int:\n    \"\"\"Find the specified element in the array\"\"\"\n    for i in range(len(nums)):\n        if nums[i] == target:\n            return i\n    return -1\n
array.cpp
/* Find the specified element in the array */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.java
/* Find the specified element in the array */\nint find(int[] nums, int target) {\n    for (int i = 0; i < nums.length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.cs
/* Find the specified element in the array */\nint Find(int[] nums, int target) {\n    for (int i = 0; i < nums.Length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.go
/* Find the specified element in the array */\nfunc find(nums []int, target int) (index int) {\n    index = -1\n    for i := 0; i < len(nums); i++ {\n        if nums[i] == target {\n            index = i\n            break\n        }\n    }\n    return\n}\n
array.swift
/* Find the specified element in the array */\nfunc find(nums: [Int], target: Int) -> Int {\n    for i in nums.indices {\n        if nums[i] == target {\n            return i\n        }\n    }\n    return -1\n}\n
array.js
/* Find the specified element in the array */\nfunction find(nums, target) {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) return i;\n    }\n    return -1;\n}\n
array.ts
/* Find the specified element in the array */\nfunction find(nums: number[], target: number): number {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) {\n            return i;\n        }\n    }\n    return -1;\n}\n
array.dart
/* Find the specified element in the array */\nint find(List<int> nums, int target) {\n  for (var i = 0; i < nums.length; i++) {\n    if (nums[i] == target) return i;\n  }\n  return -1;\n}\n
array.rs
/* Find the specified element in the array */\nfn find(nums: &[i32], target: i32) -> Option<usize> {\n    for i in 0..nums.len() {\n        if nums[i] == target {\n            return Some(i);\n        }\n    }\n    None\n}\n
array.c
/* Find the specified element in the array */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.kt
/* Find the specified element in the array */\nfun find(nums: IntArray, target: Int): Int {\n    for (i in nums.indices) {\n        if (nums[i] == target)\n            return i\n    }\n    return -1\n}\n
array.rb
### Find specified element in array ###\ndef find(nums, target)\n  for i in 0...nums.length\n    return i if nums[i] == target\n  end\n\n  -1\nend\n
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#7-expanding-arrays","level":3,"title":"7.   Expanding Arrays","text":"

In complex system environments, programs cannot guarantee that the memory space after an array is available, making it unsafe to expand the array's capacity. Therefore, in most programming languages, the length of an array is immutable.

If we want to expand an array, we need to create a new, larger array and then copy the original array elements to the new array one by one. This is an \\(O(n)\\) operation, which is very time-consuming when the array is large. The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def extend(nums: list[int], enlarge: int) -> list[int]:\n    \"\"\"Extend array length\"\"\"\n    # Initialize an array with extended length\n    res = [0] * (len(nums) + enlarge)\n    # Copy all elements from the original array to the new array\n    for i in range(len(nums)):\n        res[i] = nums[i]\n    # Return the extended new array\n    return res\n
array.cpp
/* Extend array length */\nint *extend(int *nums, int size, int enlarge) {\n    // Initialize an array with extended length\n    int *res = new int[size + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Free memory\n    delete[] nums;\n    // Return the extended new array\n    return res;\n}\n
array.java
/* Extend array length */\nint[] extend(int[] nums, int enlarge) {\n    // Initialize an array with extended length\n    int[] res = new int[nums.length + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
array.cs
/* Extend array length */\nint[] Extend(int[] nums, int enlarge) {\n    // Initialize an array with extended length\n    int[] res = new int[nums.Length + enlarge];\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < nums.Length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
array.go
/* Extend array length */\nfunc extend(nums []int, enlarge int) []int {\n    // Initialize an array with extended length\n    res := make([]int, len(nums)+enlarge)\n    // Copy all elements from the original array to the new array\n    for i, num := range nums {\n        res[i] = num\n    }\n    // Return the extended new array\n    return res\n}\n
array.swift
/* Extend array length */\nfunc extend(nums: [Int], enlarge: Int) -> [Int] {\n    // Initialize an array with extended length\n    var res = Array(repeating: 0, count: nums.count + enlarge)\n    // Copy all elements from the original array to the new array\n    for i in nums.indices {\n        res[i] = nums[i]\n    }\n    // Return the extended new array\n    return res\n}\n
array.js
/* Extend array length */\n// Note: JavaScript's Array is dynamic array, can be directly expanded\n// For learning purposes, this function treats Array as fixed-length array\nfunction extend(nums, enlarge) {\n    // Initialize an array with extended length\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Copy all elements from the original array to the new array\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
array.ts
/* Extend array length */\n// Note: TypeScript's Array is dynamic array, can be directly expanded\n// For learning purposes, this function treats Array as fixed-length array\nfunction extend(nums: number[], enlarge: number): number[] {\n    // Initialize an array with extended length\n    const res = new Array(nums.length + enlarge).fill(0);\n    // Copy all elements from the original array to the new array\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // Return the extended new array\n    return res;\n}\n
array.dart
/* Extend array length */\nList<int> extend(List<int> nums, int enlarge) {\n  // Initialize an array with extended length\n  List<int> res = List.filled(nums.length + enlarge, 0);\n  // Copy all elements from the original array to the new array\n  for (var i = 0; i < nums.length; i++) {\n    res[i] = nums[i];\n  }\n  // Return the extended new array\n  return res;\n}\n
array.rs
/* Extend array length */\nfn extend(nums: &[i32], enlarge: usize) -> Vec<i32> {\n    // Initialize an array with extended length\n    let mut res: Vec<i32> = vec![0; nums.len() + enlarge];\n    // Copy all elements from original array to new\n    res[0..nums.len()].copy_from_slice(nums);\n\n    // Return the extended new array\n    res\n}\n
array.c
/* Extend array length */\nint *extend(int *nums, int size, int enlarge) {\n    // Initialize an array with extended length\n    int *res = (int *)malloc(sizeof(int) * (size + enlarge));\n    // Copy all elements from the original array to the new array\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // Initialize expanded space\n    for (int i = size; i < size + enlarge; i++) {\n        res[i] = 0;\n    }\n    // Return the extended new array\n    return res;\n}\n
array.kt
/* Extend array length */\nfun extend(nums: IntArray, enlarge: Int): IntArray {\n    // Initialize an array with extended length\n    val res = IntArray(nums.size + enlarge)\n    // Copy all elements from the original array to the new array\n    for (i in nums.indices) {\n        res[i] = nums[i]\n    }\n    // Return the extended new array\n    return res\n}\n
array.rb
### Extend array length ###\n# Note: Ruby's Array is dynamic array, can be directly expanded\n# For learning purposes, this function treats Array as fixed-length array\ndef extend(nums, enlarge)\n  # Initialize an array with extended length\n  res = Array.new(nums.length + enlarge, 0)\n\n  # Copy all elements from the original array to the new array\n  for i in 0...nums.length\n    res[i] = nums[i]\n  end\n\n  # Return the extended new array\n  res\nend\n
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#412-advantages-and-limitations-of-arrays","level":2,"title":"4.1.2   Advantages and Limitations of Arrays","text":"

Arrays are stored in contiguous memory space with elements of the same type. This approach contains rich prior information that the system can use to optimize the efficiency of data structure operations.

  • High space efficiency: Arrays allocate contiguous memory blocks for data without additional structural overhead.
  • Support for random access: Arrays allow accessing any element in \\(O(1)\\) time.
  • Cache locality: When accessing array elements, the computer not only loads the element but also caches the surrounding data, thereby leveraging the cache to improve the execution speed of subsequent operations.

Contiguous space storage is a double-edged sword with the following limitations:

  • Low insertion and deletion efficiency: When an array has many elements, insertion and deletion operations require shifting a large number of elements.
  • Immutable length: After an array is initialized, its length is fixed. Expanding the array requires copying all data to a new array, which is very costly.
  • Space waste: If the allocated size of an array exceeds what is actually needed, the extra space is wasted.
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#413-typical-applications-of-arrays","level":2,"title":"4.1.3   Typical Applications of Arrays","text":"

Arrays are a fundamental and common data structure, frequently used in various algorithms and for implementing various complex data structures.

  • Random access: If we want to randomly sample some items, we can use an array to store them and generate a random sequence to implement random sampling based on indices.
  • Sorting and searching: Arrays are the most commonly used data structure for sorting and searching algorithms. Quick sort, merge sort, binary search, and others are primarily performed on arrays.
  • Lookup tables: When we need to quickly find an element or its corresponding relationship, we can use an array as a lookup table. For example, if we want to implement a mapping from characters to ASCII codes, we can use the ASCII code value of a character as an index, with the corresponding element stored at that position in the array.
  • Machine learning: Neural networks make extensive use of linear algebra operations between vectors, matrices, and tensors, all of which are constructed in the form of arrays. Arrays are the most commonly used data structure in neural network programming.
  • Data structure implementation: Arrays can be used to implement stacks, queues, hash tables, heaps, graphs, and other data structures. For example, the adjacency matrix representation of a graph is essentially a two-dimensional array.
","path":["Chapter 4. Array and Linked List","4.1   Array"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   Linked List","text":"

Memory space is a shared resource for all programs. In a complex system runtime environment, available memory space may be scattered throughout the memory. We know that the memory space for storing an array must be contiguous, and when the array is very large, the memory may not be able to provide such a large contiguous space. This is where the flexibility advantage of linked lists becomes apparent.

A linked list is a linear data structure in which each element is a node object, and the nodes are connected through \"references\". A reference records the memory address of the next node, through which the next node can be accessed from the current node.

The design of linked lists allows nodes to be stored scattered throughout the memory, and their memory addresses do not need to be contiguous.

Figure 4-5   Linked list definition and storage method

Observing Figure 4-5, the basic unit of a linked list is a node object. Each node contains two pieces of data: the node's \"value\" and a \"reference\" to the next node.

  • The first node of a linked list is called the \"head node\", and the last node is called the \"tail node\".
  • The tail node points to \"null\", which is denoted as null, nullptr, and None in Java, C++, and Python, respectively.
  • In languages that support pointers, such as C, C++, Go, and Rust, the aforementioned \"reference\" should be replaced with \"pointer\".

As shown in the following code, a linked list node ListNode contains not only a value but also an additional reference (pointer). Therefore, linked lists occupy more memory space than arrays when storing the same amount of data.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class ListNode:\n    \"\"\"Linked list node class\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val               # Node value\n        self.next: ListNode | None = None # Reference to the next node\n
/* Linked list node structure */\nstruct ListNode {\n    int val;         // Node value\n    ListNode *next;  // Pointer to the next node\n    ListNode(int x) : val(x), next(nullptr) {}  // Constructor\n};\n
/* Linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode next;  // Reference to the next node\n    ListNode(int x) { val = x; }  // Constructor\n}\n
/* Linked list node class */\nclass ListNode(int x) {  // Constructor\n    int val = x;         // Node value\n    ListNode? next;      // Reference to the next node\n}\n
/* Linked list node structure */\ntype ListNode struct {\n    Val  int       // Node value\n    Next *ListNode // Pointer to the next node\n}\n\n// NewListNode Constructor, creates a new linked list\nfunc NewListNode(val int) *ListNode {\n    return &ListNode{\n        Val:  val,\n        Next: nil,\n    }\n}\n
/* Linked list node class */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Reference to the next node\n\n    init(x: Int) { // Constructor\n        val = x\n    }\n}\n
/* Linked list node class */\nclass ListNode {\n    constructor(val, next) {\n        this.val = (val === undefined ? 0 : val);       // Node value\n        this.next = (next === undefined ? null : next); // Reference to the next node\n    }\n}\n
/* Linked list node class */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    constructor(val?: number, next?: ListNode | null) {\n        this.val = val === undefined ? 0 : val;        // Node value\n        this.next = next === undefined ? null : next;  // Reference to the next node\n    }\n}\n
/* Linked list node class */\nclass ListNode {\n  int val; // Node value\n  ListNode? next; // Reference to the next node\n  ListNode(this.val, [this.next]); // Constructor\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n/* Linked list node class */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Node value\n    next: Option<Rc<RefCell<ListNode>>>, // Pointer to the next node\n}\n
/* Linked list node structure */\ntypedef struct ListNode {\n    int val;               // Node value\n    struct ListNode *next; // Pointer to the next node\n} ListNode;\n\n/* Constructor */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    return node;\n}\n
/* Linked list node class */\n// Constructor\nclass ListNode(x: Int) {\n    val _val: Int = x          // Node value\n    val next: ListNode? = null // Reference to the next node\n}\n
# Linked list node class\nclass ListNode\n  attr_accessor :val  # Node value\n  attr_accessor :next # Reference to the next node\n\n  def initialize(val=0, next_node=nil)\n    @val = val\n    @next = next_node\n  end\nend\n
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#421-common-linked-list-operations","level":2,"title":"4.2.1   Common Linked List Operations","text":"","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#1-initializing-a-linked-list","level":3,"title":"1.   Initializing a Linked List","text":"

Building a linked list involves two steps: first, initializing each node object; second, constructing the reference relationships between nodes. Once initialization is complete, we can traverse all nodes starting from the head node of the linked list through the reference next.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
# Initialize linked list 1 -> 3 -> 2 -> 5 -> 4\n# Initialize each node\nn0 = ListNode(1)\nn1 = ListNode(3)\nn2 = ListNode(2)\nn3 = ListNode(5)\nn4 = ListNode(4)\n# Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
linked_list.cpp
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode* n0 = new ListNode(1);\nListNode* n1 = new ListNode(3);\nListNode* n2 = new ListNode(2);\nListNode* n3 = new ListNode(5);\nListNode* n4 = new ListNode(4);\n// Build references between nodes\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
linked_list.java
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode n0 = new ListNode(1);\nListNode n1 = new ListNode(3);\nListNode n2 = new ListNode(2);\nListNode n3 = new ListNode(5);\nListNode n4 = new ListNode(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.cs
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode n0 = new(1);\nListNode n1 = new(3);\nListNode n2 = new(2);\nListNode n3 = new(5);\nListNode n4 = new(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.go
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nn0 := NewListNode(1)\nn1 := NewListNode(3)\nn2 := NewListNode(2)\nn3 := NewListNode(5)\nn4 := NewListNode(4)\n// Build references between nodes\nn0.Next = n1\nn1.Next = n2\nn2.Next = n3\nn3.Next = n4\n
linked_list.swift
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nlet n0 = ListNode(x: 1)\nlet n1 = ListNode(x: 3)\nlet n2 = ListNode(x: 2)\nlet n3 = ListNode(x: 5)\nlet n4 = ListNode(x: 4)\n// Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
linked_list.js
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.ts
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.dart
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\\\n// Initialize each node\nListNode n0 = ListNode(1);\nListNode n1 = ListNode(3);\nListNode n2 = ListNode(2);\nListNode n3 = ListNode(5);\nListNode n4 = ListNode(4);\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.rs
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nlet n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));\nlet n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));\nlet n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));\nlet n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));\nlet n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));\n\n// Build references between nodes\nn0.borrow_mut().next = Some(n1.clone());\nn1.borrow_mut().next = Some(n2.clone());\nn2.borrow_mut().next = Some(n3.clone());\nn3.borrow_mut().next = Some(n4.clone());\n
linked_list.c
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nListNode* n0 = newListNode(1);\nListNode* n1 = newListNode(3);\nListNode* n2 = newListNode(2);\nListNode* n3 = newListNode(5);\nListNode* n4 = newListNode(4);\n// Build references between nodes\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
linked_list.kt
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\n// Initialize each node\nval n0 = ListNode(1)\nval n1 = ListNode(3)\nval n2 = ListNode(2)\nval n3 = ListNode(5)\nval n4 = ListNode(4)\n// Build references between nodes\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.rb
# Initialize linked list 1 -> 3 -> 2 -> 5 -> 4\n# Initialize each node\nn0 = ListNode.new(1)\nn1 = ListNode.new(3)\nn2 = ListNode.new(2)\nn3 = ListNode.new(5)\nn4 = ListNode.new(4)\n# Build references between nodes\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
Code Visualization

Full Screen >

An array is a single variable; for example, an array nums contains elements nums[0], nums[1], etc. A linked list, however, is composed of multiple independent node objects. We typically use the head node as the reference to the linked list; for example, the linked list in the above code can be referred to as linked list n0.

","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2-inserting-a-node","level":3,"title":"2.   Inserting a Node","text":"

Inserting a node in a linked list is very easy. As shown in Figure 4-6, suppose we want to insert a new node P between two adjacent nodes n0 and n1. We only need to change two node references (pointers), with a time complexity of \\(O(1)\\).

In contrast, the time complexity of inserting an element in an array is \\(O(n)\\), which is inefficient when dealing with large amounts of data.

Figure 4-6   Example of inserting a node into a linked list

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def insert(n0: ListNode, P: ListNode):\n    \"\"\"Insert node P after node n0 in the linked list\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
linked_list.cpp
/* Insert node P after node n0 in the linked list */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
linked_list.java
/* Insert node P after node n0 in the linked list */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.cs
/* Insert node P after node n0 in the linked list */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.go
/* Insert node P after node n0 in the linked list */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
linked_list.swift
/* Insert node P after node n0 in the linked list */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
linked_list.js
/* Insert node P after node n0 in the linked list */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.ts
/* Insert node P after node n0 in the linked list */\nfunction insert(n0: ListNode, P: ListNode): void {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.dart
/* Insert node P after node n0 in the linked list */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
linked_list.rs
/* Insert node P after node n0 in the linked list */\n#[allow(non_snake_case)]\npub fn insert<T>(n0: &Rc<RefCell<ListNode<T>>>, P: Rc<RefCell<ListNode<T>>>) {\n    let n1 = n0.borrow_mut().next.take();\n    P.borrow_mut().next = n1;\n    n0.borrow_mut().next = Some(P);\n}\n
linked_list.c
/* Insert node P after node n0 in the linked list */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
linked_list.kt
/* Insert node P after node n0 in the linked list */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
linked_list.rb
### Insert node _p after node n0 in linked list ###\n# Ruby's `p` is a built-in function, `P` is a constant, so use `_p` instead\ndef insert(n0, _p)\n  n1 = n0.next\n  _p.next = n1\n  n0.next = _p\nend\n
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#3-removing-a-node","level":3,"title":"3.   Removing a Node","text":"

As shown in Figure 4-7, removing a node in a linked list is also very convenient. We only need to change one node's reference (pointer).

Note that although node P still points to n1 after the deletion operation is complete, the linked list can no longer access P when traversing, which means P no longer belongs to this linked list.

Figure 4-7   Removing a node from a linked list

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def remove(n0: ListNode):\n    \"\"\"Remove the first node after node n0 in the linked list\"\"\"\n    if not n0.next:\n        return\n    # n0 -> P -> n1\n    P = n0.next\n    n1 = P.next\n    n0.next = n1\n
linked_list.cpp
/* Remove the first node after node n0 in the linked list */\nvoid remove(ListNode *n0) {\n    if (n0->next == nullptr)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // Free memory\n    delete P;\n}\n
linked_list.java
/* Remove the first node after node n0 in the linked list */\nvoid remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.cs
/* Remove the first node after node n0 in the linked list */\nvoid Remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode? n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.go
/* Remove the first node after node n0 in the linked list */\nfunc removeItem(n0 *ListNode) {\n    if n0.Next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    P := n0.Next\n    n1 := P.Next\n    n0.Next = n1\n}\n
linked_list.swift
/* Remove the first node after node n0 in the linked list */\nfunc remove(n0: ListNode) {\n    if n0.next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    let P = n0.next\n    let n1 = P?.next\n    n0.next = n1\n}\n
linked_list.js
/* Remove the first node after node n0 in the linked list */\nfunction remove(n0) {\n    if (!n0.next) return;\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.ts
/* Remove the first node after node n0 in the linked list */\nfunction remove(n0: ListNode): void {\n    if (!n0.next) {\n        return;\n    }\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.dart
/* Remove the first node after node n0 in the linked list */\nvoid remove(ListNode n0) {\n  if (n0.next == null) return;\n  // n0 -> P -> n1\n  ListNode P = n0.next!;\n  ListNode? n1 = P.next;\n  n0.next = n1;\n}\n
linked_list.rs
/* Remove the first node after node n0 in the linked list */\n#[allow(non_snake_case)]\npub fn remove<T>(n0: &Rc<RefCell<ListNode<T>>>) {\n    // n0 -> P -> n1\n    let P = n0.borrow_mut().next.take();\n    if let Some(node) = P {\n        let n1 = node.borrow_mut().next.take();\n        n0.borrow_mut().next = n1;\n    }\n}\n
linked_list.c
/* Remove the first node after node n0 in the linked list */\n// Note: stdio.h occupies the remove keyword\nvoid removeItem(ListNode *n0) {\n    if (!n0->next)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // Free memory\n    free(P);\n}\n
linked_list.kt
/* Remove the first node after node n0 in the linked list */\nfun remove(n0: ListNode?) {\n    if (n0?.next == null)\n        return\n    // n0 -> P -> n1\n    val p = n0.next\n    val n1 = p?.next\n    n0.next = n1\n}\n
linked_list.rb
### Delete first node after node n0 in linked list ###\ndef remove(n0)\n  return if n0.next.nil?\n\n  # n0 -> remove_node -> n1\n  remove_node = n0.next\n  n1 = remove_node.next\n  n0.next = n1\nend\n
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#4-accessing-a-node","level":3,"title":"4.   Accessing a Node","text":"

Accessing nodes in a linked list is less efficient. As mentioned in the previous section, we can access any element in an array in \\(O(1)\\) time. This is not the case with linked lists. The program needs to start from the head node and traverse backward one by one until the target node is found. That is, accessing the \\(i\\)-th node in a linked list requires \\(i - 1\\) iterations, with a time complexity of \\(O(n)\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def access(head: ListNode, index: int) -> ListNode | None:\n    \"\"\"Access the node at index index in the linked list\"\"\"\n    for _ in range(index):\n        if not head:\n            return None\n        head = head.next\n    return head\n
linked_list.cpp
/* Access the node at index index in the linked list */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == nullptr)\n            return nullptr;\n        head = head->next;\n    }\n    return head;\n}\n
linked_list.java
/* Access the node at index index in the linked list */\nListNode access(ListNode head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.cs
/* Access the node at index index in the linked list */\nListNode? Access(ListNode? head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.go
/* Access the node at index index in the linked list */\nfunc access(head *ListNode, index int) *ListNode {\n    for i := 0; i < index; i++ {\n        if head == nil {\n            return nil\n        }\n        head = head.Next\n    }\n    return head\n}\n
linked_list.swift
/* Access the node at index index in the linked list */\nfunc access(head: ListNode, index: Int) -> ListNode? {\n    var head: ListNode? = head\n    for _ in 0 ..< index {\n        if head == nil {\n            return nil\n        }\n        head = head?.next\n    }\n    return head\n}\n
linked_list.js
/* Access the node at index index in the linked list */\nfunction access(head, index) {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.ts
/* Access the node at index index in the linked list */\nfunction access(head: ListNode | null, index: number): ListNode | null {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.dart
/* Access the node at index index in the linked list */\nListNode? access(ListNode? head, int index) {\n  for (var i = 0; i < index; i++) {\n    if (head == null) return null;\n    head = head.next;\n  }\n  return head;\n}\n
linked_list.rs
/* Access the node at index index in the linked list */\npub fn access<T>(head: Rc<RefCell<ListNode<T>>>, index: i32) -> Option<Rc<RefCell<ListNode<T>>>> {\n    fn dfs<T>(\n        head: Option<&Rc<RefCell<ListNode<T>>>>,\n        index: i32,\n    ) -> Option<Rc<RefCell<ListNode<T>>>> {\n        if index <= 0 {\n            return head.cloned();\n        }\n\n        if let Some(node) = head {\n            dfs(node.borrow().next.as_ref(), index - 1)\n        } else {\n            None\n        }\n    }\n\n    dfs(Some(head).as_ref(), index)\n}\n
linked_list.c
/* Access the node at index index in the linked list */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == NULL)\n            return NULL;\n        head = head->next;\n    }\n    return head;\n}\n
linked_list.kt
/* Access the node at index index in the linked list */\nfun access(head: ListNode?, index: Int): ListNode? {\n    var h = head\n    for (i in 0..<index) {\n        if (h == null)\n            return null\n        h = h.next\n    }\n    return h\n}\n
linked_list.rb
### Access node at index in linked list ###\ndef access(head, index)\n  for i in 0...index\n    return nil if head.nil?\n    head = head.next\n  end\n\n  head\nend\n
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#5-finding-a-node","level":3,"title":"5.   Finding a Node","text":"

Traverse the linked list to find a node with value target, and output the index of that node in the linked list. This process is also a linear search. The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def find(head: ListNode, target: int) -> int:\n    \"\"\"Find the first node with value target in the linked list\"\"\"\n    index = 0\n    while head:\n        if head.val == target:\n            return index\n        head = head.next\n        index += 1\n    return -1\n
linked_list.cpp
/* Find the first node with value target in the linked list */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head != nullptr) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.java
/* Find the first node with value target in the linked list */\nint find(ListNode head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.cs
/* Find the first node with value target in the linked list */\nint Find(ListNode? head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.go
/* Find the first node with value target in the linked list */\nfunc findNode(head *ListNode, target int) int {\n    index := 0\n    for head != nil {\n        if head.Val == target {\n            return index\n        }\n        head = head.Next\n        index++\n    }\n    return -1\n}\n
linked_list.swift
/* Find the first node with value target in the linked list */\nfunc find(head: ListNode, target: Int) -> Int {\n    var head: ListNode? = head\n    var index = 0\n    while head != nil {\n        if head?.val == target {\n            return index\n        }\n        head = head?.next\n        index += 1\n    }\n    return -1\n}\n
linked_list.js
/* Find the first node with value target in the linked list */\nfunction find(head, target) {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
linked_list.ts
/* Find the first node with value target in the linked list */\nfunction find(head: ListNode | null, target: number): number {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
linked_list.dart
/* Find the first node with value target in the linked list */\nint find(ListNode? head, int target) {\n  int index = 0;\n  while (head != null) {\n    if (head.val == target) {\n      return index;\n    }\n    head = head.next;\n    index++;\n  }\n  return -1;\n}\n
linked_list.rs
/* Find the first node with value target in the linked list */\npub fn find<T: PartialEq>(head: Rc<RefCell<ListNode<T>>>, target: T) -> i32 {\n    fn find<T: PartialEq>(head: Option<&Rc<RefCell<ListNode<T>>>>, target: T, idx: i32) -> i32 {\n        if let Some(node) = head {\n            if node.borrow().val == target {\n                return idx;\n            }\n            return find(node.borrow().next.as_ref(), target, idx + 1);\n        } else {\n            -1\n        }\n    }\n\n    find(Some(head).as_ref(), target, 0)\n}\n
linked_list.c
/* Find the first node with value target in the linked list */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.kt
/* Find the first node with value target in the linked list */\nfun find(head: ListNode?, target: Int): Int {\n    var index = 0\n    var h = head\n    while (h != null) {\n        if (h._val == target)\n            return index\n        h = h.next\n        index++\n    }\n    return -1\n}\n
linked_list.rb
### Find first node with value target in linked list ###\ndef find(head, target)\n  index = 0\n  while head\n    return index if head.val == target\n    head = head.next\n    index += 1\n  end\n\n  -1\nend\n
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#422-arrays-vs-linked-lists","level":2,"title":"4.2.2   Arrays vs. Linked Lists","text":"

Table 4-1 summarizes the characteristics of arrays and linked lists and compares their operational efficiencies. Since they employ two opposite storage strategies, their various properties and operational efficiencies also exhibit contrasting characteristics.

Table 4-1   Comparison of array and linked list efficiencies

Array Linked List Storage method Contiguous memory space Scattered memory space Capacity expansion Immutable length Flexible expansion Memory efficiency Elements occupy less memory, but space may be wasted Elements occupy more memory Accessing an element \\(O(1)\\) \\(O(n)\\) Adding an element \\(O(n)\\) \\(O(1)\\) Removing an element \\(O(n)\\) \\(O(1)\\)","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#423-common-types-of-linked-lists","level":2,"title":"4.2.3   Common Types of Linked Lists","text":"

As shown in Figure 4-8, there are three common types of linked lists:

  • Singly linked list: This is the ordinary linked list introduced earlier. The nodes of a singly linked list contain a value and a reference to the next node. We call the first node the head node and the last node the tail node, which points to null None.
  • Circular linked list: If we make the tail node of a singly linked list point to the head node (connecting the tail to the head), we get a circular linked list. In a circular linked list, any node can be viewed as the head node.
  • Doubly linked list: Compared to a singly linked list, a doubly linked list records references in both directions. The node definition of a doubly linked list includes references to both the successor node (next node) and the predecessor node (previous node). Compared to a singly linked list, a doubly linked list is more flexible and can traverse the linked list in both directions, but it also requires more memory space.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class ListNode:\n    \"\"\"Doubly linked list node class\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Node value\n        self.next: ListNode | None = None  # Reference to the successor node\n        self.prev: ListNode | None = None  # Reference to the predecessor node\n
/* Doubly linked list node structure */\nstruct ListNode {\n    int val;         // Node value\n    ListNode *next;  // Pointer to the successor node\n    ListNode *prev;  // Pointer to the predecessor node\n    ListNode(int x) : val(x), next(nullptr), prev(nullptr) {}  // Constructor\n};\n
/* Doubly linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode next;  // Reference to the successor node\n    ListNode prev;  // Reference to the predecessor node\n    ListNode(int x) { val = x; }  // Constructor\n}\n
/* Doubly linked list node class */\nclass ListNode(int x) {  // Constructor\n    int val = x;    // Node value\n    ListNode next;  // Reference to the successor node\n    ListNode prev;  // Reference to the predecessor node\n}\n
/* Doubly linked list node structure */\ntype DoublyListNode struct {\n    Val  int             // Node value\n    Next *DoublyListNode // Pointer to the successor node\n    Prev *DoublyListNode // Pointer to the predecessor node\n}\n\n// NewDoublyListNode Initialization\nfunc NewDoublyListNode(val int) *DoublyListNode {\n    return &DoublyListNode{\n        Val:  val,\n        Next: nil,\n        Prev: nil,\n    }\n}\n
/* Doubly linked list node class */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Reference to the successor node\n    var prev: ListNode? // Reference to the predecessor node\n\n    init(x: Int) { // Constructor\n        val = x\n    }\n}\n
/* Doubly linked list node class */\nclass ListNode {\n    constructor(val, next, prev) {\n        this.val = val  ===  undefined ? 0 : val;        // Node value\n        this.next = next  ===  undefined ? null : next;  // Reference to the successor node\n        this.prev = prev  ===  undefined ? null : prev;  // Reference to the predecessor node\n    }\n}\n
/* Doubly linked list node class */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    prev: ListNode | null;\n    constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {\n        this.val = val  ===  undefined ? 0 : val;        // Node value\n        this.next = next  ===  undefined ? null : next;  // Reference to the successor node\n        this.prev = prev  ===  undefined ? null : prev;  // Reference to the predecessor node\n    }\n}\n
/* Doubly linked list node class */\nclass ListNode {\n    int val;        // Node value\n    ListNode? next;  // Reference to the successor node\n    ListNode? prev;  // Reference to the predecessor node\n    ListNode(this.val, [this.next, this.prev]);  // Constructor\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Doubly linked list node type */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // Node value\n    next: Option<Rc<RefCell<ListNode>>>, // Pointer to the successor node\n    prev: Option<Rc<RefCell<ListNode>>>, // Pointer to the predecessor node\n}\n\n/* Constructor */\nimpl ListNode {\n    fn new(val: i32) -> Self {\n        ListNode {\n            val,\n            next: None,\n            prev: None,\n        }\n    }\n}\n
/* Doubly linked list node structure */\ntypedef struct ListNode {\n    int val;               // Node value\n    struct ListNode *next; // Pointer to the successor node\n    struct ListNode *prev; // Pointer to the predecessor node\n} ListNode;\n\n/* Constructor */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    node->prev = NULL;\n    return node;\n}\n
/* Doubly linked list node class */\n// Constructor\nclass ListNode(x: Int) {\n    val _val: Int = x           // Node value\n    val next: ListNode? = null  // Reference to the successor node\n    val prev: ListNode? = null  // Reference to the predecessor node\n}\n
# Doubly linked list node class\nclass ListNode\n  attr_accessor :val    # Node value\n  attr_accessor :next   # Reference to the successor node\n  attr_accessor :prev   # Reference to the predecessor node\n\n  def initialize(val=0, next_node=nil, prev_node=nil)\n    @val = val\n    @next = next_node\n    @prev = prev_node\n  end\nend\n

Figure 4-8   Common types of linked lists

","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#424-typical-applications-of-linked-lists","level":2,"title":"4.2.4   Typical Applications of Linked Lists","text":"

Singly linked lists are commonly used to implement stacks, queues, hash tables, and graphs.

  • Stacks and queues: When insertion and deletion operations both occur at one end of the linked list, it exhibits last-in-first-out characteristics, corresponding to a stack. When insertion operations occur at one end of the linked list and deletion operations occur at the other end, it exhibits first-in-first-out characteristics, corresponding to a queue.
  • Hash tables: Separate chaining is one of the mainstream solutions for resolving hash collisions. In this approach, all colliding elements are placed in a linked list.
  • Graphs: An adjacency list is a common way to represent a graph, where each vertex in the graph is associated with a linked list, and each element in the linked list represents another vertex connected to that vertex.

Doubly linked lists are commonly used in scenarios where quick access to the previous and next elements is needed.

  • Advanced data structures: For example, in red-black trees and B-trees, we need to access the parent node of a node, which can be achieved by saving a reference to the parent node in the node, similar to a doubly linked list.
  • Browser history: In web browsers, when a user clicks the forward or backward button, the browser needs to know the previous and next web pages the user visited. The characteristics of doubly linked lists make this operation simple.
  • LRU algorithm: In cache eviction (LRU) algorithms, we need to quickly find the least recently used data and support quick addition and deletion of nodes. Using a doubly linked list is very suitable for this.

Circular linked lists are commonly used in scenarios that require periodic operations, such as operating system resource scheduling.

  • Round-robin scheduling algorithm: In operating systems, round-robin scheduling is a common CPU scheduling algorithm that needs to cycle through a set of processes. Each process is assigned a time slice, and when the time slice expires, the CPU switches to the next process. This cyclic operation can be implemented using a circular linked list.
  • Data buffers: In some data buffer implementations, circular linked lists may also be used. For example, in audio and video players, the data stream may be divided into multiple buffer blocks and placed in a circular linked list to achieve seamless playback.
","path":["Chapter 4. Array and Linked List","4.2   Linked List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   List","text":"

A list is an abstract data structure concept that represents an ordered collection of elements, supporting operations such as element access, modification, insertion, deletion, and traversal, without requiring users to consider capacity limitations. Lists can be implemented based on linked lists or arrays.

  • A linked list can naturally be viewed as a list, supporting element insertion, deletion, search, and modification operations, and can flexibly expand dynamically.
  • An array also supports element insertion, deletion, search, and modification, but since its length is immutable, it can only be viewed as a list with length limitations.

When implementing lists using arrays, the immutable length property reduces the practicality of the list. This is because we usually cannot determine in advance how much data we need to store, making it difficult to choose an appropriate list length. If the length is too small, it may fail to meet usage requirements; if the length is too large, it will waste memory space.

To solve this problem, we can use a dynamic array to implement a list. It inherits all the advantages of arrays and can dynamically expand during program execution.

In fact, the lists provided in the standard libraries of many programming languages are implemented based on dynamic arrays, such as list in Python, ArrayList in Java, vector in C++, and List in C#. In the following discussion, we will treat \"list\" and \"dynamic array\" as equivalent concepts.

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#431-common-list-operations","level":2,"title":"4.3.1   Common List Operations","text":"","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#1-initialize-a-list","level":3,"title":"1.   Initialize a List","text":"

We typically use two initialization methods: \"without initial values\" and \"with initial values\":

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Initialize a list\n# Without initial values\nnums1: list[int] = []\n# With initial values\nnums: list[int] = [1, 3, 2, 5, 4]\n
list.cpp
/* Initialize a list */\n// Note that vector in C++ is equivalent to nums as described in this article\n// Without initial values\nvector<int> nums1;\n// With initial values\nvector<int> nums = { 1, 3, 2, 5, 4 };\n
list.java
/* Initialize a list */\n// Without initial values\nList<Integer> nums1 = new ArrayList<>();\n// With initial values (note that array elements should use the wrapper class Integer[] instead of int[])\nInteger[] numbers = new Integer[] { 1, 3, 2, 5, 4 };\nList<Integer> nums = new ArrayList<>(Arrays.asList(numbers));\n
list.cs
/* Initialize a list */\n// Without initial values\nList<int> nums1 = [];\n// With initial values\nint[] numbers = [1, 3, 2, 5, 4];\nList<int> nums = [.. numbers];\n
list_test.go
/* Initialize a list */\n// Without initial values\nnums1 := []int{}\n// With initial values\nnums := []int{1, 3, 2, 5, 4}\n
list.swift
/* Initialize a list */\n// Without initial values\nlet nums1: [Int] = []\n// With initial values\nvar nums = [1, 3, 2, 5, 4]\n
list.js
/* Initialize a list */\n// Without initial values\nconst nums1 = [];\n// With initial values\nconst nums = [1, 3, 2, 5, 4];\n
list.ts
/* Initialize a list */\n// Without initial values\nconst nums1: number[] = [];\n// With initial values\nconst nums: number[] = [1, 3, 2, 5, 4];\n
list.dart
/* Initialize a list */\n// Without initial values\nList<int> nums1 = [];\n// With initial values\nList<int> nums = [1, 3, 2, 5, 4];\n
list.rs
/* Initialize a list */\n// Without initial values\nlet nums1: Vec<i32> = Vec::new();\n// With initial values\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Initialize a list */\n// Without initial values\nvar nums1 = listOf<Int>()\n// With initial values\nvar numbers = arrayOf(1, 3, 2, 5, 4)\nvar nums = numbers.toMutableList()\n
list.rb
# Initialize a list\n# Without initial values\nnums1 = []\n# With initial values\nnums = [1, 3, 2, 5, 4]\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#2-access-elements","level":3,"title":"2.   Access Elements","text":"

Since a list is essentially an array, we can access and update elements in \\(O(1)\\) time complexity, which is very efficient.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Access an element\nnum: int = nums[1]  # Access element at index 1\n\n# Update an element\nnums[1] = 0    # Update element at index 1 to 0\n
list.cpp
/* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
list.java
/* Access an element */\nint num = nums.get(1);  // Access element at index 1\n\n/* Update an element */\nnums.set(1, 0);  // Update element at index 1 to 0\n
list.cs
/* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
list_test.go
/* Access an element */\nnum := nums[1]  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0     // Update element at index 1 to 0\n
list.swift
/* Access an element */\nlet num = nums[1] // Access element at index 1\n\n/* Update an element */\nnums[1] = 0 // Update element at index 1 to 0\n
list.js
/* Access an element */\nconst num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
list.ts
/* Access an element */\nconst num: number = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
list.dart
/* Access an element */\nint num = nums[1];  // Access element at index 1\n\n/* Update an element */\nnums[1] = 0;  // Update element at index 1 to 0\n
list.rs
/* Access an element */\nlet num: i32 = nums[1];  // Access element at index 1\n/* Update an element */\nnums[1] = 0;             // Update element at index 1 to 0\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Access an element */\nval num = nums[1]       // Access element at index 1\n/* Update an element */\nnums[1] = 0             // Update element at index 1 to 0\n
list.rb
# Access an element\nnum = nums[1] # Access element at index 1\n# Update an element\nnums[1] = 0 # Update element at index 1 to 0\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#3-insert-and-delete-elements","level":3,"title":"3.   Insert and Delete Elements","text":"

Compared to arrays, lists can freely add and delete elements. Adding an element at the end of a list has a time complexity of \\(O(1)\\), but inserting and deleting elements still have the same efficiency as arrays, with a time complexity of \\(O(n)\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Clear the list\nnums.clear()\n\n# Add elements at the end\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n# Insert an element in the middle\nnums.insert(3, 6)  # Insert number 6 at index 3\n\n# Delete an element\nnums.pop(3)        # Delete element at index 3\n
list.cpp
/* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.push_back(1);\nnums.push_back(3);\nnums.push_back(2);\nnums.push_back(5);\nnums.push_back(4);\n\n/* Insert an element in the middle */\nnums.insert(nums.begin() + 3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.erase(nums.begin() + 3);      // Delete element at index 3\n
list.java
/* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.add(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);  // Delete element at index 3\n
list.cs
/* Clear the list */\nnums.Clear();\n\n/* Add elements at the end */\nnums.Add(1);\nnums.Add(3);\nnums.Add(2);\nnums.Add(5);\nnums.Add(4);\n\n/* Insert an element in the middle */\nnums.Insert(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.RemoveAt(3);  // Delete element at index 3\n
list_test.go
/* Clear the list */\nnums = nil\n\n/* Add elements at the end */\nnums = append(nums, 1)\nnums = append(nums, 3)\nnums = append(nums, 2)\nnums = append(nums, 5)\nnums = append(nums, 4)\n\n/* Insert an element in the middle */\nnums = append(nums[:3], append([]int{6}, nums[3:]...)...) // Insert number 6 at index 3\n\n/* Delete an element */\nnums = append(nums[:3], nums[4:]...) // Delete element at index 3\n
list.swift
/* Clear the list */\nnums.removeAll()\n\n/* Add elements at the end */\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n/* Insert an element in the middle */\nnums.insert(6, at: 3) // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(at: 3) // Delete element at index 3\n
list.js
/* Clear the list */\nnums.length = 0;\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.splice(3, 0, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.splice(3, 1);  // Delete element at index 3\n
list.ts
/* Clear the list */\nnums.length = 0;\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.splice(3, 0, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.splice(3, 1);  // Delete element at index 3\n
list.dart
/* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.insert(3, 6); // Insert number 6 at index 3\n\n/* Delete an element */\nnums.removeAt(3); // Delete element at index 3\n
list.rs
/* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* Insert an element in the middle */\nnums.insert(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);    // Delete element at index 3\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Clear the list */\nnums.clear();\n\n/* Add elements at the end */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* Insert an element in the middle */\nnums.add(3, 6);  // Insert number 6 at index 3\n\n/* Delete an element */\nnums.remove(3);  // Delete element at index 3\n
list.rb
# Clear the list\nnums.clear\n\n# Add elements at the end\nnums << 1\nnums << 3\nnums << 2\nnums << 5\nnums << 4\n\n# Insert an element in the middle\nnums.insert(3, 6) # Insert number 6 at index 3\n\n# Delete an element\nnums.delete_at(3) # Delete element at index 3\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#4-traverse-a-list","level":3,"title":"4.   Traverse a List","text":"

Like arrays, lists can be traversed by index or by directly iterating through elements.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Traverse the list by index\ncount = 0\nfor i in range(len(nums)):\n    count += nums[i]\n\n# Traverse list elements directly\nfor num in nums:\n    count += num\n
list.cpp
/* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (int num : nums) {\n    count += num;\n}\n
list.java
/* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums.get(i);\n}\n\n/* Traverse list elements directly */\nfor (int num : nums) {\n    count += num;\n}\n
list.cs
/* Traverse the list by index */\nint count = 0;\nfor (int i = 0; i < nums.Count; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nforeach (int num in nums) {\n    count += num;\n}\n
list_test.go
/* Traverse the list by index */\ncount := 0\nfor i := 0; i < len(nums); i++ {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\ncount = 0\nfor _, num := range nums {\n    count += num\n}\n
list.swift
/* Traverse the list by index */\nvar count = 0\nfor i in nums.indices {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\ncount = 0\nfor num in nums {\n    count += num\n}\n
list.js
/* Traverse the list by index */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
list.ts
/* Traverse the list by index */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
list.dart
/* Traverse the list by index */\nint count = 0;\nfor (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* Traverse list elements directly */\ncount = 0;\nfor (var num in nums) {\n    count += num;\n}\n
list.rs
// Traverse the list by index\nlet mut _count = 0;\nfor i in 0..nums.len() {\n    _count += nums[i];\n}\n\n// Traverse list elements directly\n_count = 0;\nfor num in &nums {\n    _count += num;\n}\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Traverse the list by index */\nvar count = 0\nfor (i in nums.indices) {\n    count += nums[i]\n}\n\n/* Traverse list elements directly */\nfor (num in nums) {\n    count += num\n}\n
list.rb
# Traverse the list by index\ncount = 0\nfor i in 0...nums.length\n    count += nums[i]\nend\n\n# Traverse list elements directly\ncount = 0\nfor num in nums\n    count += num\nend\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#5-concatenate-lists","level":3,"title":"5.   Concatenate Lists","text":"

Given a new list nums1, we can concatenate it to the end of the original list.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Concatenate two lists\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1  # Concatenate list nums1 to the end of nums\n
list.cpp
/* Concatenate two lists */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// Concatenate list nums1 to the end of nums\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n
list.java
/* Concatenate two lists */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1);  // Concatenate list nums1 to the end of nums\n
list.cs
/* Concatenate two lists */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1);  // Concatenate list nums1 to the end of nums\n
list_test.go
/* Concatenate two lists */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...)  // Concatenate list nums1 to the end of nums\n
list.swift
/* Concatenate two lists */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // Concatenate list nums1 to the end of nums\n
list.js
/* Concatenate two lists */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Concatenate list nums1 to the end of nums\n
list.ts
/* Concatenate two lists */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // Concatenate list nums1 to the end of nums\n
list.dart
/* Concatenate two lists */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1);  // Concatenate list nums1 to the end of nums\n
list.rs
/* Concatenate two lists */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Concatenate two lists */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1)  // Concatenate list nums1 to the end of nums\n
list.rb
# Concatenate two lists\nnums1 = [6, 8, 7, 10, 9]\nnums += nums1\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#6-sort-a-list","level":3,"title":"6.   Sort a List","text":"

After sorting a list, we can use \"binary search\" and \"two-pointer\" algorithms, which are frequently tested in array algorithm problems.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# Sort a list\nnums.sort()  # After sorting, list elements are arranged from smallest to largest\n
list.cpp
/* Sort a list */\nsort(nums.begin(), nums.end());  // After sorting, list elements are arranged from smallest to largest\n
list.java
/* Sort a list */\nCollections.sort(nums);  // After sorting, list elements are arranged from smallest to largest\n
list.cs
/* Sort a list */\nnums.Sort(); // After sorting, list elements are arranged from smallest to largest\n
list_test.go
/* Sort a list */\nsort.Ints(nums)  // After sorting, list elements are arranged from smallest to largest\n
list.swift
/* Sort a list */\nnums.sort() // After sorting, list elements are arranged from smallest to largest\n
list.js
/* Sort a list */\nnums.sort((a, b) => a - b);  // After sorting, list elements are arranged from smallest to largest\n
list.ts
/* Sort a list */\nnums.sort((a, b) => a - b);  // After sorting, list elements are arranged from smallest to largest\n
list.dart
/* Sort a list */\nnums.sort(); // After sorting, list elements are arranged from smallest to largest\n
list.rs
/* Sort a list */\nnums.sort(); // After sorting, list elements are arranged from smallest to largest\n
list.c
// C does not provide built-in dynamic arrays\n
list.kt
/* Sort a list */\nnums.sort() // After sorting, list elements are arranged from smallest to largest\n
list.rb
# Sort a list\nnums = nums.sort { |a, b| a <=> b } # After sorting, list elements are arranged from smallest to largest\n
Code Visualization

Full Screen >

","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#432-list-implementation","level":2,"title":"4.3.2   List Implementation","text":"

Many programming languages have built-in lists, such as Java, C++, and Python. Their implementations are quite complex, and the parameters are carefully considered, such as initial capacity, expansion multiples, and so on. Interested readers can consult the source code to learn more.

To deepen our understanding of how lists work, we attempt to implement a simple list with three key design considerations:

  • Initial capacity: Select a reasonable initial capacity for the underlying array. In this example, we choose 10 as the initial capacity.
  • Size tracking: Declare a variable size to record the current number of elements in the list and update it in real-time as elements are inserted and deleted. Based on this variable, we can locate the end of the list and determine whether expansion is needed.
  • Expansion mechanism: When the list capacity is full upon inserting an element, we need to expand. We create a larger array based on the expansion multiple and then move all elements from the current array to the new array in order. In this example, we specify that the array should be expanded to 2 times its previous size each time.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_list.py
class MyList:\n    \"\"\"List class\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._capacity: int = 10  # List capacity\n        self._arr: list[int] = [0] * self._capacity  # Array (stores list elements)\n        self._size: int = 0  # List length (current number of elements)\n        self._extend_ratio: int = 2  # Multiple by which the list capacity is extended each time\n\n    def size(self) -> int:\n        \"\"\"Get list length (current number of elements)\"\"\"\n        return self._size\n\n    def capacity(self) -> int:\n        \"\"\"Get list capacity\"\"\"\n        return self._capacity\n\n    def get(self, index: int) -> int:\n        \"\"\"Access element\"\"\"\n        # If the index is out of bounds, throw an exception, as below\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        return self._arr[index]\n\n    def set(self, num: int, index: int):\n        \"\"\"Update element\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        self._arr[index] = num\n\n    def add(self, num: int):\n        \"\"\"Add element at the end\"\"\"\n        # When the number of elements exceeds capacity, trigger the extension mechanism\n        if self.size() == self.capacity():\n            self.extend_capacity()\n        self._arr[self._size] = num\n        self._size += 1\n\n    def insert(self, num: int, index: int):\n        \"\"\"Insert element in the middle\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        # When the number of elements exceeds capacity, trigger the extension mechanism\n        if self._size == self.capacity():\n            self.extend_capacity()\n        # Move all elements at and after index index backward by one position\n        for j in range(self._size - 1, index - 1, -1):\n            self._arr[j + 1] = self._arr[j]\n        self._arr[index] = num\n        # Update the number of elements\n        self._size += 1\n\n    def remove(self, index: int) -> int:\n        \"\"\"Remove element\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"Index out of bounds\")\n        num = self._arr[index]\n        # Move all elements after index index forward by one position\n        for j in range(index, self._size - 1):\n            self._arr[j] = self._arr[j + 1]\n        # Update the number of elements\n        self._size -= 1\n        # Return the removed element\n        return num\n\n    def extend_capacity(self):\n        \"\"\"Extend list capacity\"\"\"\n        # Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1)\n        # Update list capacity\n        self._capacity = len(self._arr)\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return list with valid length\"\"\"\n        return self._arr[: self._size]\n
my_list.cpp
/* List class */\nclass MyList {\n  private:\n    int *arr;             // Array (stores list elements)\n    int arrCapacity = 10; // List capacity\n    int arrSize = 0;      // List length (current number of elements)\n    int extendRatio = 2;   // Multiple by which the list capacity is extended each time\n\n  public:\n    /* Constructor */\n    MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Destructor */\n    ~MyList() {\n        delete[] arr;\n    }\n\n    /* Get list length (current number of elements)*/\n    int size() {\n        return arrSize;\n    }\n\n    /* Get list capacity */\n    int capacity() {\n        return arrCapacity;\n    }\n\n    /* Update element */\n    int get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    void set(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    void add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size() == capacity())\n            extendCapacity();\n        arr[size()] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Sort list */\n    void insert(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size() == capacity())\n            extendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = size() - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Remove element */\n    int remove(int index) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"Index out of bounds\");\n        int num = arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for (int j = index; j < size() - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        arrSize--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    void extendCapacity() {\n        // Create a new array with length extendRatio times the original array\n        int newCapacity = capacity() * extendRatio;\n        int *tmp = arr;\n        arr = new int[newCapacity];\n        // Copy all elements from the original array to the new array\n        for (int i = 0; i < size(); i++) {\n            arr[i] = tmp[i];\n        }\n        // Free memory\n        delete[] tmp;\n        arrCapacity = newCapacity;\n    }\n\n    /* Convert list to Vector for printing */\n    vector<int> toVector() {\n        // Elements enqueue\n        vector<int> vec(size());\n        for (int i = 0; i < size(); i++) {\n            vec[i] = arr[i];\n        }\n        return vec;\n    }\n};\n
my_list.java
/* List class */\nclass MyList {\n    private int[] arr; // Array (stores list elements)\n    private int capacity = 10; // List capacity\n    private int size = 0; // List length (current number of elements)\n    private int extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    public MyList() {\n        arr = new int[capacity];\n    }\n\n    /* Get list length (current number of elements) */\n    public int size() {\n        return size;\n    }\n\n    /* Get list capacity */\n    public int capacity() {\n        return capacity;\n    }\n\n    /* Update element */\n    public int get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    public void set(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public void add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity();\n        arr[size] = num;\n        // Update the number of elements\n        size++;\n    }\n\n    /* Sort list */\n    public void insert(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = size - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        size++;\n    }\n\n    /* Remove element */\n    public int remove(int index) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"Index out of bounds\");\n        int num = arr[index];\n        // Move all elements after index forward by one position\n        for (int j = index; j < size - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public void extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = Arrays.copyOf(arr, capacity() * extendRatio);\n        // Add elements at the end\n        capacity = arr.length;\n    }\n\n    /* Convert list to array */\n    public int[] toArray() {\n        int size = size();\n        // Elements enqueue\n        int[] arr = new int[size];\n        for (int i = 0; i < size; i++) {\n            arr[i] = get(i);\n        }\n        return arr;\n    }\n}\n
my_list.cs
/* List class */\nclass MyList {\n    private int[] arr;           // Array (stores list elements)\n    private int arrCapacity = 10;    // List capacity\n    private int arrSize = 0;         // List length (current number of elements)\n    private readonly int extendRatio = 2;  // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    public MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* Get list length (current number of elements) */\n    public int Size() {\n        return arrSize;\n    }\n\n    /* Get list capacity */\n    public int Capacity() {\n        return arrCapacity;\n    }\n\n    /* Update element */\n    public int Get(int index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        return arr[index];\n    }\n\n    /* Add elements at the end */\n    public void Set(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public void Add(int num) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        arr[arrSize] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Sort list */\n    public void Insert(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        // Move all elements after index index forward by one position\n        for (int j = arrSize - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // Update the number of elements\n        arrSize++;\n    }\n\n    /* Remove element */\n    public int Remove(int index) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"Index out of bounds\");\n        int num = arr[index];\n        // Move all elements after index forward by one position\n        for (int j = index; j < arrSize - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // Update the number of elements\n        arrSize--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public void ExtendCapacity() {\n        // Create new array of length arrCapacity * extendRatio and copy original array to new array\n        Array.Resize(ref arr, arrCapacity * extendRatio);\n        // Add elements at the end\n        arrCapacity = arr.Length;\n    }\n\n    /* Convert list to array */\n    public int[] ToArray() {\n        // Elements enqueue\n        int[] arr = new int[arrSize];\n        for (int i = 0; i < arrSize; i++) {\n            arr[i] = Get(i);\n        }\n        return arr;\n    }\n}\n
my_list.go
/* List class */\ntype myList struct {\n    arrCapacity int\n    arr         []int\n    arrSize     int\n    extendRatio int\n}\n\n/* Constructor */\nfunc newMyList() *myList {\n    return &myList{\n        arrCapacity: 10,              // List capacity\n        arr:         make([]int, 10), // Array (stores list elements)\n        arrSize:     0,               // List length (current number of elements)\n        extendRatio: 2,               // Multiple by which the list capacity is extended each time\n    }\n}\n\n/* Get list length (current number of elements) */\nfunc (l *myList) size() int {\n    return l.arrSize\n}\n\n/* Get list capacity */\nfunc (l *myList) capacity() int {\n    return l.arrCapacity\n}\n\n/* Update element */\nfunc (l *myList) get(index int) int {\n    // If the index is out of bounds, throw an exception, as below\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    return l.arr[index]\n}\n\n/* Add elements at the end */\nfunc (l *myList) set(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    l.arr[index] = num\n}\n\n/* Direct traversal of list elements */\nfunc (l *myList) add(num int) {\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    l.arr[l.arrSize] = num\n    // Update the number of elements\n    l.arrSize++\n}\n\n/* Sort list */\nfunc (l *myList) insert(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    // Move all elements after index index forward by one position\n    for j := l.arrSize - 1; j >= index; j-- {\n        l.arr[j+1] = l.arr[j]\n    }\n    l.arr[index] = num\n    // Update the number of elements\n    l.arrSize++\n}\n\n/* Remove element */\nfunc (l *myList) remove(index int) int {\n    if index < 0 || index >= l.arrSize {\n        panic(\"Index out of bounds\")\n    }\n    num := l.arr[index]\n    // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n    for j := index; j < l.arrSize-1; j++ {\n        l.arr[j] = l.arr[j+1]\n    }\n    // Update the number of elements\n    l.arrSize--\n    // Return the removed element\n    return num\n}\n\n/* Driver Code */\nfunc (l *myList) extendCapacity() {\n    // Create a new array with length extendRatio times the original array and copy the original array to the new array\n    l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...)\n    // Add elements at the end\n    l.arrCapacity = len(l.arr)\n}\n\n/* Return list with valid length */\nfunc (l *myList) toArray() []int {\n    // Elements enqueue\n    return l.arr[:l.arrSize]\n}\n
my_list.swift
/* List class */\nclass MyList {\n    private var arr: [Int] // Array (stores list elements)\n    private var _capacity: Int // List capacity\n    private var _size: Int // List length (current number of elements)\n    private let extendRatio: Int // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    init() {\n        _capacity = 10\n        _size = 0\n        extendRatio = 2\n        arr = Array(repeating: 0, count: _capacity)\n    }\n\n    /* Get list length (current number of elements) */\n    func size() -> Int {\n        _size\n    }\n\n    /* Get list capacity */\n    func capacity() -> Int {\n        _capacity\n    }\n\n    /* Update element */\n    func get(index: Int) -> Int {\n        // Throw error if index out of bounds, same below\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        return arr[index]\n    }\n\n    /* Add elements at the end */\n    func set(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        arr[index] = num\n    }\n\n    /* Direct traversal of list elements */\n    func add(num: Int) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if size() == capacity() {\n            extendCapacity()\n        }\n        arr[size()] = num\n        // Update the number of elements\n        _size += 1\n    }\n\n    /* Sort list */\n    func insert(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if size() == capacity() {\n            extendCapacity()\n        }\n        // Move all elements after index index forward by one position\n        for j in (index ..< size()).reversed() {\n            arr[j + 1] = arr[j]\n        }\n        arr[index] = num\n        // Update the number of elements\n        _size += 1\n    }\n\n    /* Remove element */\n    @discardableResult\n    func remove(index: Int) -> Int {\n        if index < 0 || index >= size() {\n            fatalError(\"Index out of bounds\")\n        }\n        let num = arr[index]\n        // Move all elements after index forward by one position\n        for j in index ..< (size() - 1) {\n            arr[j] = arr[j + 1]\n        }\n        // Update the number of elements\n        _size -= 1\n        // Return the removed element\n        return num\n    }\n\n    /* Driver Code */\n    func extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1))\n        // Add elements at the end\n        _capacity = arr.count\n    }\n\n    /* Convert list to array */\n    func toArray() -> [Int] {\n        Array(arr.prefix(size()))\n    }\n}\n
my_list.js
/* List class */\nclass MyList {\n    #arr = new Array(); // Array (stores list elements)\n    #capacity = 10; // List capacity\n    #size = 0; // List length (current number of elements)\n    #extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    constructor() {\n        this.#arr = new Array(this.#capacity);\n    }\n\n    /* Get list length (current number of elements) */\n    size() {\n        return this.#size;\n    }\n\n    /* Get list capacity */\n    capacity() {\n        return this.#capacity;\n    }\n\n    /* Update element */\n    get(index) {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        return this.#arr[index];\n    }\n\n    /* Add elements at the end */\n    set(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        this.#arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    add(num) {\n        // If length equals capacity, need to expand\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Add new element to end of list\n        this.#arr[this.#size] = num;\n        this.#size++;\n    }\n\n    /* Sort list */\n    insert(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // Move all elements after index index forward by one position\n        for (let j = this.#size - 1; j >= index; j--) {\n            this.#arr[j + 1] = this.#arr[j];\n        }\n        // Update the number of elements\n        this.#arr[index] = num;\n        this.#size++;\n    }\n\n    /* Remove element */\n    remove(index) {\n        if (index < 0 || index >= this.#size) throw new Error('Index out of bounds');\n        let num = this.#arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for (let j = index; j < this.#size - 1; j++) {\n            this.#arr[j] = this.#arr[j + 1];\n        }\n        // Update the number of elements\n        this.#size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        this.#arr = this.#arr.concat(\n            new Array(this.capacity() * (this.#extendRatio - 1))\n        );\n        // Add elements at the end\n        this.#capacity = this.#arr.length;\n    }\n\n    /* Convert list to array */\n    toArray() {\n        let size = this.size();\n        // Elements enqueue\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
my_list.ts
/* List class */\nclass MyList {\n    private arr: Array<number>; // Array (stores list elements)\n    private _capacity: number = 10; // List capacity\n    private _size: number = 0; // List length (current number of elements)\n    private extendRatio: number = 2; // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    constructor() {\n        this.arr = new Array(this._capacity);\n    }\n\n    /* Get list length (current number of elements) */\n    public size(): number {\n        return this._size;\n    }\n\n    /* Get list capacity */\n    public capacity(): number {\n        return this._capacity;\n    }\n\n    /* Update element */\n    public get(index: number): number {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        return this.arr[index];\n    }\n\n    /* Add elements at the end */\n    public set(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        this.arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    public add(num: number): void {\n        // If length equals capacity, need to expand\n        if (this._size === this._capacity) this.extendCapacity();\n        // Add new element to end of list\n        this.arr[this._size] = num;\n        this._size++;\n    }\n\n    /* Sort list */\n    public insert(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (this._size === this._capacity) {\n            this.extendCapacity();\n        }\n        // Move all elements after index index forward by one position\n        for (let j = this._size - 1; j >= index; j--) {\n            this.arr[j + 1] = this.arr[j];\n        }\n        // Update the number of elements\n        this.arr[index] = num;\n        this._size++;\n    }\n\n    /* Remove element */\n    public remove(index: number): number {\n        if (index < 0 || index >= this._size) throw new Error('Index out of bounds');\n        let num = this.arr[index];\n        // Move all elements after index forward by one position\n        for (let j = index; j < this._size - 1; j++) {\n            this.arr[j] = this.arr[j + 1];\n        }\n        // Update the number of elements\n        this._size--;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    public extendCapacity(): void {\n        // Create new array of length size and copy original array to new array\n        this.arr = this.arr.concat(\n            new Array(this.capacity() * (this.extendRatio - 1))\n        );\n        // Add elements at the end\n        this._capacity = this.arr.length;\n    }\n\n    /* Convert list to array */\n    public toArray(): number[] {\n        let size = this.size();\n        // Elements enqueue\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
my_list.dart
/* List class */\nclass MyList {\n  late List<int> _arr; // Array (stores list elements)\n  int _capacity = 10; // List capacity\n  int _size = 0; // List length (current number of elements)\n  int _extendRatio = 2; // Multiple by which the list capacity is extended each time\n\n  /* Constructor */\n  MyList() {\n    _arr = List.filled(_capacity, 0);\n  }\n\n  /* Get list length (current number of elements) */\n  int size() => _size;\n\n  /* Get list capacity */\n  int capacity() => _capacity;\n\n  /* Update element */\n  int get(int index) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    return _arr[index];\n  }\n\n  /* Add elements at the end */\n  void set(int index, int _num) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    _arr[index] = _num;\n  }\n\n  /* Direct traversal of list elements */\n  void add(int _num) {\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (_size == _capacity) extendCapacity();\n    _arr[_size] = _num;\n    // Update the number of elements\n    _size++;\n  }\n\n  /* Sort list */\n  void insert(int index, int _num) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (_size == _capacity) extendCapacity();\n    // Move all elements after index index forward by one position\n    for (var j = _size - 1; j >= index; j--) {\n      _arr[j + 1] = _arr[j];\n    }\n    _arr[index] = _num;\n    // Update the number of elements\n    _size++;\n  }\n\n  /* Remove element */\n  int remove(int index) {\n    if (index >= _size) throw RangeError('Index out of bounds');\n    int _num = _arr[index];\n    // Move all elements after index forward by one position\n    for (var j = index; j < _size - 1; j++) {\n      _arr[j] = _arr[j + 1];\n    }\n    // Update the number of elements\n    _size--;\n    // Return the removed element\n    return _num;\n  }\n\n  /* Driver Code */\n  void extendCapacity() {\n    // Create new array with length _extendRatio times original array\n    final _newNums = List.filled(_capacity * _extendRatio, 0);\n    // Copy original array to new array\n    List.copyRange(_newNums, 0, _arr);\n    // Update _arr reference\n    _arr = _newNums;\n    // Add elements at the end\n    _capacity = _arr.length;\n  }\n\n  /* Convert list to array */\n  List<int> toArray() {\n    List<int> arr = [];\n    for (var i = 0; i < _size; i++) {\n      arr.add(get(i));\n    }\n    return arr;\n  }\n}\n
my_list.rs
/* List class */\n#[allow(dead_code)]\nstruct MyList {\n    arr: Vec<i32>,       // Array (stores list elements)\n    capacity: usize,     // List capacity\n    size: usize,         // List length (current number of elements)\n    extend_ratio: usize, // Multiple by which the list capacity is extended each time\n}\n\n#[allow(unused, unused_comparisons)]\nimpl MyList {\n    /* Constructor */\n    pub fn new(capacity: usize) -> Self {\n        let mut vec = vec![0; capacity];\n        Self {\n            arr: vec,\n            capacity,\n            size: 0,\n            extend_ratio: 2,\n        }\n    }\n\n    /* Get list length (current number of elements) */\n    pub fn size(&self) -> usize {\n        return self.size;\n    }\n\n    /* Get list capacity */\n    pub fn capacity(&self) -> usize {\n        return self.capacity;\n    }\n\n    /* Update element */\n    pub fn get(&self, index: usize) -> i32 {\n        // If the index is out of bounds, throw an exception, as below\n        if index >= self.size {\n            panic!(\"Index out of bounds\")\n        };\n        return self.arr[index];\n    }\n\n    /* Add elements at the end */\n    pub fn set(&mut self, index: usize, num: i32) {\n        if index >= self.size {\n            panic!(\"Index out of bounds\")\n        };\n        self.arr[index] = num;\n    }\n\n    /* Direct traversal of list elements */\n    pub fn add(&mut self, num: i32) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        self.arr[self.size] = num;\n        // Update the number of elements\n        self.size += 1;\n    }\n\n    /* Sort list */\n    pub fn insert(&mut self, index: usize, num: i32) {\n        if index >= self.size() {\n            panic!(\"Index out of bounds\")\n        };\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        // Move all elements after index index forward by one position\n        for j in (index..self.size).rev() {\n            self.arr[j + 1] = self.arr[j];\n        }\n        self.arr[index] = num;\n        // Update the number of elements\n        self.size += 1;\n    }\n\n    /* Remove element */\n    pub fn remove(&mut self, index: usize) -> i32 {\n        if index >= self.size() {\n            panic!(\"Index out of bounds\")\n        };\n        let num = self.arr[index];\n        // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array\n        for j in index..self.size - 1 {\n            self.arr[j] = self.arr[j + 1];\n        }\n        // Update the number of elements\n        self.size -= 1;\n        // Return the removed element\n        return num;\n    }\n\n    /* Driver Code */\n    pub fn extend_capacity(&mut self) {\n        // Create new array with length extend_ratio times original, copy original array to new array\n        let new_capacity = self.capacity * self.extend_ratio;\n        self.arr.resize(new_capacity, 0);\n        // Add elements at the end\n        self.capacity = new_capacity;\n    }\n\n    /* Convert list to array */\n    pub fn to_array(&self) -> Vec<i32> {\n        // Elements enqueue\n        let mut arr = Vec::new();\n        for i in 0..self.size {\n            arr.push(self.get(i));\n        }\n        arr\n    }\n}\n
my_list.c
/* List class */\ntypedef struct {\n    int *arr;        // Array (stores list elements)\n    int capacity;    // List capacity\n    int size;        // List size\n    int extendRatio; // List expansion multiplier\n} MyList;\n\n/* Constructor */\nMyList *newMyList() {\n    MyList *nums = malloc(sizeof(MyList));\n    nums->capacity = 10;\n    nums->arr = malloc(sizeof(int) * nums->capacity);\n    nums->size = 0;\n    nums->extendRatio = 2;\n    return nums;\n}\n\n/* Destructor */\nvoid delMyList(MyList *nums) {\n    free(nums->arr);\n    free(nums);\n}\n\n/* Get list length */\nint size(MyList *nums) {\n    return nums->size;\n}\n\n/* Get list capacity */\nint capacity(MyList *nums) {\n    return nums->capacity;\n}\n\n/* Update element */\nint get(MyList *nums, int index) {\n    assert(index >= 0 && index < nums->size);\n    return nums->arr[index];\n}\n\n/* Add elements at the end */\nvoid set(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < nums->size);\n    nums->arr[index] = num;\n}\n\n/* Direct traversal of list elements */\nvoid add(MyList *nums, int num) {\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Expand capacity\n    }\n    nums->arr[size(nums)] = num;\n    nums->size++;\n}\n\n/* Sort list */\nvoid insert(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < size(nums));\n    // When the number of elements exceeds capacity, trigger the extension mechanism\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // Expand capacity\n    }\n    for (int i = size(nums); i > index; --i) {\n        nums->arr[i] = nums->arr[i - 1];\n    }\n    nums->arr[index] = num;\n    nums->size++;\n}\n\n/* Remove element */\n// Note: stdio.h occupies the remove keyword\nint removeItem(MyList *nums, int index) {\n    assert(index >= 0 && index < size(nums));\n    int num = nums->arr[index];\n    for (int i = index; i < size(nums) - 1; i++) {\n        nums->arr[i] = nums->arr[i + 1];\n    }\n    nums->size--;\n    return num;\n}\n\n/* Driver Code */\nvoid extendCapacity(MyList *nums) {\n    // Allocate space first\n    int newCapacity = capacity(nums) * nums->extendRatio;\n    int *extend = (int *)malloc(sizeof(int) * newCapacity);\n    int *temp = nums->arr;\n\n    // Copy old data to new data\n    for (int i = 0; i < size(nums); i++)\n        extend[i] = nums->arr[i];\n\n    // Free old data\n    free(temp);\n\n    // Update new data\n    nums->arr = extend;\n    nums->capacity = newCapacity;\n}\n\n/* Convert list to Array for printing */\nint *toArray(MyList *nums) {\n    return nums->arr;\n}\n
my_list.kt
/* List class */\nclass MyList {\n    private var arr: IntArray = intArrayOf() // Array (stores list elements)\n    private var capacity: Int = 10 // List capacity\n    private var size: Int = 0 // List length (current number of elements)\n    private var extendRatio: Int = 2 // Multiple by which the list capacity is extended each time\n\n    /* Constructor */\n    init {\n        arr = IntArray(capacity)\n    }\n\n    /* Get list length (current number of elements) */\n    fun size(): Int {\n        return size\n    }\n\n    /* Get list capacity */\n    fun capacity(): Int {\n        return capacity\n    }\n\n    /* Update element */\n    fun get(index: Int): Int {\n        // If the index is out of bounds, throw an exception, as below\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        return arr[index]\n    }\n\n    /* Add elements at the end */\n    fun set(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        arr[index] = num\n    }\n\n    /* Direct traversal of list elements */\n    fun add(num: Int) {\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity()\n        arr[size] = num\n        // Update the number of elements\n        size++\n    }\n\n    /* Sort list */\n    fun insert(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        // When the number of elements exceeds capacity, trigger the extension mechanism\n        if (size == capacity())\n            extendCapacity()\n        // Move all elements after index index forward by one position\n        for (j in size - 1 downTo index)\n            arr[j + 1] = arr[j]\n        arr[index] = num\n        // Update the number of elements\n        size++\n    }\n\n    /* Remove element */\n    fun remove(index: Int): Int {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"Index out of bounds\")\n        val num = arr[index]\n        // Move all elements after index forward by one position\n        for (j in index..<size - 1)\n            arr[j] = arr[j + 1]\n        // Update the number of elements\n        size--\n        // Return the removed element\n        return num\n    }\n\n    /* Driver Code */\n    fun extendCapacity() {\n        // Create a new array with length extendRatio times the original array and copy the original array to the new array\n        arr = arr.copyOf(capacity() * extendRatio)\n        // Add elements at the end\n        capacity = arr.size\n    }\n\n    /* Convert list to array */\n    fun toArray(): IntArray {\n        val size = size()\n        // Elements enqueue\n        val arr = IntArray(size)\n        for (i in 0..<size) {\n            arr[i] = get(i)\n        }\n        return arr\n    }\n}\n
my_list.rb
### List class ###\nclass MyList\n  attr_reader :size       # Get list length (current number of elements)\n  attr_reader :capacity   # Get list capacity\n\n  ### Constructor ###\n  def initialize\n    @capacity = 10\n    @size = 0\n    @extend_ratio = 2\n    @arr = Array.new(capacity)\n  end\n\n  ### Access element ###\n  def get(index)\n    # If the index is out of bounds, throw an exception, as below\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    @arr[index]\n  end\n\n  ### Access element ###\n  def set(index, num)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    @arr[index] = num\n  end\n\n  ### Add element at end ###\n  def add(num)\n    # When the number of elements exceeds capacity, trigger the extension mechanism\n    extend_capacity if size == capacity\n    @arr[size] = num\n\n    # Update the number of elements\n    @size += 1\n  end\n\n  ### Insert element in middle ###\n  def insert(index, num)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n\n    # When the number of elements exceeds capacity, trigger the extension mechanism\n    extend_capacity if size == capacity\n\n    # Move all elements after index index forward by one position\n    for j in (size - 1).downto(index)\n      @arr[j + 1] = @arr[j]\n    end\n    @arr[index] = num\n\n    # Update the number of elements\n    @size += 1\n  end\n\n  ### Delete element ###\n  def remove(index)\n    raise IndexError, \"Index out of bounds\" if index < 0 || index >= size\n    num = @arr[index]\n\n    # Move all elements after index forward by one position\n    for j in index...size\n      @arr[j] = @arr[j + 1]\n    end\n\n    # Update the number of elements\n    @size -= 1\n\n    # Return the removed element\n    num\n  end\n\n  ### Expand list capacity ###\n  def extend_capacity\n    # Create new array with length extend_ratio times original, copy original array to new array\n    arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1))\n    # Add elements at the end\n    @capacity = arr.length\n  end\n\n  ### Convert list to array ###\n  def to_array\n    sz = size\n    # Elements enqueue\n    arr = Array.new(sz)\n    for i in 0...sz\n      arr[i] = get(i)\n    end\n    arr\n  end\nend\n
","path":["Chapter 4. Array and Linked List","4.3   List"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/","level":1,"title":"4.4   Random-Access Memory and Cache *","text":"

In the first two sections of this chapter, we explored arrays and linked lists, two fundamental and important data structures that represent \"contiguous storage\" and \"distributed storage\" as two physical structures, respectively.

In fact, physical structure largely determines the efficiency with which programs utilize memory and cache, which in turn affects the overall performance of algorithmic programs.

","path":["Chapter 4. Array and Linked List","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#441-computer-storage-devices","level":2,"title":"4.4.1   Computer Storage Devices","text":"

Computers include three types of storage devices: hard disk, random-access memory (RAM), and cache memory. The following table shows their different roles and performance characteristics in a computer system.

Table 4-2   Computer Storage Devices

Hard Disk RAM Cache Purpose Long-term storage of data, including operating systems, programs, and files Temporary storage of currently running programs and data being processed Storage of frequently accessed data and instructions to reduce CPU's accesses to memory Volatility Data is not lost after power-off Data is lost after power-off Data is lost after power-off Capacity Large, on the order of terabytes (TB) Small, on the order of gigabytes (GB) Very small, on the order of megabytes (MB) Speed Slow, hundreds to thousands of MB/s Fast, tens of GB/s Very fast, tens to hundreds of GB/s Cost (USD/GB) Inexpensive, fractions of a dollar to a few dollars per GB Expensive, tens to hundreds of dollars per GB Very expensive, priced as part of the CPU package

We can imagine the computer storage system as a pyramid structure as shown in the diagram below. Storage devices closer to the top of the pyramid are faster, have smaller capacity, and are more expensive. This multi-layered design is not by accident, but rather the result of careful consideration by computer scientists and engineers.

  • Hard disk cannot be easily replaced by RAM. First, data in memory is lost after power-off, making it unsuitable for long-term data storage. Second, memory is tens of times more expensive than hard disk, which makes it difficult to popularize in the consumer market.
  • Cache cannot simultaneously achieve large capacity and high speed. As the capacity of L1, L2, and L3 caches increases, their physical size becomes larger, and the physical distance between them and the CPU core increases, resulting in longer data transmission time and higher element access latency. With current technology, the multi-layered cache structure represents the best balance point between capacity, speed, and cost.

Figure 4-9   Computer Storage System

Tip

The storage hierarchy of computers embodies a delicate balance among speed, capacity, and cost. In fact, such trade-offs are common across all industrial fields, requiring us to find the optimal balance point between different advantages and constraints.

In summary, hard disk is used for long-term storage of large amounts of data, RAM is used for temporary storage of data being processed during program execution, and cache is used for storage of frequently accessed data and instructions, to improve program execution efficiency. The three work together to ensure efficient operation of the computer system.

As shown in the diagram below, during program execution, data is read from the hard disk into RAM for CPU computation. Cache can be viewed as part of the CPU, it intelligently loads data from RAM, providing the CPU with high-speed data reading, thereby significantly improving program execution efficiency and reducing reliance on slower RAM.

Figure 4-10   Data Flow Among Hard Disk, RAM, and Cache

","path":["Chapter 4. Array and Linked List","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#442-memory-efficiency-of-data-structures","level":2,"title":"4.4.2   Memory Efficiency of Data Structures","text":"

In terms of memory space utilization, arrays and linked lists each have advantages and limitations.

On one hand, memory is limited, and the same memory cannot be shared by multiple programs, so we hope data structures can utilize space as efficiently as possible. Array elements are tightly packed and do not require additional space to store references (pointers) between linked list nodes, thus having higher space efficiency. However, arrays need to allocate sufficient contiguous memory space at once, which may lead to memory waste, and array expansion requires additional time and space costs. In comparison, linked lists perform dynamic memory allocation and deallocation on a \"node\" basis, providing greater flexibility.

On the other hand, during program execution, as memory is repeatedly allocated and freed, the degree of fragmentation of free memory becomes increasingly severe, leading to reduced memory utilization efficiency. Arrays, due to their contiguous storage approach, are relatively less prone to memory fragmentation. Conversely, linked list elements are distributed in storage, and frequent insertion and deletion operations are more likely to cause memory fragmentation.

","path":["Chapter 4. Array and Linked List","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#443-cache-efficiency-of-data-structures","level":2,"title":"4.4.3   Cache Efficiency of Data Structures","text":"

Although cache has much smaller space capacity than memory, it is much faster than memory and plays a crucial role in program execution speed. Since cache capacity is limited and can only store a small portion of frequently accessed data, when the CPU attempts to access data that is not in the cache, a cache miss occurs, and the CPU must load the required data from the slower memory.

Clearly, the fewer \"cache misses,\" the higher the efficiency of CPU data reads and writes, and the better the program performance. We call the proportion of data that the CPU successfully obtains from the cache the cache hit rate, a metric typically used to measure cache efficiency.

To achieve the highest efficiency possible, cache employs the following data loading mechanisms.

  • Cache lines: The cache does not store and load data on a byte-by-byte basis, but rather as cache lines. Compared to byte-by-byte transmission, cache line transmission is more efficient.
  • Prefetching mechanism: The processor attempts to predict data access patterns (e.g., sequential access, fixed-stride jumping access, etc.) and loads data into the cache according to specific patterns, thereby improving hit rate.
  • Spatial locality: If a piece of data is accessed, nearby data may also be accessed in the near future. Therefore, when the cache loads a particular piece of data, it also loads nearby data to improve hit rate.
  • Temporal locality: If a piece of data is accessed, it is likely to be accessed again in the near future. Cache leverages this principle by retaining recently accessed data to improve hit rate.

In fact, arrays and linked lists have different efficiencies in utilizing cache, manifested in the following aspects.

  • Space occupied: Linked list elements occupy more space than array elements, resulting in fewer effective data in the cache.
  • Cache lines: Linked list data are scattered throughout memory, while cache loads \"by lines,\" so the proportion of invalid data loaded is higher.
  • Prefetching mechanism: Arrays have more \"predictable\" data access patterns than linked lists, making it easier for the system to guess which data will be loaded next.
  • Spatial locality: Arrays are stored in centralized memory space, so data near loaded data is more likely to be accessed soon.

Overall, arrays have higher cache hit rates, thus they usually outperform linked lists in operation efficiency. This makes data structures implemented based on arrays more popular when solving algorithmic problems.

It is important to note that high cache efficiency does not mean arrays are superior to linked lists in all cases. In practical applications, which data structure to choose should be determined based on specific requirements. For example, both arrays and linked lists can implement the \"stack\" data structure (which will be discussed in detail in the next chapter), but they are suitable for different scenarios.

  • When solving algorithm problems, we tend to prefer stack implementations based on arrays, because they provide higher operation efficiency and the ability of random access, at the cost of needing to pre-allocate a certain amount of memory space for the array.
  • If the data volume is very large, the dynamic nature is high, and the expected size of the stack is difficult to estimate, then a stack implementation based on linked lists is more suitable. Linked lists can distribute large amounts of data across different parts of memory and avoid the additional overhead produced by array expansion.
","path":["Chapter 4. Array and Linked List","4.4   Random-Access Memory and Cache *"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/","level":1,"title":"4.5   Summary","text":"","path":["Chapter 4. Array and Linked List","4.5   Summary"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Arrays and linked lists are two fundamental data structures, representing two different ways data can be stored in computer memory: contiguous memory storage and scattered memory storage. The characteristics of the two complement each other.
  • Arrays support random access and use less memory; however, inserting and deleting elements is inefficient, and the length is immutable after initialization.
  • Linked lists achieve efficient insertion and deletion of nodes by modifying references (pointers), and can flexibly adjust length; however, node access is inefficient and memory consumption is higher. Common linked list types include singly linked lists, circular linked lists, and doubly linked lists.
  • A list is an ordered collection of elements that supports insertion, deletion, search, and modification, typically implemented based on dynamic arrays. It retains the advantages of arrays while allowing flexible adjustment of length.
  • The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space.
  • During program execution, data is primarily stored in memory. Arrays provide higher memory space efficiency, while linked lists offer greater flexibility in memory usage.
  • Caches provide fast data access to the CPU through mechanisms such as cache lines, prefetching, and spatial and temporal locality, significantly improving program execution efficiency.
  • Because arrays have higher cache hit rates, they are generally more efficient than linked lists. When choosing a data structure, appropriate selection should be made based on specific requirements and scenarios.
","path":["Chapter 4. Array and Linked List","4.5   Summary"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Does storing an array on the stack versus on the heap affect time efficiency and space efficiency?

Arrays stored on the stack and on the heap are both stored in contiguous memory space, so data operation efficiency is basically the same. However, the stack and heap have their own characteristics, leading to the following differences.

  1. Allocation and deallocation efficiency: The stack is a relatively small piece of memory, with allocation automatically handled by the compiler; the heap is relatively larger and can be dynamically allocated in code, more prone to fragmentation. Therefore, allocation and deallocation operations on the heap are usually slower than on the stack.
  2. Size limitations: Stack memory is relatively small, and the heap size is generally limited by available memory. Therefore, the heap is more suitable for storing large arrays.
  3. Flexibility: The size of an array on the stack must be determined at compile time, while the size of an array on the heap can be determined dynamically at runtime.

Q: Why do arrays require elements of the same type, while linked lists do not emphasize this requirement?

Linked lists are composed of nodes, with nodes connected through references (pointers), and each node can store different types of data, such as int, double, string, object, etc.

In contrast, array elements must be of the same type, so that the corresponding element position can be obtained by calculating the offset. For example, if an array contains both int and long types, with individual elements occupying 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset, because the array contains two different \"element lengths\".

# Element Memory Address = Array Memory Address (first Element Memory address) + Element Length * Element Index\n

Q: After deleting node P, do we need to set P.next to None?

It is not necessary to modify P.next. From the perspective of the linked list, traversing from the head node to the tail node will no longer encounter P. This means that node P has been removed from the linked list, and it doesn't matter where node P points to at this time—it won't affect the linked list.

From a data structures and algorithms perspective (problem-solving), not disconnecting the pointer doesn't matter as long as the program logic is correct. From the perspective of standard libraries, disconnecting is safer and the logic is clearer. If not disconnected, assuming the deleted node is not properly reclaimed, it may affect the memory reclamation of its successor nodes.

Q: In a linked list, the time complexity of insertion and deletion operations is \\(O(1)\\). However, both insertion and deletion require \\(O(n)\\) time to find the element; why isn't the time complexity \\(O(n)\\)?

If the element is first found and then deleted, the time complexity is indeed \\(O(n)\\). However, the advantage of \\(O(1)\\) insertion and deletion in linked lists can be demonstrated in other applications. For example, a deque is well-suited for linked list implementation, where we maintain pointer variables always pointing to the head and tail nodes, with each insertion and deletion operation being \\(O(1)\\).

Q: In the diagram \"Linked List Definition and Storage Methods\", does the light blue pointer node occupy a single memory address, or does it share equally with the node value?

This diagram is a qualitative representation; a quantitative representation requires analysis based on the specific situation.

  • Different types of node values occupy different amounts of space, such as int, long, double, and instance objects, etc.
  • The amount of memory space occupied by pointer variables depends on the operating system and compilation environment used, usually 8 bytes or 4 bytes.

Q: Is appending an element at the end of a list always \\(O(1)\\)?

If appending an element exceeds the list length, the list must first be expanded before adding. The system allocates a new block of memory and moves all elements from the original list to it, in which case the time complexity becomes \\(O(n)\\).

Q: \"The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space\"—does this space waste refer to the memory occupied by additional variables such as capacity, length, and expansion factor?

This space waste mainly has two aspects: on one hand, lists typically set an initial length, which we may not need to fully utilize; on the other hand, to prevent frequent expansion, expansion generally multiplies by a coefficient, such as \\(\\times 1.5\\). As a result, there will be many empty positions that we typically cannot completely fill.

Q: In Python, after initializing n = [1, 2, 3], the addresses of these 3 elements are contiguous, but initializing m = [2, 1, 3] reveals that each element's id is not continuous; rather, they are the same as those in n. Since the addresses of these elements are not contiguous, is m still an array?

If we replace list elements with linked list nodes n = [n1, n2, n3, n4, n5], usually these 5 node objects are also scattered throughout memory. However, given a list index, we can still obtain the node memory address in \\(O(1)\\) time, thereby accessing the corresponding node. This is because the array stores references to nodes, not the nodes themselves.

Unlike many languages, numbers in Python are wrapped as objects, and lists store not the numbers themselves, but references to the numbers. Therefore, we find that the same numbers in two arrays have the same id, and the memory addresses of these numbers need not be contiguous.

Q: C++ STL has std::list which has already implemented a doubly linked list, but it seems that some algorithm books don't use it directly. Is there a limitation?

On one hand, we often prefer to use arrays for implementing algorithms and only use linked lists when necessary, mainly for two reasons.

  • Space overhead: Since each element requires two additional pointers (one for the previous element and one for the next element), std::list typically consumes more space than std::vector.
  • Cache unfriendliness: Since data is not stored contiguously, std::list has lower cache utilization. In general, std::vector has better performance.

On the other hand, cases where linked lists are necessary mainly involve binary trees and graphs. Stacks and queues usually use the stack and queue provided by the programming language, rather than linked lists.

Q: Does the operation res = [[0]] * n create a 2D list where each [0] is independent?

No, they are not independent. In this 2D list, all the [0] are actually references to the same object. If we modify one element, we will find that all corresponding elements change accordingly.

If we want each [0] in the 2D list to be independent, we can use res = [[0] for _ in range(n)] to achieve this. The principle of this approach is to initialize \\(n\\) independent [0] list objects.

Q: Does the operation res = [0] * n create a list where each integer 0 is independent?

In this list, all integer 0s are references to the same object. This is because Python uses a caching mechanism for small integers (typically -5 to 256) to maximize object reuse and improve performance.

Although they point to the same object, we can still independently modify each element in the list. This is because Python integers are \"immutable objects\". When we modify an element, we are actually switching to a reference of another object, rather than changing the original object itself.

However, when list elements are \"mutable objects\" (such as lists, dictionaries, or class instances), modifying an element directly changes the object itself, and all elements referencing that object will have the same change.

","path":["Chapter 4. Array and Linked List","4.5   Summary"],"tags":[]},{"location":"chapter_backtracking/","level":1,"title":"Chapter 13.   Backtracking","text":"

Abstract

We are like explorers in a maze, and may encounter difficulties on the path forward.

The power of backtracking allows us to start over, keep trying, and eventually find the exit leading to light.

","path":["Chapter 13. Backtracking","Chapter 13.   Backtracking"],"tags":[]},{"location":"chapter_backtracking/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 13.1   Backtracking Algorithm
  • 13.2   Permutations Problem
  • 13.3   Subset-Sum Problem
  • 13.4   N-Queens Problem
  • 13.5   Summary
","path":["Chapter 13. Backtracking","Chapter 13.   Backtracking"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   Backtracking Algorithm","text":"

The backtracking algorithm is a method for solving problems through exhaustive search. Its core idea is to start from an initial state and exhaustively search all possible solutions. When a correct solution is found, it is recorded. This process continues until a solution is found or all possible choices have been tried without finding a solution.

The backtracking algorithm typically employs \"depth-first search\" to traverse the solution space. In the \"Binary Tree\" chapter, we mentioned that preorder, inorder, and postorder traversals all belong to depth-first search. Next, we will construct a backtracking problem using preorder traversal to progressively understand how the backtracking algorithm works.

Example 1

Given a binary tree, search and record all nodes with value \\(7\\), and return a list of these nodes.

For this problem, we perform a preorder traversal of the tree and check whether the current node's value is \\(7\\). If it is, we add the node to the result list res. The relevant implementation is shown in the following figure and code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 1\"\"\"\n    if root is None:\n        return\n    if root.val == 7:\n        # Record solution\n        res.append(root)\n    pre_order(root.left)\n    pre_order(root.right)\n
preorder_traversal_i_compact.cpp
/* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(root);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
preorder_traversal_i_compact.java
/* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Record solution\n        res.add(root);\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n}\n
preorder_traversal_i_compact.cs
/* Preorder traversal: Example 1 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // Record solution\n        res.Add(root);\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n
preorder_traversal_i_compact.go
/* Preorder traversal: Example 1 */\nfunc preOrderI(root *TreeNode, res *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    if (root.Val).(int) == 7 {\n        // Record solution\n        *res = append(*res, root)\n    }\n    preOrderI(root.Left, res)\n    preOrderI(root.Right, res)\n}\n
preorder_traversal_i_compact.swift
/* Preorder traversal: Example 1 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    if root.val == 7 {\n        // Record solution\n        res.append(root)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n
preorder_traversal_i_compact.js
/* Preorder traversal: Example 1 */\nfunction preOrder(root, res) {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Record solution\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.ts
/* Preorder traversal: Example 1 */\nfunction preOrder(root: TreeNode | null, res: TreeNode[]): void {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // Record solution\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.dart
/* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode? root, List<TreeNode> res) {\n  if (root == null) {\n    return;\n  }\n  if (root.val == 7) {\n    // Record solution\n    res.add(root);\n  }\n  preOrder(root.left, res);\n  preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.rs
/* Preorder traversal: Example 1 */\nfn pre_order(res: &mut Vec<Rc<RefCell<TreeNode>>>, root: Option<&Rc<RefCell<TreeNode>>>) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        if node.borrow().val == 7 {\n            // Record solution\n            res.push(node.clone());\n        }\n        pre_order(res, node.borrow().left.as_ref());\n        pre_order(res, node.borrow().right.as_ref());\n    }\n}\n
preorder_traversal_i_compact.c
/* Preorder traversal: Example 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    if (root->val == 7) {\n        // Record solution\n        res[resSize++] = root;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
preorder_traversal_i_compact.kt
/* Preorder traversal: Example 1 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(root)\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n}\n
preorder_traversal_i_compact.rb
### Pre-order traversal: example 1 ###\ndef pre_order(root)\n  return unless root\n\n  # Record solution\n  $res << root if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\nend\n

Figure 13-1   Search for nodes in preorder traversal

","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1311-attempt-and-backtrack","level":2,"title":"13.1.1   Attempt and Backtrack","text":"

The reason it is called a backtracking algorithm is that it employs \"attempt\" and \"backtrack\" strategies when searching the solution space. When the algorithm encounters a state where it cannot continue forward or cannot find a solution that satisfies the constraints, it will undo the previous choice, return to a previous state, and try other possible choices.

For Example 1, visiting each node represents an \"attempt\", while skipping over a leaf node or a function return from the parent node represents a \"backtrack\".

It is worth noting that backtracking is not limited to function returns alone. To illustrate this, let's extend Example 1 slightly.

Example 2

In a binary tree, search all nodes with value \\(7\\), and return the paths from the root node to these nodes.

Based on the code from Example 1, we need to use a list path to record the visited node path. When we reach a node with value \\(7\\), we copy path and add it to the result list res. After traversal is complete, res contains all the solutions. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 2\"\"\"\n    if root is None:\n        return\n    # Attempt\n    path.append(root)\n    if root.val == 7:\n        # Record solution\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Backtrack\n    path.pop()\n
preorder_traversal_ii_compact.cpp
/* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    // Attempt\n    path.push_back(root);\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    path.pop_back();\n}\n
preorder_traversal_ii_compact.java
/* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    // Attempt\n    path.add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Backtrack\n    path.remove(path.size() - 1);\n}\n
preorder_traversal_ii_compact.cs
/* Preorder traversal: Example 2 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    // Attempt\n    path.Add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Backtrack\n    path.RemoveAt(path.Count - 1);\n}\n
preorder_traversal_ii_compact.go
/* Preorder traversal: Example 2 */\nfunc preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    // Attempt\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Record solution\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderII(root.Left, res, path)\n    preOrderII(root.Right, res, path)\n    // Backtrack\n    *path = (*path)[:len(*path)-1]\n}\n
preorder_traversal_ii_compact.swift
/* Preorder traversal: Example 2 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Attempt\n    path.append(root)\n    if root.val == 7 {\n        // Record solution\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Backtrack\n    path.removeLast()\n}\n
preorder_traversal_ii_compact.js
/* Preorder traversal: Example 2 */\nfunction preOrder(root, path, res) {\n    if (root === null) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
preorder_traversal_ii_compact.ts
/* Preorder traversal: Example 2 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    if (root === null) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
preorder_traversal_ii_compact.dart
/* Preorder traversal: Example 2 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null) {\n    return;\n  }\n\n  // Attempt\n  path.add(root);\n  if (root.val == 7) {\n    // Record solution\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Backtrack\n  path.removeLast();\n}\n
preorder_traversal_ii_compact.rs
/* Preorder traversal: Example 2 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        // Attempt\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Record solution\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // Backtrack\n        path.pop();\n    }\n}\n
preorder_traversal_ii_compact.c
/* Preorder traversal: Example 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    // Attempt\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Record solution\n        for (int i = 0; i < pathSize; ++i) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    pathSize--;\n}\n
preorder_traversal_ii_compact.kt
/* Preorder traversal: Example 2 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    // Attempt\n    path!!.add(root)\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Backtrack\n    path!!.removeAt(path!!.size - 1)\n}\n
preorder_traversal_ii_compact.rb
### Pre-order traversal: example 2 ###\ndef pre_order(root)\n  return unless root\n\n  # Attempt\n  $path << root\n\n  # Record solution\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Backtrack\n  $path.pop\nend\n

In each \"attempt\", we record the path by adding the current node to path; before \"backtracking\", we need to remove the node from path, to restore the state before this attempt.

Observing the process shown in the following figure, we can understand attempt and backtrack as \"advance\" and \"undo\", two operations that are the reverse of each other.

<1><2><3><4><5><6><7><8><9><10><11>

Figure 13-2   Attempt and backtrack

","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1312-pruning","level":2,"title":"13.1.2   Pruning","text":"

Complex backtracking problems usually contain one or more constraints. Constraints can typically be used for \"pruning\".

Example 3

In a binary tree, search all nodes with value \\(7\\) and return the paths from the root node to these nodes, but require that the paths do not contain nodes with value \\(3\\).

To satisfy the above constraints, we need to add pruning operations: during the search process, if we encounter a node with value \\(3\\), we return early and do not continue searching. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
def pre_order(root: TreeNode):\n    \"\"\"Preorder traversal: Example 3\"\"\"\n    # Pruning\n    if root is None or root.val == 3:\n        return\n    # Attempt\n    path.append(root)\n    if root.val == 7:\n        # Record solution\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # Backtrack\n    path.pop()\n
preorder_traversal_iii_compact.cpp
/* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode *root) {\n    // Pruning\n    if (root == nullptr || root->val == 3) {\n        return;\n    }\n    // Attempt\n    path.push_back(root);\n    if (root->val == 7) {\n        // Record solution\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    path.pop_back();\n}\n
preorder_traversal_iii_compact.java
/* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode root) {\n    // Pruning\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Attempt\n    path.add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // Backtrack\n    path.remove(path.size() - 1);\n}\n
preorder_traversal_iii_compact.cs
/* Preorder traversal: Example 3 */\nvoid PreOrder(TreeNode? root) {\n    // Pruning\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // Attempt\n    path.Add(root);\n    if (root.val == 7) {\n        // Record solution\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // Backtrack\n    path.RemoveAt(path.Count - 1);\n}\n
preorder_traversal_iii_compact.go
/* Preorder traversal: Example 3 */\nfunc preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    // Pruning\n    if root == nil || root.Val == 3 {\n        return\n    }\n    // Attempt\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // Record solution\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderIII(root.Left, res, path)\n    preOrderIII(root.Right, res, path)\n    // Backtrack\n    *path = (*path)[:len(*path)-1]\n}\n
preorder_traversal_iii_compact.swift
/* Preorder traversal: Example 3 */\nfunc preOrder(root: TreeNode?) {\n    // Pruning\n    guard let root = root, root.val != 3 else {\n        return\n    }\n    // Attempt\n    path.append(root)\n    if root.val == 7 {\n        // Record solution\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // Backtrack\n    path.removeLast()\n}\n
preorder_traversal_iii_compact.js
/* Preorder traversal: Example 3 */\nfunction preOrder(root, path, res) {\n    // Pruning\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
preorder_traversal_iii_compact.ts
/* Preorder traversal: Example 3 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Pruning\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // Attempt\n    path.push(root);\n    if (root.val === 7) {\n        // Record solution\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // Backtrack\n    path.pop();\n}\n
preorder_traversal_iii_compact.dart
/* Preorder traversal: Example 3 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null || root.val == 3) {\n    return;\n  }\n\n  // Attempt\n  path.add(root);\n  if (root.val == 7) {\n    // Record solution\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // Backtrack\n  path.removeLast();\n}\n
preorder_traversal_iii_compact.rs
/* Preorder traversal: Example 3 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    // Pruning\n    if root.is_none() || root.as_ref().unwrap().borrow().val == 3 {\n        return;\n    }\n    if let Some(node) = root {\n        // Attempt\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // Record solution\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // Backtrack\n        path.pop();\n    }\n}\n
preorder_traversal_iii_compact.c
/* Preorder traversal: Example 3 */\nvoid preOrder(TreeNode *root) {\n    // Pruning\n    if (root == NULL || root->val == 3) {\n        return;\n    }\n    // Attempt\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // Record solution\n        for (int i = 0; i < pathSize; i++) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // Backtrack\n    pathSize--;\n}\n
preorder_traversal_iii_compact.kt
/* Preorder traversal: Example 3 */\nfun preOrder(root: TreeNode?) {\n    // Pruning\n    if (root == null || root._val == 3) {\n        return\n    }\n    // Attempt\n    path!!.add(root)\n    if (root._val == 7) {\n        // Record solution\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // Backtrack\n    path!!.removeAt(path!!.size - 1)\n}\n
preorder_traversal_iii_compact.rb
### Pre-order traversal: example 3 ###\ndef pre_order(root)\n  # Pruning\n  return if !root || root.val == 3\n\n  # Attempt\n  $path.append(root)\n\n  # Record solution\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # Backtrack\n  $path.pop\nend\n

\"Pruning\" is a vivid term. As shown in the following figure, during the search process, we \"prune\" search branches that do not satisfy the constraints, avoiding many meaningless attempts and thus improving search efficiency.

Figure 13-3   Pruning according to constraints

","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1313-framework-code","level":2,"title":"13.1.3   Framework Code","text":"

Next, we attempt to extract the main framework of backtracking's \"attempt, backtrack, and pruning\", to improve code generality.

In the following framework code, state represents the current state of the problem, and choices represents the choices available in the current state:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def backtrack(state: State, choices: list[choice], res: list[state]):\n    \"\"\"Backtracking algorithm framework\"\"\"\n    # Check if it is a solution\n    if is_solution(state):\n        # Record the solution\n        record_solution(state, res)\n        # Stop searching\n        return\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: check if the choice is valid\n        if is_valid(state, choice):\n            # Attempt: make a choice and update the state\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice)\n
/* Backtracking algorithm framework */\nvoid backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (Choice choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nvoid backtrack(State state, List<Choice> choices, List<State> res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (Choice choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nvoid Backtrack(State state, List<Choice> choices, List<State> res) {\n    // Check if it is a solution\n    if (IsSolution(state)) {\n        // Record the solution\n        RecordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    foreach (Choice choice in choices) {\n        // Pruning: check if the choice is valid\n        if (IsValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            MakeChoice(state, choice);\n            Backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            UndoChoice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nfunc backtrack(state *State, choices []Choice, res *[]State) {\n    // Check if it is a solution\n    if isSolution(state) {\n        // Record the solution\n        recordSolution(state, res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for _, choice := range choices {\n        // Pruning: check if the choice is valid\n        if isValid(state, choice) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nfunc backtrack(state: inout State, choices: [Choice], res: inout [State]) {\n    // Check if it is a solution\n    if isSolution(state: state) {\n        // Record the solution\n        recordSolution(state: state, res: &res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if isValid(state: state, choice: choice) {\n            // Attempt: make a choice and update the state\n            makeChoice(state: &state, choice: choice)\n            backtrack(state: &state, choices: choices, res: &res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nfunction backtrack(state, choices, res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (let choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nfunction backtrack(state: State, choices: Choice[], res: State[]): void {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (let choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nvoid backtrack(State state, List<Choice>, List<State> res) {\n  // Check if it is a solution\n  if (isSolution(state)) {\n    // Record the solution\n    recordSolution(state, res);\n    // Stop searching\n    return;\n  }\n  // Traverse all choices\n  for (Choice choice in choices) {\n    // Pruning: check if the choice is valid\n    if (isValid(state, choice)) {\n      // Attempt: make a choice and update the state\n      makeChoice(state, choice);\n      backtrack(state, choices, res);\n      // Backtrack: undo the choice and restore to the previous state\n      undoChoice(state, choice);\n    }\n  }\n}\n
/* Backtracking algorithm framework */\nfn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {\n    // Check if it is a solution\n    if is_solution(state) {\n        // Record the solution\n        record_solution(state, res);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if is_valid(state, choice) {\n            // Attempt: make a choice and update the state\n            make_choice(state, choice);\n            backtrack(state, choices, res);\n            // Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nvoid backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res, numRes);\n        // Stop searching\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < numChoices; i++) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, &choices[i])) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, &choices[i]);\n            backtrack(state, choices, numChoices, res, numRes);\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, &choices[i]);\n        }\n    }\n}\n
/* Backtracking algorithm framework */\nfun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record the solution\n        recordSolution(state, res)\n        // Stop searching\n        return\n    }\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make a choice and update the state\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // Backtrack: undo the choice and restore to the previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
### Backtracking algorithm framework ###\ndef backtrack(state, choices, res)\n    # Check if it is a solution\n    if is_solution?(state)\n        # Record the solution\n        record_solution(state, res)\n        return\n    end\n\n    # Traverse all choices\n    for choice in choices\n        # Pruning: check if the choice is valid\n        if is_valid?(state, choice)\n            # Attempt: make a choice and update the state\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # Backtrack: undo the choice and restore to the previous state\n            undo_choice(state, choice)\n        end\n    end\nend\n

Next, we solve Example 3 based on the framework code. The state state is the node traversal path, the choices choices are the left and right child nodes of the current node, and the result res is a list of paths:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_template.py
def is_solution(state: list[TreeNode]) -> bool:\n    \"\"\"Check if the current state is a solution\"\"\"\n    return state and state[-1].val == 7\n\ndef record_solution(state: list[TreeNode], res: list[list[TreeNode]]):\n    \"\"\"Record solution\"\"\"\n    res.append(list(state))\n\ndef is_valid(state: list[TreeNode], choice: TreeNode) -> bool:\n    \"\"\"Check if the choice is valid under the current state\"\"\"\n    return choice is not None and choice.val != 3\n\ndef make_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Update state\"\"\"\n    state.append(choice)\n\ndef undo_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"Restore state\"\"\"\n    state.pop()\n\ndef backtrack(\n    state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]]\n):\n    \"\"\"Backtracking algorithm: Example 3\"\"\"\n    # Check if it is a solution\n    if is_solution(state):\n        # Record solution\n        record_solution(state, res)\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: check if the choice is valid\n        if is_valid(state, choice):\n            # Attempt: make choice, update state\n            make_choice(state, choice)\n            # Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res)\n            # Backtrack: undo choice, restore to previous state\n            undo_choice(state, choice)\n
preorder_traversal_iii_template.cpp
/* Check if the current state is a solution */\nbool isSolution(vector<TreeNode *> &state) {\n    return !state.empty() && state.back()->val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(vector<TreeNode *> &state, vector<vector<TreeNode *>> &res) {\n    res.push_back(state);\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(vector<TreeNode *> &state, TreeNode *choice) {\n    return choice != nullptr && choice->val != 3;\n}\n\n/* Update state */\nvoid makeChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.push_back(choice);\n}\n\n/* Restore state */\nvoid undoChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.pop_back();\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(vector<TreeNode *> &state, vector<TreeNode *> &choices, vector<vector<TreeNode *>> &res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (TreeNode *choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            vector<TreeNode *> nextChoices{choice->left, choice->right};\n            backtrack(state, nextChoices, res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.java
/* Check if the current state is a solution */\nboolean isSolution(List<TreeNode> state) {\n    return !state.isEmpty() && state.get(state.size() - 1).val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.add(new ArrayList<>(state));\n}\n\n/* Check if the choice is valid under the current state */\nboolean isValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid makeChoice(List<TreeNode> state, TreeNode choice) {\n    state.add(choice);\n}\n\n/* Restore state */\nvoid undoChoice(List<TreeNode> state, TreeNode choice) {\n    state.remove(state.size() - 1);\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (TreeNode choice : choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, Arrays.asList(choice.left, choice.right), res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.cs
/* Check if the current state is a solution */\nbool IsSolution(List<TreeNode> state) {\n    return state.Count != 0 && state[^1].val == 7;\n}\n\n/* Record solution */\nvoid RecordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.Add(new List<TreeNode>(state));\n}\n\n/* Check if the choice is valid under the current state */\nbool IsValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid MakeChoice(List<TreeNode> state, TreeNode choice) {\n    state.Add(choice);\n}\n\n/* Restore state */\nvoid UndoChoice(List<TreeNode> state, TreeNode choice) {\n    state.RemoveAt(state.Count - 1);\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid Backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // Check if it is a solution\n    if (IsSolution(state)) {\n        // Record solution\n        RecordSolution(state, res);\n    }\n    // Traverse all choices\n    foreach (TreeNode choice in choices) {\n        // Pruning: check if the choice is valid\n        if (IsValid(state, choice)) {\n            // Attempt: make choice, update state\n            MakeChoice(state, choice);\n            // Proceed to the next round of selection\n            Backtrack(state, [choice.left!, choice.right!], res);\n            // Backtrack: undo choice, restore to previous state\n            UndoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.go
/* Check if the current state is a solution */\nfunc isSolution(state *[]*TreeNode) bool {\n    return len(*state) != 0 && (*state)[len(*state)-1].Val == 7\n}\n\n/* Record solution */\nfunc recordSolution(state *[]*TreeNode, res *[][]*TreeNode) {\n    *res = append(*res, append([]*TreeNode{}, *state...))\n}\n\n/* Check if the choice is valid under the current state */\nfunc isValid(state *[]*TreeNode, choice *TreeNode) bool {\n    return choice != nil && choice.Val != 3\n}\n\n/* Update state */\nfunc makeChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = append(*state, choice)\n}\n\n/* Restore state */\nfunc undoChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = (*state)[:len(*state)-1]\n}\n\n/* Backtracking algorithm: Example 3 */\nfunc backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) {\n    // Check if it is a solution\n    if isSolution(state) {\n        // Record solution\n        recordSolution(state, res)\n    }\n    // Traverse all choices\n    for _, choice := range *choices {\n        // Pruning: check if the choice is valid\n        if isValid(state, choice) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice)\n            // Proceed to the next round of selection\n            temp := make([]*TreeNode, 0)\n            temp = append(temp, choice.Left, choice.Right)\n            backtrackIII(state, &temp, res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.swift
/* Check if the current state is a solution */\nfunc isSolution(state: [TreeNode]) -> Bool {\n    !state.isEmpty && state.last!.val == 7\n}\n\n/* Record solution */\nfunc recordSolution(state: [TreeNode], res: inout [[TreeNode]]) {\n    res.append(state)\n}\n\n/* Check if the choice is valid under the current state */\nfunc isValid(state: [TreeNode], choice: TreeNode?) -> Bool {\n    choice != nil && choice!.val != 3\n}\n\n/* Update state */\nfunc makeChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.append(choice)\n}\n\n/* Restore state */\nfunc undoChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.removeLast()\n}\n\n/* Backtracking algorithm: Example 3 */\nfunc backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) {\n    // Check if it is a solution\n    if isSolution(state: state) {\n        recordSolution(state: state, res: &res)\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: check if the choice is valid\n        if isValid(state: state, choice: choice) {\n            // Attempt: make choice, update state\n            makeChoice(state: &state, choice: choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.js
/* Check if the current state is a solution */\nfunction isSolution(state) {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Record solution */\nfunction recordSolution(state, res) {\n    res.push([...state]);\n}\n\n/* Check if the choice is valid under the current state */\nfunction isValid(state, choice) {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Update state */\nfunction makeChoice(state, choice) {\n    state.push(choice);\n}\n\n/* Restore state */\nfunction undoChoice(state) {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 3 */\nfunction backtrack(state, choices, res) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state);\n        }\n    }\n}\n
preorder_traversal_iii_template.ts
/* Check if the current state is a solution */\nfunction isSolution(state: TreeNode[]): boolean {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* Record solution */\nfunction recordSolution(state: TreeNode[], res: TreeNode[][]): void {\n    res.push([...state]);\n}\n\n/* Check if the choice is valid under the current state */\nfunction isValid(state: TreeNode[], choice: TreeNode): boolean {\n    return choice !== null && choice.val !== 3;\n}\n\n/* Update state */\nfunction makeChoice(state: TreeNode[], choice: TreeNode): void {\n    state.push(choice);\n}\n\n/* Restore state */\nfunction undoChoice(state: TreeNode[]): void {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 3 */\nfunction backtrack(\n    state: TreeNode[],\n    choices: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res);\n    }\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice);\n            // Proceed to the next round of selection\n            backtrack(state, [choice.left, choice.right], res);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state);\n        }\n    }\n}\n
preorder_traversal_iii_template.dart
/* Check if the current state is a solution */\nbool isSolution(List<TreeNode> state) {\n  return state.isNotEmpty && state.last.val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n  res.add(List.from(state));\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(List<TreeNode> state, TreeNode? choice) {\n  return choice != null && choice.val != 3;\n}\n\n/* Update state */\nvoid makeChoice(List<TreeNode> state, TreeNode? choice) {\n  state.add(choice!);\n}\n\n/* Restore state */\nvoid undoChoice(List<TreeNode> state, TreeNode? choice) {\n  state.removeLast();\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(\n  List<TreeNode> state,\n  List<TreeNode?> choices,\n  List<List<TreeNode>> res,\n) {\n  // Check if it is a solution\n  if (isSolution(state)) {\n    // Record solution\n    recordSolution(state, res);\n  }\n  // Traverse all choices\n  for (TreeNode? choice in choices) {\n    // Pruning: check if the choice is valid\n    if (isValid(state, choice)) {\n      // Attempt: make choice, update state\n      makeChoice(state, choice);\n      // Proceed to the next round of selection\n      backtrack(state, [choice!.left, choice.right], res);\n      // Backtrack: undo choice, restore to previous state\n      undoChoice(state, choice);\n    }\n  }\n}\n
preorder_traversal_iii_template.rs
/* Check if the current state is a solution */\nfn is_solution(state: &mut Vec<Rc<RefCell<TreeNode>>>) -> bool {\n    return !state.is_empty() && state.last().unwrap().borrow().val == 7;\n}\n\n/* Record solution */\nfn record_solution(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    res.push(state.clone());\n}\n\n/* Check if the choice is valid under the current state */\nfn is_valid(_: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Option<&Rc<RefCell<TreeNode>>>) -> bool {\n    return choice.is_some() && choice.unwrap().borrow().val != 3;\n}\n\n/* Update state */\nfn make_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Rc<RefCell<TreeNode>>) {\n    state.push(choice);\n}\n\n/* Restore state */\nfn undo_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, _: Rc<RefCell<TreeNode>>) {\n    state.pop();\n}\n\n/* Backtracking algorithm: Example 3 */\nfn backtrack(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    choices: &Vec<Option<&Rc<RefCell<TreeNode>>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    // Check if it is a solution\n    if is_solution(state) {\n        // Record solution\n        record_solution(state, res);\n    }\n    // Traverse all choices\n    for &choice in choices.iter() {\n        // Pruning: check if the choice is valid\n        if is_valid(state, choice) {\n            // Attempt: make choice, update state\n            make_choice(state, choice.unwrap().clone());\n            // Proceed to the next round of selection\n            backtrack(\n                state,\n                &vec![\n                    choice.unwrap().borrow().left.as_ref(),\n                    choice.unwrap().borrow().right.as_ref(),\n                ],\n                res,\n            );\n            // Backtrack: undo choice, restore to previous state\n            undo_choice(state, choice.unwrap().clone());\n        }\n    }\n}\n
preorder_traversal_iii_template.c
/* Check if the current state is a solution */\nbool isSolution(void) {\n    return pathSize > 0 && path[pathSize - 1]->val == 7;\n}\n\n/* Record solution */\nvoid recordSolution(void) {\n    for (int i = 0; i < pathSize; i++) {\n        res[resSize][i] = path[i];\n    }\n    resSize++;\n}\n\n/* Check if the choice is valid under the current state */\nbool isValid(TreeNode *choice) {\n    return choice != NULL && choice->val != 3;\n}\n\n/* Update state */\nvoid makeChoice(TreeNode *choice) {\n    path[pathSize++] = choice;\n}\n\n/* Restore state */\nvoid undoChoice(void) {\n    pathSize--;\n}\n\n/* Backtracking algorithm: Example 3 */\nvoid backtrack(TreeNode *choices[2]) {\n    // Check if it is a solution\n    if (isSolution()) {\n        // Record solution\n        recordSolution();\n    }\n    // Traverse all choices\n    for (int i = 0; i < 2; i++) {\n        TreeNode *choice = choices[i];\n        // Pruning: check if the choice is valid\n        if (isValid(choice)) {\n            // Attempt: make choice, update state\n            makeChoice(choice);\n            // Proceed to the next round of selection\n            TreeNode *nextChoices[2] = {choice->left, choice->right};\n            backtrack(nextChoices);\n            // Backtrack: undo choice, restore to previous state\n            undoChoice();\n        }\n    }\n}\n
preorder_traversal_iii_template.kt
/* Check if the current state is a solution */\nfun isSolution(state: MutableList<TreeNode?>): Boolean {\n    return state.isNotEmpty() && state[state.size - 1]?._val == 7\n}\n\n/* Record solution */\nfun recordSolution(state: MutableList<TreeNode?>?, res: MutableList<MutableList<TreeNode?>?>) {\n    res.add(state!!.toMutableList())\n}\n\n/* Check if the choice is valid under the current state */\nfun isValid(state: MutableList<TreeNode?>?, choice: TreeNode?): Boolean {\n    return choice != null && choice._val != 3\n}\n\n/* Update state */\nfun makeChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.add(choice)\n}\n\n/* Restore state */\nfun undoChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.removeLast()\n}\n\n/* Backtracking algorithm: Example 3 */\nfun backtrack(\n    state: MutableList<TreeNode?>,\n    choices: MutableList<TreeNode?>,\n    res: MutableList<MutableList<TreeNode?>?>\n) {\n    // Check if it is a solution\n    if (isSolution(state)) {\n        // Record solution\n        recordSolution(state, res)\n    }\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: check if the choice is valid\n        if (isValid(state, choice)) {\n            // Attempt: make choice, update state\n            makeChoice(state, choice)\n            // Proceed to the next round of selection\n            backtrack(state, mutableListOf(choice!!.left, choice.right), res)\n            // Backtrack: undo choice, restore to previous state\n            undoChoice(state, choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.rb
### Check if current state is solution ###\ndef is_solution?(state)\n  !state.empty? && state.last.val == 7\nend\n\n### Record solution ###\ndef record_solution(state, res)\n  res << state.dup\nend\n\n### Check if choice is valid in current state ###\ndef is_valid?(state, choice)\n  choice && choice.val != 3\nend\n\n### Update state ###\ndef make_choice(state, choice)\n  state << choice\nend\n\n### Restore state ###\ndef undo_choice(state, choice)\n  state.pop\nend\n\n### Backtracking: example 3 ###\ndef backtrack(state, choices, res)\n  # Check if it is a solution\n  record_solution(state, res) if is_solution?(state)\n\n  # Traverse all choices\n  for choice in choices\n    # Pruning: check if the choice is valid\n    if is_valid?(state, choice)\n      # Attempt: make choice, update state\n      make_choice(state, choice)\n      # Proceed to the next round of selection\n      backtrack(state, [choice.left, choice.right], res)\n      # Backtrack: undo choice, restore to previous state\n      undo_choice(state, choice)\n    end\n  end\nend\n

As per the problem statement, we should continue searching after finding a node with value \\(7\\). Therefore, we need to remove the return statement after recording the solution. The following figure compares the search process with and without the return statement.

Figure 13-4   Comparison of search process with and without return statement

Compared to code based on preorder traversal, code based on the backtracking algorithm framework appears more verbose, but has better generality. In fact, many backtracking problems can be solved within this framework. We only need to define state and choices for the specific problem and implement each method in the framework.

","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314-common-terminology","level":2,"title":"13.1.4   Common Terminology","text":"

To analyze algorithmic problems more clearly, we summarize the meanings of common terminology used in backtracking algorithms and provide corresponding examples from Example 3, as shown in the following table.

Table 13-1   Common Backtracking Algorithm Terminology

Term Definition Example 3 Solution (solution) A solution is an answer that satisfies the specific conditions of a problem; there may be one or more solutions All paths from root to nodes with value \\(7\\) that satisfy the constraint Constraint (constraint) A constraint is a condition in the problem that limits the feasibility of solutions, typically used for pruning Paths do not contain nodes with value \\(3\\) State (state) State represents the situation of a problem at a certain moment, including the choices already made The currently visited node path, i.e., the path list of nodes Attempt (attempt) An attempt is the process of exploring the solution space according to available choices, including making choices, updating state, and checking if it is a solution Recursively visit left (right) child nodes, add nodes to path, check if node value is \\(7\\) Backtrack (backtracking) Backtracking refers to undoing previous choices and returning to a previous state when encountering a state that does not satisfy constraints Stop searching when passing over leaf nodes, ending node visits, or encountering nodes with value \\(3\\); function returns Pruning (pruning) Pruning is a method of avoiding meaningless search paths according to problem characteristics and constraints, which can improve search efficiency When encountering a node with value \\(3\\), do not continue searching

Tip

The concepts of problem, solution, state, etc. are universal and are involved in divide-and-conquer, backtracking, dynamic programming, greedy and other algorithms.

","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1315-advantages-and-limitations","level":2,"title":"13.1.5   Advantages and Limitations","text":"

The backtracking algorithm is essentially a depth-first search algorithm that tries all possible solutions until it finds one that satisfies the conditions. The advantage of this approach is that it can find all possible solutions, and with reasonable pruning operations, it achieves high efficiency.

However, when dealing with large-scale or complex problems, the running efficiency of the backtracking algorithm may be unacceptable.

  • Time: The backtracking algorithm usually needs to traverse all possibilities in the solution space, and the time complexity can reach exponential or factorial order.
  • Space: During recursive calls, the current state needs to be saved (such as paths, auxiliary variables used for pruning, etc.), and when the depth is large, the space requirement can become very large.

Nevertheless, the backtracking algorithm is still the best solution for certain search problems and constraint satisfaction problems. For these problems, since we cannot predict which choices will generate valid solutions, we must traverse all possible choices. In this case, the key is how to optimize efficiency. There are two common efficiency optimization methods.

  • Pruning: Avoid searching paths that are guaranteed not to produce solutions, thereby saving time and space.
  • Heuristic search: Introduce certain strategies or estimation values during the search process to prioritize searching paths that are most likely to produce valid solutions.
","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316-typical-backtracking-examples","level":2,"title":"13.1.6   Typical Backtracking Examples","text":"

The backtracking algorithm can be used to solve many search problems, constraint satisfaction problems, and combinatorial optimization problems.

Search problems: The goal of these problems is to find solutions that satisfy specific conditions.

  • Permutation problem: Given a set, find all possible permutations and combinations.
  • Subset sum problem: Given a set and a target sum, find all subsets in the set whose elements sum to the target.
  • Tower of Hanoi: Given three pegs and a series of disks of different sizes, move all disks from one peg to another, moving only one disk at a time, and never placing a larger disk on a smaller disk.

Constraint satisfaction problems: The goal of these problems is to find solutions that satisfy all constraints.

  • N-Queens: Place \\(n\\) queens on an \\(n \\times n\\) chessboard such that they do not attack each other.
  • Sudoku: Fill numbers \\(1\\) to \\(9\\) in a \\(9 \\times 9\\) grid such that each row, column, and \\(3 \\times 3\\) subgrid contains no repeated digits.
  • Graph coloring: Given an undirected graph, color each vertex with the minimum number of colors such that adjacent vertices have different colors.

Combinatorial optimization problems: The goal of these problems is to find an optimal solution that satisfies certain conditions in a combinatorial space.

  • 0-1 Knapsack: Given a set of items and a knapsack, each item has a value and weight. Under the knapsack capacity constraint, select items to maximize total value.
  • Traveling Salesman Problem: Starting from a point in a graph, visit all other points exactly once and return to the starting point, finding the shortest path.
  • Maximum Clique: Given an undirected graph, find the largest complete subgraph, i.e., a subgraph where any two vertices are connected by an edge.

Note that for many combinatorial optimization problems, backtracking is not the optimal solution.

  • The 0-1 Knapsack problem is usually solved using dynamic programming to achieve higher time efficiency.
  • The Traveling Salesman Problem is a famous NP-Hard problem; common solutions include genetic algorithms and ant colony algorithms.
  • The Maximum Clique problem is a classical problem in graph theory and can be solved using heuristic algorithms such as greedy algorithms.
","path":["Chapter 13. Backtracking","13.1   Backtracking Algorithm"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/","level":1,"title":"13.4   N-Queens Problem","text":"

Question

According to the rules of chess, a queen can attack pieces that share the same row, column, or diagonal line. Given \\(n\\) queens and an \\(n \\times n\\) chessboard, find a placement scheme such that no two queens can attack each other.

As shown in Figure 13-15, when \\(n = 4\\), there are two solutions that can be found. From the perspective of the backtracking algorithm, an \\(n \\times n\\) chessboard has \\(n^2\\) squares, which provide all the choices choices. During the process of placing queens one by one, the chessboard state changes continuously, and the chessboard at each moment represents the state state.

Figure 13-15   Solution to the 4-queens problem

Figure 13-16 illustrates the three constraints of this problem: multiple queens cannot be in the same row, the same column, or on the same diagonal. It is worth noting that diagonals are divided into two types: the main diagonal \\ and the anti-diagonal /.

Figure 13-16   Constraints of the n-queens problem

","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1-row-by-row-placement-strategy","level":3,"title":"1.   Row-By-Row Placement Strategy","text":"

Since both the number of queens and the number of rows on the chessboard are \\(n\\), we can easily derive a conclusion: each row of the chessboard allows and only allows exactly one queen to be placed.

This means we can adopt a row-by-row placement strategy: starting from the first row, place one queen in each row until the last row is completed.

Figure 13-17 shows the row-by-row placement process for the 4-queens problem. Due to space limitations, the figure only expands one search branch of the first row, and all schemes that do not satisfy the column constraint and diagonal constraints are pruned.

Figure 13-17   Row-by-row placement strategy

Essentially, the row-by-row placement strategy serves a pruning function, as it avoids all search branches where multiple queens appear in the same row.

","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2-column-and-diagonal-pruning","level":3,"title":"2.   Column and Diagonal Pruning","text":"

To satisfy the column constraint, we can use a boolean array cols of length \\(n\\) to record whether each column has a queen. Before each placement decision, we use cols to prune columns that already have queens, and dynamically update the state of cols during backtracking.

Tip

Please note that the origin of the matrix is located in the upper-left corner, where the row index increases from top to bottom, and the column index increases from left to right.

So how do we handle diagonal constraints? Consider a square on the chessboard with row and column indices \\((row, col)\\). If we select a specific main diagonal in the matrix, we find that all squares on that diagonal have the same difference between their row and column indices, meaning that \\(row - col\\) is a constant value for all squares on the main diagonal.

In other words, if two squares satisfy \\(row_1 - col_1 = row_2 - col_2\\), they must be on the same main diagonal. Using this pattern, we can use the array diags1 shown in Figure 13-18 to record whether there is a queen on each main diagonal.

Similarly, for all squares on an anti-diagonal, the sum \\(row + col\\) is a constant value. We can likewise use the array diags2 to handle anti-diagonal constraints.

Figure 13-18   Handling column and diagonal constraints

","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

Please note that in an \\(n\\)-dimensional square matrix, the range of \\(row - col\\) is \\([-n + 1, n - 1]\\), and the range of \\(row + col\\) is \\([0, 2n - 2]\\). Therefore, the number of both main diagonals and anti-diagonals is \\(2n - 1\\), meaning the length of both arrays diags1 and diags2 is \\(2n - 1\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby n_queens.py
def backtrack(\n    row: int,\n    n: int,\n    state: list[list[str]],\n    res: list[list[list[str]]],\n    cols: list[bool],\n    diags1: list[bool],\n    diags2: list[bool],\n):\n    \"\"\"Backtracking algorithm: N queens\"\"\"\n    # When all rows are placed, record the solution\n    if row == n:\n        res.append([list(row) for row in state])\n        return\n    # Traverse all columns\n    for col in range(n):\n        # Calculate the main diagonal and anti-diagonal corresponding to this cell\n        diag1 = row - col + n - 1\n        diag2 = row + col\n        # Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if not cols[col] and not diags1[diag1] and not diags2[diag2]:\n            # Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            cols[col] = diags1[diag1] = diags2[diag2] = True\n            # Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            # Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            cols[col] = diags1[diag1] = diags2[diag2] = False\n\ndef n_queens(n: int) -> list[list[list[str]]]:\n    \"\"\"Solve N queens\"\"\"\n    # Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    state = [[\"#\" for _ in range(n)] for _ in range(n)]\n    cols = [False] * n  # Record whether there is a queen in the column\n    diags1 = [False] * (2 * n - 1)  # Record whether there is a queen on the main diagonal\n    diags2 = [False] * (2 * n - 1)  # Record whether there is a queen on the anti-diagonal\n    res = []\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n
n_queens.cpp
/* Backtracking algorithm: N queens */\nvoid backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,\n               vector<bool> &diags1, vector<bool> &diags2) {\n    // When all rows are placed, record the solution\n    if (row == n) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nvector<vector<vector<string>>> nQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    vector<vector<string>> state(n, vector<string>(n, \"#\"));\n    vector<bool> cols(n, false);           // Record whether there is a queen in the column\n    vector<bool> diags1(2 * n - 1, false); // Record whether there is a queen on the main diagonal\n    vector<bool> diags2(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal\n    vector<vector<vector<string>>> res;\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.java
/* Backtracking algorithm: N queens */\nvoid backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,\n        boolean[] cols, boolean[] diags1, boolean[] diags2) {\n    // When all rows are placed, record the solution\n    if (row == n) {\n        List<List<String>> copyState = new ArrayList<>();\n        for (List<String> sRow : state) {\n            copyState.add(new ArrayList<>(sRow));\n        }\n        res.add(copyState);\n        return;\n    }\n    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state.get(row).set(col, \"Q\");\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state.get(row).set(col, \"#\");\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nList<List<List<String>>> nQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    List<List<String>> state = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<String> row = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            row.add(\"#\");\n        }\n        state.add(row);\n    }\n    boolean[] cols = new boolean[n]; // Record whether there is a queen in the column\n    boolean[] diags1 = new boolean[2 * n - 1]; // Record whether there is a queen on the main diagonal\n    boolean[] diags2 = new boolean[2 * n - 1]; // Record whether there is a queen on the anti-diagonal\n    List<List<List<String>>> res = new ArrayList<>();\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.cs
/* Backtracking algorithm: N queens */\nvoid Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,\n        bool[] cols, bool[] diags1, bool[] diags2) {\n    // When all rows are placed, record the solution\n    if (row == n) {\n        List<List<string>> copyState = [];\n        foreach (List<string> sRow in state) {\n            copyState.Add(new List<string>(sRow));\n        }\n        res.Add(copyState);\n        return;\n    }\n    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            Backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nList<List<List<string>>> NQueens(int n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    List<List<string>> state = [];\n    for (int i = 0; i < n; i++) {\n        List<string> row = [];\n        for (int j = 0; j < n; j++) {\n            row.Add(\"#\");\n        }\n        state.Add(row);\n    }\n    bool[] cols = new bool[n]; // Record whether there is a queen in the column\n    bool[] diags1 = new bool[2 * n - 1]; // Record whether there is a queen on the main diagonal\n    bool[] diags2 = new bool[2 * n - 1]; // Record whether there is a queen on the anti-diagonal\n    List<List<List<string>>> res = [];\n\n    Backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.go
/* Backtracking algorithm: N queens */\nfunc backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {\n    // When all rows are placed, record the solution\n    if row == n {\n        newState := make([][]string, len(*state))\n        for i, _ := range newState {\n            newState[i] = make([]string, len((*state)[0]))\n            copy(newState[i], (*state)[i])\n\n        }\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all columns\n    for col := 0; col < n; col++ {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        diag1 := row - col + n - 1\n        diag2 := row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {\n            // Attempt: place the queen in this cell\n            (*state)[row][col] = \"Q\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true\n            // Place the next row\n            backtrack(row+1, n, state, res, cols, diags1, diags2)\n            // Backtrack: restore this cell to an empty cell\n            (*state)[row][col] = \"#\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false\n        }\n    }\n}\n\n/* Solve N queens */\nfunc nQueens(n int) [][][]string {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    state := make([][]string, n)\n    for i := 0; i < n; i++ {\n        row := make([]string, n)\n        for i := 0; i < n; i++ {\n            row[i] = \"#\"\n        }\n        state[i] = row\n    }\n    // Record whether there is a queen in the column\n    cols := make([]bool, n)\n    diags1 := make([]bool, 2*n-1)\n    diags2 := make([]bool, 2*n-1)\n    res := make([][][]string, 0)\n    backtrack(0, n, &state, &res, &cols, &diags1, &diags2)\n    return res\n}\n
n_queens.swift
/* Backtracking algorithm: N queens */\nfunc backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {\n    // When all rows are placed, record the solution\n    if row == n {\n        res.append(state)\n        return\n    }\n    // Traverse all columns\n    for col in 0 ..< n {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        let diag1 = row - col + n - 1\n        let diag2 = row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            cols[col] = true\n            diags1[diag1] = true\n            diags2[diag2] = true\n            // Place the next row\n            backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            cols[col] = false\n            diags1[diag1] = false\n            diags2[diag2] = false\n        }\n    }\n}\n\n/* Solve N queens */\nfunc nQueens(n: Int) -> [[[String]]] {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    var state = Array(repeating: Array(repeating: \"#\", count: n), count: n)\n    var cols = Array(repeating: false, count: n) // Record whether there is a queen in the column\n    var diags1 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the main diagonal\n    var diags2 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the anti-diagonal\n    var res: [[[String]]] = []\n\n    backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n\n    return res\n}\n
n_queens.js
/* Backtracking algorithm: N queens */\nfunction backtrack(row, n, state, res, cols, diags1, diags2) {\n    // When all rows are placed, record the solution\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Traverse all columns\n    for (let col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nfunction nQueens(n) {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Record whether there is a queen in the column\n    const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal\n    const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal\n    const res = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
n_queens.ts
/* Backtracking algorithm: N queens */\nfunction backtrack(\n    row: number,\n    n: number,\n    state: string[][],\n    res: string[][][],\n    cols: boolean[],\n    diags1: boolean[],\n    diags2: boolean[]\n): void {\n    // When all rows are placed, record the solution\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // Traverse all columns\n    for (let col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nfunction nQueens(n: number): string[][][] {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // Record whether there is a queen in the column\n    const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal\n    const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal\n    const res: string[][][] = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
n_queens.dart
/* Backtracking algorithm: N queens */\nvoid backtrack(\n  int row,\n  int n,\n  List<List<String>> state,\n  List<List<List<String>>> res,\n  List<bool> cols,\n  List<bool> diags1,\n  List<bool> diags2,\n) {\n  // When all rows are placed, record the solution\n  if (row == n) {\n    List<List<String>> copyState = [];\n    for (List<String> sRow in state) {\n      copyState.add(List.from(sRow));\n    }\n    res.add(copyState);\n    return;\n  }\n  // Traverse all columns\n  for (int col = 0; col < n; col++) {\n    // Calculate the main diagonal and anti-diagonal corresponding to this cell\n    int diag1 = row - col + n - 1;\n    int diag2 = row + col;\n    // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n    if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n      // Attempt: place the queen in this cell\n      state[row][col] = \"Q\";\n      cols[col] = true;\n      diags1[diag1] = true;\n      diags2[diag2] = true;\n      // Place the next row\n      backtrack(row + 1, n, state, res, cols, diags1, diags2);\n      // Backtrack: restore this cell to an empty cell\n      state[row][col] = \"#\";\n      cols[col] = false;\n      diags1[diag1] = false;\n      diags2[diag2] = false;\n    }\n  }\n}\n\n/* Solve N queens */\nList<List<List<String>>> nQueens(int n) {\n  // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n  List<List<String>> state = List.generate(n, (index) => List.filled(n, \"#\"));\n  List<bool> cols = List.filled(n, false); // Record whether there is a queen in the column\n  List<bool> diags1 = List.filled(2 * n - 1, false); // Record whether there is a queen on the main diagonal\n  List<bool> diags2 = List.filled(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal\n  List<List<List<String>>> res = [];\n\n  backtrack(0, n, state, res, cols, diags1, diags2);\n\n  return res;\n}\n
n_queens.rs
/* Backtracking algorithm: N queens */\nfn backtrack(\n    row: usize,\n    n: usize,\n    state: &mut Vec<Vec<String>>,\n    res: &mut Vec<Vec<Vec<String>>>,\n    cols: &mut [bool],\n    diags1: &mut [bool],\n    diags2: &mut [bool],\n) {\n    // When all rows are placed, record the solution\n    if row == n {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all columns\n    for col in 0..n {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        let diag1 = row + n - 1 - col;\n        let diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);\n        }\n    }\n}\n\n/* Solve N queens */\nfn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    let mut state: Vec<Vec<String>> = vec![vec![\"#\".to_string(); n]; n];\n    let mut cols = vec![false; n]; // Record whether there is a queen in the column\n    let mut diags1 = vec![false; 2 * n - 1]; // Record whether there is a queen on the main diagonal\n    let mut diags2 = vec![false; 2 * n - 1]; // Record whether there is a queen on the anti-diagonal\n    let mut res: Vec<Vec<Vec<String>>> = Vec::new();\n\n    backtrack(\n        0,\n        n,\n        &mut state,\n        &mut res,\n        &mut cols,\n        &mut diags1,\n        &mut diags2,\n    );\n\n    res\n}\n
n_queens.c
/* Backtracking algorithm: N queens */\nvoid backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE],\n               bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) {\n    // When all rows are placed, record the solution\n    if (row == n) {\n        res[*resSize] = (char **)malloc(sizeof(char *) * n);\n        for (int i = 0; i < n; ++i) {\n            res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1));\n            strcpy(res[*resSize][i], state[i]);\n        }\n        (*resSize)++;\n        return;\n    }\n    // Traverse all columns\n    for (int col = 0; col < n; col++) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // Place the next row\n            backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* Solve N queens */\nchar ***nQueens(int n, int *returnSize) {\n    char state[MAX_SIZE][MAX_SIZE];\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    for (int i = 0; i < n; ++i) {\n        for (int j = 0; j < n; ++j) {\n            state[i][j] = '#';\n        }\n        state[i][n] = '\\0';\n    }\n    bool cols[MAX_SIZE] = {false};           // Record whether there is a queen in the column\n    bool diags1[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the main diagonal\n    bool diags2[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the anti-diagonal\n\n    char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE);\n    *returnSize = 0;\n    backtrack(0, n, state, res, returnSize, cols, diags1, diags2);\n    return res;\n}\n
n_queens.kt
/* Backtracking algorithm: N queens */\nfun backtrack(\n    row: Int,\n    n: Int,\n    state: MutableList<MutableList<String>>,\n    res: MutableList<MutableList<MutableList<String>>?>,\n    cols: BooleanArray,\n    diags1: BooleanArray,\n    diags2: BooleanArray\n) {\n    // When all rows are placed, record the solution\n    if (row == n) {\n        val copyState = mutableListOf<MutableList<String>>()\n        for (sRow in state) {\n            copyState.add(sRow.toMutableList())\n        }\n        res.add(copyState)\n        return\n    }\n    // Traverse all columns\n    for (col in 0..<n) {\n        // Calculate the main diagonal and anti-diagonal corresponding to this cell\n        val diag1 = row - col + n - 1\n        val diag2 = row + col\n        // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // Attempt: place the queen in this cell\n            state[row][col] = \"Q\"\n            diags2[diag2] = true\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n            // Place the next row\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            // Backtrack: restore this cell to an empty cell\n            state[row][col] = \"#\"\n            diags2[diag2] = false\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n        }\n    }\n}\n\n/* Solve N queens */\nfun nQueens(n: Int): MutableList<MutableList<MutableList<String>>?> {\n    // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n    val state = mutableListOf<MutableList<String>>()\n    for (i in 0..<n) {\n        val row = mutableListOf<String>()\n        for (j in 0..<n) {\n            row.add(\"#\")\n        }\n        state.add(row)\n    }\n    val cols = BooleanArray(n) // Record whether there is a queen in the column\n    val diags1 = BooleanArray(2 * n - 1) // Record whether there is a queen on the main diagonal\n    val diags2 = BooleanArray(2 * n - 1) // Record whether there is a queen on the anti-diagonal\n    val res = mutableListOf<MutableList<MutableList<String>>?>()\n\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n}\n
n_queens.rb
### Backtracking: n queens ###\ndef backtrack(row, n, state, res, cols, diags1, diags2)\n  # When all rows are placed, record the solution\n  if row == n\n    res << state.map { |row| row.dup }\n    return\n  end\n\n  # Traverse all columns\n  for col in 0...n\n    # Calculate the main diagonal and anti-diagonal corresponding to this cell\n    diag1 = row - col + n - 1\n    diag2 = row + col\n    # Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell\n    if !cols[col] && !diags1[diag1] && !diags2[diag2]\n      # Attempt: place the queen in this cell\n      state[row][col] = \"Q\"\n      cols[col] = diags1[diag1] = diags2[diag2] = true\n      # Place the next row\n      backtrack(row + 1, n, state, res, cols, diags1, diags2)\n      # Backtrack: restore this cell to an empty cell\n      state[row][col] = \"#\"\n      cols[col] = diags1[diag1] = diags2[diag2] = false\n    end\n  end\nend\n\n### Solve n queens ###\ndef n_queens(n)\n  # Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell\n  state = Array.new(n) { Array.new(n, \"#\") }\n  cols = Array.new(n, false) # Record whether there is a queen in the column\n  diags1 = Array.new(2 * n - 1, false) # Record whether there is a queen on the main diagonal\n  diags2 = Array.new(2 * n - 1, false) # Record whether there is a queen on the anti-diagonal\n  res = []\n  backtrack(0, n, state, res, cols, diags1, diags2)\n\n  res\nend\n

Placing \\(n\\) queens row by row, considering the column constraint, from the first row to the last row there are \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\) choices, using \\(O(n!)\\) time. When recording a solution, it is necessary to copy the matrix state and add it to res, and the copy operation uses \\(O(n^2)\\) time. Therefore, the overall time complexity is \\(O(n! \\cdot n^2)\\). In practice, pruning based on diagonal constraints can also significantly reduce the search space, so the search efficiency is often better than the time complexity mentioned above.

The array state uses \\(O(n^2)\\) space, and the arrays cols, diags1, and diags2 each use \\(O(n)\\) space. The maximum recursion depth is \\(n\\), using \\(O(n)\\) stack frame space. Therefore, the space complexity is \\(O(n^2)\\).

","path":["Chapter 13. Backtracking","13.4   N-Queens Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/","level":1,"title":"13.2   Permutations Problem","text":"

The permutations problem is a classic application of backtracking algorithms. It is defined as finding all possible arrangements of elements in a given collection (such as an array or string).

Table 13-2 shows several example datasets, including input arrays and their corresponding permutations.

Table 13-2   Permutations Examples

Input Array All Permutations \\([1]\\) \\([1]\\) \\([1, 2]\\) \\([1, 2], [2, 1]\\) \\([1, 2, 3]\\) \\([1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]\\)","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1321-case-with-distinct-elements","level":2,"title":"13.2.1   Case with Distinct Elements","text":"

Question

Given an integer array with no duplicate elements, return all possible permutations.

From the perspective of backtracking algorithms, we can imagine the process of generating permutations as the result of a series of choices. Suppose the input array is \\([1, 2, 3]\\). If we first choose \\(1\\), then choose \\(3\\), and finally choose \\(2\\), we obtain the permutation \\([1, 3, 2]\\). Backtracking means undoing a choice and then trying other choices.

From the perspective of backtracking code, the candidate set choices consists of all elements in the input array, and the state state is the elements that have been chosen so far. Note that each element can only be chosen once, therefore all elements in state should be unique.

As shown in Figure 13-5, we can unfold the search process into a recursion tree, where each node in the tree represents the current state state. Starting from the root node, after three rounds of choices, we reach a leaf node, and each leaf node corresponds to a permutation.

Figure 13-5   Recursion tree of permutations

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1-pruning-duplicate-choices","level":3,"title":"1.   Pruning Duplicate Choices","text":"

To ensure that each element is chosen only once, we consider introducing a boolean array selected, where selected[i] indicates whether choices[i] has been chosen. We implement the following pruning operation based on it.

  • After making a choice choice[i], we set selected[i] to \\(\\text{True}\\), indicating that it has been chosen.
  • When traversing the candidate list choices, we skip all nodes that have been chosen, which is pruning.

As shown in Figure 13-6, suppose we choose \\(1\\) in the first round, \\(3\\) in the second round, and \\(2\\) in the third round. Then we need to prune the branch of element \\(1\\) in the second round and prune the branches of elements \\(1\\) and \\(3\\) in the third round.

Figure 13-6   Pruning example of permutations

Observing the above figure, we find that this pruning operation reduces the search space size from \\(O(n^n)\\) to \\(O(n!)\\).

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

After understanding the above information, we can fill in the blanks in the template code. To shorten the overall code, we do not implement each function in the template separately, but instead unfold them in the backtrack() function:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_i.py
def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Permutations I\"\"\"\n    # When the state length equals the number of elements, record the solution\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Traverse all choices\n    for i, choice in enumerate(choices):\n        # Pruning: do not allow repeated selection of elements\n        if not selected[i]:\n            # Attempt: make choice, update state\n            selected[i] = True\n            state.append(choice)\n            # Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            # Backtrack: undo choice, restore to previous state\n            selected[i] = False\n            state.pop()\n\ndef permutations_i(nums: list[int]) -> list[list[int]]:\n    \"\"\"Permutations I\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
permutations_i.cpp
/* Backtracking algorithm: Permutations I */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push_back(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Permutations I */\nvector<vector<int>> permutationsI(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
permutations_i.java
/* Backtracking algorithm: Permutations I */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.add(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Permutations I */\nList<List<Integer>> permutationsI(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
permutations_i.cs
/* Backtracking algorithm: Permutations I */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.Add(choice);\n            // Proceed to the next round of selection\n            Backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Permutations I */\nList<List<int>> PermutationsI(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
permutations_i.go
/* Backtracking algorithm: Permutations I */\nfunc backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // When the state length equals the number of elements, record the solution\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Traverse all choices\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Pruning: do not allow repeated selection of elements\n        if !(*selected)[i] {\n            // Attempt: make choice, update state\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Proceed to the next round of selection\n            backtrackI(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Permutations I */\nfunc permutationsI(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackI(&state, &nums, &selected, &res)\n    return res\n}\n
permutations_i.swift
/* Backtracking algorithm: Permutations I */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // When the state length equals the number of elements, record the solution\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    for (i, choice) in choices.enumerated() {\n        // Pruning: do not allow repeated selection of elements\n        if !selected[i] {\n            // Attempt: make choice, update state\n            selected[i] = true\n            state.append(choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Permutations I */\nfunc permutationsI(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
permutations_i.js
/* Backtracking algorithm: Permutations I */\nfunction backtrack(state, choices, selected, res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations I */\nfunction permutationsI(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_i.ts
/* Backtracking algorithm: Permutations I */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations I */\nfunction permutationsI(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_i.dart
/* Backtracking algorithm: Permutations I */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // When the state length equals the number of elements, record the solution\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Pruning: do not allow repeated selection of elements\n    if (!selected[i]) {\n      // Attempt: make choice, update state\n      selected[i] = true;\n      state.add(choice);\n      // Proceed to the next round of selection\n      backtrack(state, choices, selected, res);\n      // Backtrack: undo choice, restore to previous state\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Permutations I */\nList<List<int>> permutationsI(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
permutations_i.rs
/* Backtracking algorithm: Permutations I */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // When the state length equals the number of elements, record the solution\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Traverse all choices\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if !selected[i] {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state.clone(), choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Permutations I */\nfn permutations_i(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new(); // State (subset)\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
permutations_i.c
/* Backtracking algorithm: Permutations I */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // When the state length equals the number of elements, record the solution\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Proceed to the next round of selection\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n        }\n    }\n}\n\n/* Permutations I */\nint **permutationsI(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
permutations_i.kt
/* Backtracking algorithm: Permutations I */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Pruning: do not allow repeated selection of elements\n        if (!selected[i]) {\n            // Attempt: make choice, update state\n            selected[i] = true\n            state.add(choice)\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Permutations I */\nfun permutationsI(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
permutations_i.rb
### Backtracking: permutations I ###\ndef backtrack(state, choices, selected, res)\n  # When the state length equals the number of elements, record the solution\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  choices.each_with_index do |choice, i|\n    # Pruning: do not allow repeated selection of elements\n    unless selected[i]\n      # Attempt: make choice, update state\n      selected[i] = true\n      state << choice\n      # Proceed to the next round of selection\n      backtrack(state, choices, selected, res)\n      # Backtrack: undo choice, restore to previous state\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Permutations I ###\ndef permutations_i(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1322-case-with-duplicate-elements","level":2,"title":"13.2.2   Case with Duplicate Elements","text":"

Question

Given an integer array that may contain duplicate elements, return all unique permutations.

Suppose the input array is \\([1, 1, 2]\\). To distinguish the two duplicate elements \\(1\\), we denote the second \\(1\\) as \\(\\hat{1}\\).

As shown in Figure 13-7, the method described above generates permutations where half are duplicates.

Figure 13-7   Duplicate permutations

So how do we remove duplicate permutations? The most direct approach is to use a hash set to directly deduplicate the permutation results. However, this is not elegant because the search branches that generate duplicate permutations are unnecessary and should be identified and pruned early, which can further improve algorithm efficiency.

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1-pruning-duplicate-elements","level":3,"title":"1.   Pruning Duplicate Elements","text":"

Observe Figure 13-8. In the first round, choosing \\(1\\) or choosing \\(\\hat{1}\\) is equivalent. All permutations generated under these two choices are duplicates. Therefore, we should prune \\(\\hat{1}\\).

Similarly, after choosing \\(2\\) in the first round, the \\(1\\) and \\(\\hat{1}\\) in the second round also produce duplicate branches, so the second round's \\(\\hat{1}\\) should also be pruned.

Essentially, our goal is to ensure that multiple equal elements are chosen only once in a certain round of choices.

Figure 13-8   Pruning duplicate permutations

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2-code-implementation_1","level":3,"title":"2.   Code Implementation","text":"

Building on the code from the previous problem, we consider opening a hash set duplicated in each round of choices to record which elements have been tried in this round, and prune duplicate elements:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_ii.py
def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Permutations II\"\"\"\n    # When the state length equals the number of elements, record the solution\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # Traverse all choices\n    duplicated = set[int]()\n    for i, choice in enumerate(choices):\n        # Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if not selected[i] and choice not in duplicated:\n            # Attempt: make choice, update state\n            duplicated.add(choice)  # Record the selected element value\n            selected[i] = True\n            state.append(choice)\n            # Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            # Backtrack: undo choice, restore to previous state\n            selected[i] = False\n            state.pop()\n\ndef permutations_ii(nums: list[int]) -> list[list[int]]:\n    \"\"\"Permutations II\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
permutations_ii.cpp
/* Backtracking algorithm: Permutations II */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    unordered_set<int> duplicated;\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && duplicated.find(choice) == duplicated.end()) {\n            // Attempt: make choice, update state\n            duplicated.emplace(choice); // Record the selected element value\n            selected[i] = true;\n            state.push_back(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* Permutations II */\nvector<vector<int>> permutationsII(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
permutations_ii.java
/* Backtracking algorithm: Permutations II */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // Traverse all choices\n    Set<Integer> duplicated = new HashSet<Integer>();\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.add(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* Permutations II */\nList<List<Integer>> permutationsII(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
permutations_ii.cs
/* Backtracking algorithm: Permutations II */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    HashSet<int> duplicated = [];\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.Contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.Add(choice); // Record the selected element value\n            selected[i] = true;\n            state.Add(choice);\n            // Proceed to the next round of selection\n            Backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* Permutations II */\nList<List<int>> PermutationsII(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
permutations_ii.go
/* Backtracking algorithm: Permutations II */\nfunc backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // When the state length equals the number of elements, record the solution\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // Traverse all choices\n    duplicated := make(map[int]struct{}, 0)\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if _, ok := duplicated[choice]; !ok && !(*selected)[i] {\n            // Attempt: make choice, update state\n            // Record the selected element value\n            duplicated[choice] = struct{}{}\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // Proceed to the next round of selection\n            backtrackII(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* Permutations II */\nfunc permutationsII(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackII(&state, &nums, &selected, &res)\n    return res\n}\n
permutations_ii.swift
/* Backtracking algorithm: Permutations II */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // When the state length equals the number of elements, record the solution\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    var duplicated: Set<Int> = []\n    for (i, choice) in choices.enumerated() {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if !selected[i], !duplicated.contains(choice) {\n            // Attempt: make choice, update state\n            duplicated.insert(choice) // Record the selected element value\n            selected[i] = true\n            state.append(choice)\n            // Proceed to the next round of selection\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* Permutations II */\nfunc permutationsII(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
permutations_ii.js
/* Backtracking algorithm: Permutations II */\nfunction backtrack(state, choices, selected, res) {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations II */\nfunction permutationsII(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_ii.ts
/* Backtracking algorithm: Permutations II */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // When the state length equals the number of elements, record the solution\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.has(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* Permutations II */\nfunction permutationsII(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_ii.dart
/* Backtracking algorithm: Permutations II */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // When the state length equals the number of elements, record the solution\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  Set<int> duplicated = {};\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n    if (!selected[i] && !duplicated.contains(choice)) {\n      // Attempt: make choice, update state\n      duplicated.add(choice); // Record the selected element value\n      selected[i] = true;\n      state.add(choice);\n      // Proceed to the next round of selection\n      backtrack(state, choices, selected, res);\n      // Backtrack: undo choice, restore to previous state\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* Permutations II */\nList<List<int>> permutationsII(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
permutations_ii.rs
/* Backtracking algorithm: Permutations II */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // When the state length equals the number of elements, record the solution\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // Traverse all choices\n    let mut duplicated = HashSet::<i32>::new();\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if !selected[i] && !duplicated.contains(&choice) {\n            // Attempt: make choice, update state\n            duplicated.insert(choice); // Record the selected element value\n            selected[i] = true;\n            state.push(choice);\n            // Proceed to the next round of selection\n            backtrack(state.clone(), choices, selected, res);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* Permutations II */\nfn permutations_ii(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new();\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
permutations_ii.c
/* Backtracking algorithm: Permutations II */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // When the state length equals the number of elements, record the solution\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // Traverse all choices\n    bool duplicated[MAX_SIZE] = {false};\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated[choice]) {\n            // Attempt: make choice, update state\n            duplicated[choice] = true; // Record the selected element value\n            selected[i] = true;\n            state[stateSize] = choice;\n            // Proceed to the next round of selection\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false;\n        }\n    }\n}\n\n/* Permutations II */\nint **permutationsII(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
permutations_ii.kt
/* Backtracking algorithm: Permutations II */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the state length equals the number of elements, record the solution\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    val duplicated = HashSet<Int>()\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // Attempt: make choice, update state\n            duplicated.add(choice) // Record the selected element value\n            selected[i] = true\n            state.add(choice)\n            // Proceed to the next round of selection\n            backtrack(state, choices, selected, res)\n            // Backtrack: undo choice, restore to previous state\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* Permutations II */\nfun permutationsII(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
permutations_ii.rb
### Backtracking: permutations II ###\ndef backtrack(state, choices, selected, res)\n  # When the state length equals the number of elements, record the solution\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  duplicated = Set.new\n  choices.each_with_index do |choice, i|\n    # Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements\n    if !selected[i] && !duplicated.include?(choice)\n      # Attempt: make choice, update state\n      duplicated.add(choice)\n      selected[i] = true\n      state << choice\n      # Proceed to the next round of selection\n      backtrack(state, choices, selected, res)\n      # Backtrack: undo choice, restore to previous state\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### Permutations II ###\ndef permutations_ii(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n

Assuming elements are pairwise distinct, there are \\(n!\\) (factorial) permutations of \\(n\\) elements. When recording results, we need to copy a list of length \\(n\\), using \\(O(n)\\) time. Therefore, the time complexity is \\(O(n! \\cdot n)\\).

The maximum recursion depth is \\(n\\), using \\(O(n)\\) stack frame space. selected uses \\(O(n)\\) space. At most \\(n\\) duplicated sets exist simultaneously, using \\(O(n^2)\\) space. Therefore, the space complexity is \\(O(n^2)\\).

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3-comparison-of-two-pruning-methods","level":3,"title":"3.   Comparison of Two Pruning Methods","text":"

Note that although both selected and duplicated are used for pruning, they have different objectives.

  • Pruning duplicate choices: There is only one selected throughout the entire search process. It records which elements are included in the current state, and its purpose is to prevent an element from appearing repeatedly in state.
  • Pruning duplicate elements: Each round of choices (each backtrack function call) contains a duplicated set. It records which elements have been chosen in this round's iteration (the for loop), and its purpose is to ensure that equal elements are chosen only once.

Figure 13-9 shows the effective scope of the two pruning conditions. Note that each node in the tree represents a choice, and the nodes on the path from the root to a leaf node form a permutation.

Figure 13-9   Effective scope of two pruning conditions

","path":["Chapter 13. Backtracking","13.2   Permutations Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/","level":1,"title":"13.3   Subset-Sum Problem","text":"","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1331-without-duplicate-elements","level":2,"title":"13.3.1   Without Duplicate Elements","text":"

Question

Given a positive integer array nums and a target positive integer target, find all possible combinations where the sum of elements in the combination equals target. The given array has no duplicate elements, and each element can be selected multiple times. Return these combinations in list form, where the list should not contain duplicate combinations.

For example, given the set \\(\\{3, 4, 5\\}\\) and target integer \\(9\\), the solutions are \\(\\{3, 3, 3\\}, \\{4, 5\\}\\). Note the following two points:

  • Elements in the input set can be selected repeatedly without limit.
  • Subsets do not distinguish element order; for example, \\(\\{4, 5\\}\\) and \\(\\{5, 4\\}\\) are the same subset.
","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1-reference-to-full-permutation-solution","level":3,"title":"1.   Reference to Full Permutation Solution","text":"

Similar to the full permutation problem, we can imagine the process of generating subsets as a series of choices, and update the \"sum of elements\" in real-time during the selection process. When the sum equals target, we record the subset to the result list.

Unlike the full permutation problem, elements in this problem's set can be selected unlimited times, so we do not need to use a selected boolean list to track whether an element has been selected. We can make minor modifications to the full permutation code and initially obtain the solution:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i_naive.py
def backtrack(\n    state: list[int],\n    target: int,\n    total: int,\n    choices: list[int],\n    res: list[list[int]],\n):\n    \"\"\"Backtracking algorithm: Subset sum I\"\"\"\n    # When the subset sum equals target, record the solution\n    if total == target:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    for i in range(len(choices)):\n        # Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target:\n            continue\n        # Attempt: make choice, update element sum total\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum I (including duplicate subsets)\"\"\"\n    state = []  # State (subset)\n    total = 0  # Subset sum\n    res = []  # Result list (subset list)\n    backtrack(state, target, total, nums, res)\n    return res\n
subset_sum_i_naive.cpp
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    for (size_t i = 0; i < choices.size(); i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nvector<vector<int>> subsetSumINaive(vector<int> &nums, int target) {\n    vector<int> state;       // State (subset)\n    int total = 0;           // Subset sum\n    vector<vector<int>> res; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.java
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<Integer>> subsetSumINaive(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    int total = 0; // Subset sum\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.cs
/* Backtracking algorithm: Subset sum I */\nvoid Backtrack(List<int> state, int target, int total, int[] choices, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choices.Length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<int>> SubsetSumINaive(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    int total = 0; // Subset sum\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.go
/* Backtracking algorithm: Subset sum I */\nfunc backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == total {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    for i := 0; i < len(*choices); i++ {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total+(*choices)[i] > target {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunc subsetSumINaive(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    total := 0              // Subset sum\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumINaive(total, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_i_naive.swift
/* Backtracking algorithm: Subset sum I */\nfunc backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if total == target {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    for i in choices.indices {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunc subsetSumINaive(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let total = 0 // Subset sum\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, total: total, choices: nums, res: &res)\n    return res\n}\n
subset_sum_i_naive.js
/* Backtracking algorithm: Subset sum I */\nfunction backtrack(state, target, total, choices, res) {\n    // When the subset sum equals target, record the solution\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    for (let i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunction subsetSumINaive(nums, target) {\n    const state = []; // State (subset)\n    const total = 0; // Subset sum\n    const res = []; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.ts
/* Backtracking algorithm: Subset sum I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    total: number,\n    choices: number[],\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    for (let i = 0; i < choices.length; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfunction subsetSumINaive(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    const total = 0; // Subset sum\n    const res = []; // Result list (subset list)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.dart
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  int total,\n  List<int> choices,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (total == target) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  for (int i = 0; i < choices.length; i++) {\n    // Pruning: if the subset sum exceeds target, skip this choice\n    if (total + choices[i] > target) {\n      continue;\n    }\n    // Attempt: make choice, update element sum total\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target, total + choices[i], choices, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nList<List<int>> subsetSumINaive(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  int total = 0; // Sum of elements\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, total, nums, res);\n  return res;\n}\n
subset_sum_i_naive.rs
/* Backtracking algorithm: Subset sum I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    total: i32,\n    choices: &[i32],\n    res: &mut Vec<Vec<i32>>,\n) {\n    // When the subset sum equals target, record the solution\n    if total == target {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    for i in 0..choices.len() {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if total + choices[i] > target {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    let total = 0; // Subset sum\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, total, nums, &mut res);\n    res\n}\n
subset_sum_i_naive.c
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(int target, int total, int *choices, int choicesSize) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Traverse all choices\n    for (int i = 0; i < choicesSize; i++) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // Attempt: make choice, update element sum total\n        state[stateSize++] = choices[i];\n        // Proceed to the next round of selection\n        backtrack(target, total + choices[i], choices, choicesSize);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nvoid subsetSumINaive(int *nums, int numsSize, int target) {\n    resSize = 0; // Initialize solution count to 0\n    backtrack(target, 0, nums, numsSize);\n}\n
subset_sum_i_naive.kt
/* Backtracking algorithm: Subset sum I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    total: Int,\n    choices: IntArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (total == target) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    for (i in choices.indices) {\n        // Pruning: if the subset sum exceeds target, skip this choice\n        if (total + choices[i] > target) {\n            continue\n        }\n        // Attempt: make choice, update element sum total\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target, total + choices[i], choices, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum I (including duplicate subsets) */\nfun subsetSumINaive(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    val total = 0 // Subset sum\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, total, nums, res)\n    return res\n}\n
subset_sum_i_naive.rb
### Backtracking: subset sum I ###\ndef backtrack(state, target, total, choices, res)\n  # When the subset sum equals target, record the solution\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  for i in 0...choices.length\n    # Pruning: if the subset sum exceeds target, skip this choice\n    next if total + choices[i] > target\n    # Attempt: make choice, update element sum total\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target, total + choices[i], choices, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum I (with duplicate subsets) ###\ndef subset_sum_i_naive(nums, target)\n  state = [] # State (subset)\n  total = 0 # Subset sum\n  res = [] # Result list (subset list)\n  backtrack(state, target, total, nums, res)\n  res\nend\n

When we input array \\([3, 4, 5]\\) and target element \\(9\\) to the above code, the output is \\([3, 3, 3], [4, 5], [5, 4]\\). Although we successfully find all subsets that sum to \\(9\\), there are duplicate subsets \\([4, 5]\\) and \\([5, 4]\\).

This is because the search process distinguishes the order of selections, but subsets do not distinguish selection order. As shown in Figure 13-10, selecting 4 first and then 5 versus selecting 5 first and then 4 are different branches, but they correspond to the same subset.

Figure 13-10   Subset search and boundary pruning

To eliminate duplicate subsets, one straightforward idea is to deduplicate the result list. However, this approach is very inefficient for two reasons:

  • When there are many array elements, especially when target is large, the search process generates many duplicate subsets.
  • Comparing subsets (arrays) is very time-consuming, requiring sorting the arrays first, then comparing each element in them.
","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2-pruning-duplicate-subsets","level":3,"title":"2.   Pruning Duplicate Subsets","text":"

We consider deduplication through pruning during the search process. Observing Figure 13-11, duplicate subsets occur when array elements are selected in different orders, as in the following cases:

  1. When the first and second rounds select \\(3\\) and \\(4\\) respectively, all subsets containing these two elements are generated, denoted as \\([3, 4, \\dots]\\).
  2. Afterward, when the first round selects \\(4\\), the second round should skip \\(3\\), because the subset \\([4, 3, \\dots]\\) generated by this choice is completely duplicate with the subset generated in step 1.

In the search process, each level's choices are tried from left to right, so the rightmost branches are pruned more.

  1. The first two rounds select \\(3\\) and \\(5\\), generating subset \\([3, 5, \\dots]\\).
  2. The first two rounds select \\(4\\) and \\(5\\), generating subset \\([4, 5, \\dots]\\).
  3. If the first round selects \\(5\\), the second round should skip \\(3\\) and \\(4\\), because subsets \\([5, 3, \\dots]\\) and \\([5, 4, \\dots]\\) are completely duplicate with the subsets described in steps 1. and 2.

Figure 13-11   Different selection orders leading to duplicate subsets

In summary, given an input array \\([x_1, x_2, \\dots, x_n]\\), let the selection sequence in the search process be \\([x_{i_1}, x_{i_2}, \\dots, x_{i_m}]\\). This selection sequence must satisfy \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\); any selection sequence that does not satisfy this condition will cause duplicates and should be pruned.

","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

To implement this pruning, we initialize a variable start to indicate the starting point of traversal. After making choice \\(x_{i}\\), set the next round to start traversal from index \\(i\\). This ensures that the selection sequence satisfies \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\), guaranteeing subset uniqueness.

In addition, we have made the following two optimizations to the code:

  • Before starting the search, first sort the array nums. When traversing all choices, end the loop immediately when the subset sum exceeds target, because subsequent elements are larger, and their subset sums must exceed target.
  • Omit the element sum variable total and use subtraction on target to track the sum of elements. Record the solution when target equals \\(0\\).
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i.py
def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Subset sum I\"\"\"\n    # When the subset sum equals target, record the solution\n    if target == 0:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    # Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in range(start, len(choices)):\n        # Pruning 1: if the subset sum exceeds target, end the loop directly\n        # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0:\n            break\n        # Attempt: make choice, update target, start\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_i(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum I\"\"\"\n    state = []  # State (subset)\n    nums.sort()  # Sort nums\n    start = 0  # Start point for traversal\n    res = []  # Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n
subset_sum_i.cpp
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.size(); i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum I */\nvector<vector<int>> subsetSumI(vector<int> &nums, int target) {\n    vector<int> state;              // State (subset)\n    sort(nums.begin(), nums.end()); // Sort nums\n    int start = 0;                  // Start point for traversal\n    vector<vector<int>> res;        // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.java
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum I */\nList<List<Integer>> subsetSumI(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    Arrays.sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.cs
/* Backtracking algorithm: Subset sum I */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choices.Length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum I */\nList<List<int>> SubsetSumI(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    Array.Sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.go
/* Backtracking algorithm: Subset sum I */\nfunc backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i := start; i < len(*choices); i++ {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Attempt: make choice, update target, start\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum I */\nfunc subsetSumI(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    sort.Ints(nums)         // Sort nums\n    start := 0              // Start point for traversal\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumI(start, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_i.swift
/* Backtracking algorithm: Subset sum I */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in choices.indices.dropFirst(start) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Attempt: make choice, update target, start\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum I */\nfunc subsetSumI(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let nums = nums.sorted() // Sort nums\n    let start = 0 // Start point for traversal\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
subset_sum_i.js
/* Backtracking algorithm: Subset sum I */\nfunction backtrack(state, target, choices, start, res) {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfunction subsetSumI(nums, target) {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.ts
/* Backtracking algorithm: Subset sum I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfunction subsetSumI(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.dart
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  // Pruning 2: start traversing from start to avoid generating duplicate subsets\n  for (int i = start; i < choices.length; i++) {\n    // Pruning 1: if the subset sum exceeds target, end the loop directly\n    // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Attempt: make choice, update target, start\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum I */\nList<List<int>> subsetSumI(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  nums.sort(); // Sort nums\n  int start = 0; // Start point for traversal\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
subset_sum_i.rs
/* Backtracking algorithm: Subset sum I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for i in start..choices.len() {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum I */\nfn subset_sum_i(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    nums.sort(); // Sort nums\n    let start = 0; // Start point for traversal\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
subset_sum_i.c
/* Backtracking algorithm: Subset sum I */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        for (int i = 0; i < stateSize; ++i) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (int i = start; i < choicesSize; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Attempt: make choice, update target, start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Proceed to the next round of selection\n        backtrack(target - choices[i], choices, choicesSize, i);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum I */\nvoid subsetSumI(int *nums, int numsSize, int target) {\n    qsort(nums, numsSize, sizeof(int), cmp); // Sort nums\n    int start = 0;                           // Start point for traversal\n    backtrack(target, nums, numsSize, start);\n}\n
subset_sum_i.kt
/* Backtracking algorithm: Subset sum I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    for (i in start..<choices.size) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum I */\nfun subsetSumI(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    nums.sort() // Sort nums\n    val start = 0 // Start point for traversal\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
subset_sum_i.rb
### Backtracking: subset sum I ###\ndef backtrack(state, target, choices, start, res)\n  # When the subset sum equals target, record the solution\n  if target.zero?\n    res << state.dup\n    return\n  end\n  # Traverse all choices\n  # Pruning 2: start traversing from start to avoid generating duplicate subsets\n  for i in start...choices.length\n    # Pruning 1: if the subset sum exceeds target, end the loop directly\n    # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    break if target - choices[i] < 0\n    # Attempt: make choice, update target, start\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum I ###\ndef subset_sum_i(nums, target)\n  state = [] # State (subset)\n  nums.sort! # Sort nums\n  start = 0 # Start point for traversal\n  res = [] # Result list (subset list)\n  backtrack(state, target, nums, start, res)\n  res\nend\n

Figure 13-12 shows the complete backtracking process when array \\([3, 4, 5]\\) and target element \\(9\\) are input to the above code.

Figure 13-12   Subset-sum I backtracking process

","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1332-with-duplicate-elements-in-array","level":2,"title":"13.3.2   With Duplicate Elements in Array","text":"

Question

Given a positive integer array nums and a target positive integer target, find all possible combinations where the sum of elements in the combination equals target. The given array may contain duplicate elements, and each element can be selected at most once. Return these combinations in list form, where the list should not contain duplicate combinations.

Compared to the previous problem, the input array in this problem may contain duplicate elements, which introduces new challenges. For example, given array \\([4, \\hat{4}, 5]\\) and target element \\(9\\), the output of the existing code is \\([4, 5], [\\hat{4}, 5]\\), which contains duplicate subsets.

The reason for this duplication is that equal elements are selected multiple times in a certain round. In Figure 13-13, the first round has three choices, two of which are \\(4\\), creating two duplicate search branches that output duplicate subsets. Similarly, the two \\(4\\)'s in the second round also produce duplicate subsets.

Figure 13-13   Duplicate subsets caused by equal elements

","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1-pruning-equal-elements","level":3,"title":"1.   Pruning Equal Elements","text":"

To solve this problem, we need to limit equal elements to be selected only once in each round. The implementation is quite clever: since the array is already sorted, equal elements are adjacent. This means that in a certain round of selection, if the current element equals the element to its left, it means this element has already been selected, so we skip the current element directly.

At the same time, this problem specifies that each array element can only be selected once. Fortunately, we can also use the variable start to satisfy this constraint: after making choice \\(x_{i}\\), set the next round to start traversal from index \\(i + 1\\) onwards. This both eliminates duplicate subsets and avoids selecting elements multiple times.

","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_ii.py
def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"Backtracking algorithm: Subset sum II\"\"\"\n    # When the subset sum equals target, record the solution\n    if target == 0:\n        res.append(list(state))\n        return\n    # Traverse all choices\n    # Pruning 2: start traversing from start to avoid generating duplicate subsets\n    # Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in range(start, len(choices)):\n        # Pruning 1: if the subset sum exceeds target, end the loop directly\n        # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0:\n            break\n        # Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start and choices[i] == choices[i - 1]:\n            continue\n        # Attempt: make choice, update target, start\n        state.append(choices[i])\n        # Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        # Backtrack: undo choice, restore to previous state\n        state.pop()\n\ndef subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"Solve subset sum II\"\"\"\n    state = []  # State (subset)\n    nums.sort()  # Sort nums\n    start = 0  # Start point for traversal\n    res = []  # Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n
subset_sum_ii.cpp
/* Backtracking algorithm: Subset sum II */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.size(); i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push_back(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop_back();\n    }\n}\n\n/* Solve subset sum II */\nvector<vector<int>> subsetSumII(vector<int> &nums, int target) {\n    vector<int> state;              // State (subset)\n    sort(nums.begin(), nums.end()); // Sort nums\n    int start = 0;                  // Start point for traversal\n    vector<vector<int>> res;        // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.java
/* Backtracking algorithm: Subset sum II */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.remove(state.size() - 1);\n    }\n}\n\n/* Solve subset sum II */\nList<List<Integer>> subsetSumII(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // State (subset)\n    Arrays.sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<Integer>> res = new ArrayList<>(); // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.cs
/* Backtracking algorithm: Subset sum II */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choices.Length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.Add(choices[i]);\n        // Proceed to the next round of selection\n        Backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* Solve subset sum II */\nList<List<int>> SubsetSumII(int[] nums, int target) {\n    List<int> state = []; // State (subset)\n    Array.Sort(nums); // Sort nums\n    int start = 0; // Start point for traversal\n    List<List<int>> res = []; // Result list (subset list)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.go
/* Backtracking algorithm: Subset sum II */\nfunc backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i := start; i < len(*choices); i++ {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start && (*choices)[i] == (*choices)[i-1] {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        *state = append(*state, (*choices)[i])\n        // Proceed to the next round of selection\n        backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res)\n        // Backtrack: undo choice, restore to previous state\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* Solve subset sum II */\nfunc subsetSumII(nums []int, target int) [][]int {\n    state := make([]int, 0) // State (subset)\n    sort.Ints(nums)         // Sort nums\n    start := 0              // Start point for traversal\n    res := make([][]int, 0) // Result list (subset list)\n    backtrackSubsetSumII(start, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_ii.swift
/* Backtracking algorithm: Subset sum II */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in choices.indices.dropFirst(start) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start, choices[i] == choices[i - 1] {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        state.append(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeLast()\n    }\n}\n\n/* Solve subset sum II */\nfunc subsetSumII(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // State (subset)\n    let nums = nums.sorted() // Sort nums\n    let start = 0 // Start point for traversal\n    var res: [[Int]] = [] // Result list (subset list)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
subset_sum_ii.js
/* Backtracking algorithm: Subset sum II */\nfunction backtrack(state, target, choices, start, res) {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfunction subsetSumII(nums, target) {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.ts
/* Backtracking algorithm: Subset sum II */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // When the subset sum equals target, record the solution\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (let i = start; i < choices.length; i++) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfunction subsetSumII(nums: number[], target: number): number[][] {\n    const state = []; // State (subset)\n    nums.sort((a, b) => a - b); // Sort nums\n    const start = 0; // Start point for traversal\n    const res = []; // Result list (subset list)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.dart
/* Backtracking algorithm: Subset sum II */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // When the subset sum equals target, record the solution\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // Traverse all choices\n  // Pruning 2: start traversing from start to avoid generating duplicate subsets\n  // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n  for (int i = start; i < choices.length; i++) {\n    // Pruning 1: if the subset sum exceeds target, end the loop directly\n    // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n    if (i > start && choices[i] == choices[i - 1]) {\n      continue;\n    }\n    // Attempt: make choice, update target, start\n    state.add(choices[i]);\n    // Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i + 1, res);\n    // Backtrack: undo choice, restore to previous state\n    state.removeLast();\n  }\n}\n\n/* Solve subset sum II */\nList<List<int>> subsetSumII(List<int> nums, int target) {\n  List<int> state = []; // State (subset)\n  nums.sort(); // Sort nums\n  int start = 0; // Start point for traversal\n  List<List<int>> res = []; // Result list (subset list)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
subset_sum_ii.rs
/* Backtracking algorithm: Subset sum II */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // When the subset sum equals target, record the solution\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for i in start..choices.len() {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if i > start && choices[i] == choices[i - 1] {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state.push(choices[i]);\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // Backtrack: undo choice, restore to previous state\n        state.pop();\n    }\n}\n\n/* Solve subset sum II */\nfn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // State (subset)\n    nums.sort(); // Sort nums\n    let start = 0; // Start point for traversal\n    let mut res = Vec::new(); // Result list (subset list)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
subset_sum_ii.c
/* Backtracking algorithm: Subset sum II */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (int i = start; i < choicesSize; i++) {\n        // Pruning 1: Skip if subset sum exceeds target\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // Attempt: make choice, update target, start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // Proceed to the next round of selection\n        backtrack(target - choices[i], choices, choicesSize, i + 1);\n        // Backtrack: undo choice, restore to previous state\n        stateSize--;\n    }\n}\n\n/* Solve subset sum II */\nvoid subsetSumII(int *nums, int numsSize, int target) {\n    // Sort nums\n    qsort(nums, numsSize, sizeof(int), cmp);\n    // Start backtracking\n    backtrack(target, nums, numsSize, 0);\n}\n
subset_sum_ii.kt
/* Backtracking algorithm: Subset sum II */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // When the subset sum equals target, record the solution\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // Traverse all choices\n    // Pruning 2: start traversing from start to avoid generating duplicate subsets\n    // Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n    for (i in start..<choices.size) {\n        // Pruning 1: if the subset sum exceeds target, end the loop directly\n        // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue\n        }\n        // Attempt: make choice, update target, start\n        state.add(choices[i])\n        // Proceed to the next round of selection\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        // Backtrack: undo choice, restore to previous state\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* Solve subset sum II */\nfun subsetSumII(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // State (subset)\n    nums.sort() // Sort nums\n    val start = 0 // Start point for traversal\n    val res = mutableListOf<MutableList<Int>?>() // Result list (subset list)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
subset_sum_ii.rb
### Backtracking: subset sum II ###\ndef backtrack(state, target, choices, start, res)\n  # When the subset sum equals target, record the solution\n  if target.zero?\n    res << state.dup\n    return\n  end\n\n  # Traverse all choices\n  # Pruning 2: start traversing from start to avoid generating duplicate subsets\n  # Pruning 3: start traversing from start to avoid repeatedly selecting the same element\n  for i in start...choices.length\n    # Pruning 1: if the subset sum exceeds target, end the loop directly\n    # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target\n    break if target - choices[i] < 0\n    # Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly\n    next if i > start && choices[i] == choices[i - 1]\n    # Attempt: make choice, update target, start\n    state << choices[i]\n    # Proceed to the next round of selection\n    backtrack(state, target - choices[i], choices, i + 1, res)\n    # Backtrack: undo choice, restore to previous state\n    state.pop\n  end\nend\n\n### Solve subset sum II ###\ndef subset_sum_ii(nums, target)\n  state = [] # State (subset)\n  nums.sort! # Sort nums\n  start = 0 # Start point for traversal\n  res = [] # Result list (subset list)\n  backtrack(state, target, nums, start, res)\n  res\nend\n

Figure 13-14 shows the backtracking process for array \\([4, 4, 5]\\) and target element \\(9\\), which includes four types of pruning operations. Combine the illustration with the code comments to understand the entire search process and how each pruning operation works.

Figure 13-14   Subset-sum II backtracking process

","path":["Chapter 13. Backtracking","13.3   Subset-Sum Problem"],"tags":[]},{"location":"chapter_backtracking/summary/","level":1,"title":"13.5   Summary","text":"","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_backtracking/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • The backtracking algorithm is fundamentally an exhaustive search method. It finds solutions that meet specified conditions by performing a depth-first traversal of the solution space. During the search process, when a solution satisfying the conditions is found, it is recorded. The search ends either after finding all solutions or when the traversal is complete.
  • The backtracking algorithm search process consists of two parts: attempting and backtracking. It tries various choices through depth-first search. When encountering situations that violate constraints, it reverts the previous choice, returns to the previous state, and continues exploring other options. Attempting and backtracking are operations in opposite directions.
  • Backtracking problems typically contain multiple constraints, which can be utilized to implement pruning operations. Pruning can terminate unnecessary search branches early, significantly improving search efficiency.
  • The backtracking algorithm is primarily used to solve search problems and constraint satisfaction problems. While combinatorial optimization problems can be solved with backtracking, there are often more efficient or better-performing solutions available.
  • The permutation problem aims to find all possible permutations of elements in a given set. We use an array to record whether each element has been selected, thereby pruning search branches that attempt to select the same element repeatedly, ensuring each element is selected exactly once.
  • In the permutation problem, if the set contains duplicate elements, the final result will contain duplicate permutations. We need to impose a constraint so that equal elements can only be selected once per round, which is typically achieved using a hash set.
  • The subset-sum problem aims to find all subsets of a given set that sum to a target value. Since the set is unordered but the search process outputs results in all orders, duplicate subsets are generated. We sort the data before backtracking and use a variable to indicate the starting point of each round's traversal, thereby pruning search branches that generate duplicate subsets.
  • For the subset-sum problem, equal elements in the array produce duplicate sets. We leverage the precondition that the array is sorted by checking whether adjacent elements are equal to implement pruning, ensuring that equal elements can only be selected once per round.
  • The \\(n\\) queens problem aims to find placements of \\(n\\) queens on an \\(n \\times n\\) chessboard such that no two queens can attack each other. The constraints of this problem include row constraints, column constraints, and main and anti-diagonal constraints. To satisfy row constraints, we adopt a row-by-row placement strategy, ensuring exactly one queen is placed in each row.
  • The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether each column has a queen, thereby indicating whether a selected cell is valid. For diagonal constraints, we use two arrays to separately record whether queens exist on each main or anti-diagonal. The challenge lies in finding the row-column index pattern that characterizes cells on the same main (anti-)diagonal.
","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_backtracking/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: How should we understand the relationship between backtracking and recursion?

Overall, backtracking is an \"algorithm strategy\", while recursion is more like a \"tool\".

  • The backtracking algorithm is typically implemented based on recursion. However, backtracking is one application scenario of recursion and represents the application of recursion in search problems.
  • The structure of recursion embodies the \"subproblem decomposition\" problem-solving paradigm, commonly used to solve problems involving divide-and-conquer, backtracking, and dynamic programming (memoized recursion).
","path":["Chapter 13. Backtracking","13.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"Chapter 2.   Complexity Analysis","text":"

Abstract

Complexity analysis is like a space-time guide in the vast universe of algorithms.

It leads us to explore deeply within the two dimensions of time and space, seeking more elegant solutions.

","path":["Chapter 2. Complexity Analysis","Chapter 2.   Complexity Analysis"],"tags":[]},{"location":"chapter_computational_complexity/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 2.1   Algorithm Efficiency Evaluation
  • 2.2   Iteration and Recursion
  • 2.3   Time Complexity
  • 2.4   Space Complexity
  • 2.5   Summary
","path":["Chapter 2. Complexity Analysis","Chapter 2.   Complexity Analysis"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   Iteration and Recursion","text":"

In algorithms, repeatedly executing a task is very common and closely related to complexity analysis. Therefore, before introducing time complexity and space complexity, let's first understand how to implement repeated task execution in programs, namely the two basic program control structures: iteration and recursion.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#221-iteration","level":2,"title":"2.2.1   Iteration","text":"

Iteration is a control structure for repeatedly executing a task. In iteration, a program repeatedly executes a segment of code under certain conditions until those conditions are no longer satisfied.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-for-loop","level":3,"title":"1.   For Loop","text":"

The for loop is one of the most common forms of iteration, suitable for use when the number of iterations is known in advance.

The following function implements the summation \\(1 + 2 + \\dots + n\\) based on a for loop, with the sum result recorded using the variable res. Note that in Python, range(a, b) corresponds to a \"left-closed, right-open\" interval, with the traversal range being \\(a, a + 1, \\dots, b-1\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def for_loop(n: int) -> int:\n    \"\"\"for loop\"\"\"\n    res = 0\n    # Sum 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        res += i\n    return res\n
iteration.cpp
/* for loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        res += i;\n    }\n    return res;\n}\n
iteration.java
/* for loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.cs
/* for loop */\nint ForLoop(int n) {\n    int res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.go
/* for loop */\nfunc forLoop(n int) int {\n    res := 0\n    // Sum 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        res += i\n    }\n    return res\n}\n
iteration.swift
/* for loop */\nfunc forLoop(n: Int) -> Int {\n    var res = 0\n    // Sum 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        res += i\n    }\n    return res\n}\n
iteration.js
/* for loop */\nfunction forLoop(n) {\n    let res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.ts
/* for loop */\nfunction forLoop(n: number): number {\n    let res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.dart
/* for loop */\nint forLoop(int n) {\n  int res = 0;\n  // Sum 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    res += i;\n  }\n  return res;\n}\n
iteration.rs
/* for loop */\nfn for_loop(n: i32) -> i32 {\n    let mut res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for i in 1..=n {\n        res += i;\n    }\n    res\n}\n
iteration.c
/* for loop */\nint forLoop(int n) {\n    int res = 0;\n    // Sum 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.kt
/* for loop */\nfun forLoop(n: Int): Int {\n    var res = 0\n    // Sum 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        res += i\n    }\n    return res\n}\n
iteration.rb
### for loop ###\ndef for_loop(n)\n  res = 0\n\n  # Sum 1, 2, ..., n-1, n\n  for i in 1..n\n    res += i\n  end\n\n  res\nend\n

Figure 2-1 shows the flowchart of this summation function.

Figure 2-1   Flowchart of the summation function

The number of operations in this summation function is proportional to the input data size \\(n\\), or has a \"linear relationship\". In fact, time complexity describes precisely this \"linear relationship\". Related content will be introduced in detail in the next section.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-while-loop","level":3,"title":"2.   While Loop","text":"

Similar to the for loop, the while loop is also a method for implementing iteration. In a while loop, the program first checks the condition in each round; if the condition is true, it continues execution, otherwise it ends the loop.

Below we use a while loop to implement the summation \\(1 + 2 + \\dots + n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def while_loop(n: int) -> int:\n    \"\"\"while loop\"\"\"\n    res = 0\n    i = 1  # Initialize condition variable\n    # Sum 1, 2, ..., n-1, n\n    while i <= n:\n        res += i\n        i += 1  # Update condition variable\n    return res\n
iteration.cpp
/* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
iteration.java
/* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
iteration.cs
/* while loop */\nint WhileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i += 1; // Update condition variable\n    }\n    return res;\n}\n
iteration.go
/* while loop */\nfunc whileLoop(n int) int {\n    res := 0\n    // Initialize condition variable\n    i := 1\n    // Sum 1, 2, ..., n-1, n\n    for i <= n {\n        res += i\n        // Update condition variable\n        i++\n    }\n    return res\n}\n
iteration.swift
/* while loop */\nfunc whileLoop(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while i <= n {\n        res += i\n        i += 1 // Update condition variable\n    }\n    return res\n}\n
iteration.js
/* while loop */\nfunction whileLoop(n) {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
iteration.ts
/* while loop */\nfunction whileLoop(n: number): number {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
iteration.dart
/* while loop */\nint whileLoop(int n) {\n  int res = 0;\n  int i = 1; // Initialize condition variable\n  // Sum 1, 2, ..., n-1, n\n  while (i <= n) {\n    res += i;\n    i++; // Update condition variable\n  }\n  return res;\n}\n
iteration.rs
/* while loop */\nfn while_loop(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Initialize condition variable\n\n    // Sum 1, 2, ..., n-1, n\n    while i <= n {\n        res += i;\n        i += 1; // Update condition variable\n    }\n    res\n}\n
iteration.c
/* while loop */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // Update condition variable\n    }\n    return res;\n}\n
iteration.kt
/* while loop */\nfun whileLoop(n: Int): Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i\n        i++ // Update condition variable\n    }\n    return res\n}\n
iteration.rb
### while loop ###\ndef while_loop(n)\n  res = 0\n  i = 1 # Initialize condition variable\n\n  # Sum 1, 2, ..., n-1, n\n  while i <= n\n    res += i\n    i += 1 # Update condition variable\n  end\n\n  res\nend\n

The while loop has greater flexibility than the for loop. In a while loop, we can freely design the initialization and update steps of the condition variable.

For example, in the following code, the condition variable \\(i\\) is updated twice per round, which is not convenient to implement using a for loop:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def while_loop_ii(n: int) -> int:\n    \"\"\"while loop (two updates)\"\"\"\n    res = 0\n    i = 1  # Initialize condition variable\n    # Sum 1, 4, 10, ...\n    while i <= n:\n        res += i\n        # Update condition variable\n        i += 1\n        i *= 2\n    return res\n
iteration.cpp
/* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.java
/* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.cs
/* while loop (two updates) */\nint WhileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i += 1; \n        i *= 2;\n    }\n    return res;\n}\n
iteration.go
/* while loop (two updates) */\nfunc whileLoopII(n int) int {\n    res := 0\n    // Initialize condition variable\n    i := 1\n    // Sum 1, 4, 10, ...\n    for i <= n {\n        res += i\n        // Update condition variable\n        i++\n        i *= 2\n    }\n    return res\n}\n
iteration.swift
/* while loop (two updates) */\nfunc whileLoopII(n: Int) -> Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while i <= n {\n        res += i\n        // Update condition variable\n        i += 1\n        i *= 2\n    }\n    return res\n}\n
iteration.js
/* while loop (two updates) */\nfunction whileLoopII(n) {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.ts
/* while loop (two updates) */\nfunction whileLoopII(n: number): number {\n    let res = 0;\n    let i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.dart
/* while loop (two updates) */\nint whileLoopII(int n) {\n  int res = 0;\n  int i = 1; // Initialize condition variable\n  // Sum 1, 4, 10, ...\n  while (i <= n) {\n    res += i;\n    // Update condition variable\n    i++;\n    i *= 2;\n  }\n  return res;\n}\n
iteration.rs
/* while loop (two updates) */\nfn while_loop_ii(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // Initialize condition variable\n\n    // Sum 1, 4, 10, ...\n    while i <= n {\n        res += i;\n        // Update condition variable\n        i += 1;\n        i *= 2;\n    }\n    res\n}\n
iteration.c
/* while loop (two updates) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // Update condition variable\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.kt
/* while loop (two updates) */\nfun whileLoopII(n: Int): Int {\n    var res = 0\n    var i = 1 // Initialize condition variable\n    // Sum 1, 4, 10, ...\n    while (i <= n) {\n        res += i\n        // Update condition variable\n        i++\n        i *= 2\n    }\n    return res\n}\n
iteration.rb
### while loop (two updates) ###\ndef while_loop_ii(n)\n  res = 0\n  i = 1 # Initialize condition variable\n\n  # Sum 1, 4, 10, ...\n  while i <= n\n    res += i\n    # Update condition variable\n    i += 1\n    i *= 2\n  end\n\n  res\nend\n

Overall, for loops have more compact code, while while loops are more flexible; both can implement iterative structures. The choice of which to use should be determined based on the requirements of the specific problem.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3-nested-loops","level":3,"title":"3.   Nested Loops","text":"

We can nest one loop structure inside another. Below is an example using for loops:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def nested_for_loop(n: int) -> str:\n    \"\"\"Nested for loop\"\"\"\n    res = \"\"\n    # Loop i = 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        # Loop j = 1, 2, ..., n-1, n\n        for j in range(1, n + 1):\n            res += f\"({i}, {j}), \"\n    return res\n
iteration.cpp
/* Nested for loop */\nstring nestedForLoop(int n) {\n    ostringstream res;\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; ++j) {\n            res << \"(\" << i << \", \" << j << \"), \";\n        }\n    }\n    return res.str();\n}\n
iteration.java
/* Nested for loop */\nString nestedForLoop(int n) {\n    StringBuilder res = new StringBuilder();\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            res.append(\"(\" + i + \", \" + j + \"), \");\n        }\n    }\n    return res.toString();\n}\n
iteration.cs
/* Nested for loop */\nstring NestedForLoop(int n) {\n    StringBuilder res = new();\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            res.Append($\"({i}, {j}), \");\n        }\n    }\n    return res.ToString();\n}\n
iteration.go
/* Nested for loop */\nfunc nestedForLoop(n int) string {\n    res := \"\"\n    // Loop i = 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= n; j++ {\n            // Loop j = 1, 2, ..., n-1, n\n            res += fmt.Sprintf(\"(%d, %d), \", i, j)\n        }\n    }\n    return res\n}\n
iteration.swift
/* Nested for loop */\nfunc nestedForLoop(n: Int) -> String {\n    var res = \"\"\n    // Loop i = 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        // Loop j = 1, 2, ..., n-1, n\n        for j in 1 ... n {\n            res.append(\"(\\(i), \\(j)), \")\n        }\n    }\n    return res\n}\n
iteration.js
/* Nested for loop */\nfunction nestedForLoop(n) {\n    let res = '';\n    // Loop i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
iteration.ts
/* Nested for loop */\nfunction nestedForLoop(n: number): string {\n    let res = '';\n    // Loop i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
iteration.dart
/* Nested for loop */\nString nestedForLoop(int n) {\n  String res = \"\";\n  // Loop i = 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    // Loop j = 1, 2, ..., n-1, n\n    for (int j = 1; j <= n; j++) {\n      res += \"($i, $j), \";\n    }\n  }\n  return res;\n}\n
iteration.rs
/* Nested for loop */\nfn nested_for_loop(n: i32) -> String {\n    let mut res = vec![];\n    // Loop i = 1, 2, ..., n-1, n\n    for i in 1..=n {\n        // Loop j = 1, 2, ..., n-1, n\n        for j in 1..=n {\n            res.push(format!(\"({}, {}), \", i, j));\n        }\n    }\n    res.join(\"\")\n}\n
iteration.c
/* Nested for loop */\nchar *nestedForLoop(int n) {\n    // n * n is the number of points, \"(i, j), \" string max length is 6+10*2, plus extra space for null character \\0\n    int size = n * n * 26 + 1;\n    char *res = malloc(size * sizeof(char));\n    // Loop i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            char tmp[26];\n            snprintf(tmp, sizeof(tmp), \"(%d, %d), \", i, j);\n            strncat(res, tmp, size - strlen(res) - 1);\n        }\n    }\n    return res;\n}\n
iteration.kt
/* Nested for loop */\nfun nestedForLoop(n: Int): String {\n    val res = StringBuilder()\n    // Loop i = 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        // Loop j = 1, 2, ..., n-1, n\n        for (j in 1..n) {\n            res.append(\" ($i, $j), \")\n        }\n    }\n    return res.toString()\n}\n
iteration.rb
### Nested for loop ###\ndef nested_for_loop(n)\n  res = \"\"\n\n  # Loop i = 1, 2, ..., n-1, n\n  for i in 1..n\n    # Loop j = 1, 2, ..., n-1, n\n    for j in 1..n\n      res += \"(#{i}, #{j}), \"\n    end\n  end\n\n  res\nend\n

Figure 2-2 shows the flowchart of this nested loop.

Figure 2-2   Flowchart of nested loops

In this case, the number of operations of the function is proportional to \\(n^2\\), or the algorithm's running time has a \"quadratic relationship\" with the input data size \\(n\\).

We can continue adding nested loops, where each nesting is a \"dimension increase\", raising the time complexity to \"cubic relationship\", \"quartic relationship\", and so on.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222-recursion","level":2,"title":"2.2.2   Recursion","text":"

Recursion is an algorithmic strategy that solves problems by having a function call itself. It mainly consists of two phases.

  1. Descend: The program continuously calls itself deeper, usually passing in smaller or more simplified parameters, until reaching a \"termination condition\".
  2. Ascend: After triggering the \"termination condition\", the program returns layer by layer from the deepest recursive function, aggregating the result of each layer.

From an implementation perspective, recursive code mainly consists of three elements.

  1. Termination condition: Used to determine when to switch from \"descending\" to \"ascending\".
  2. Recursive call: Corresponds to \"descending\", where the function calls itself, usually with smaller or more simplified parameters.
  3. Return result: Corresponds to \"ascending\", returning the result of the current recursion level to the previous layer.

Observe the following code. We only need to call the function recur(n) to complete the calculation of \\(1 + 2 + \\dots + n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def recur(n: int) -> int:\n    \"\"\"Recursion\"\"\"\n    # Termination condition\n    if n == 1:\n        return 1\n    # Recurse: recursive call\n    res = recur(n - 1)\n    # Return: return result\n    return n + res\n
recursion.cpp
/* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.java
/* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.cs
/* Recursion */\nint Recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = Recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.go
/* Recursion */\nfunc recur(n int) int {\n    // Termination condition\n    if n == 1 {\n        return 1\n    }\n    // Recurse: recursive call\n    res := recur(n - 1)\n    // Return: return result\n    return n + res\n}\n
recursion.swift
/* Recursion */\nfunc recur(n: Int) -> Int {\n    // Termination condition\n    if n == 1 {\n        return 1\n    }\n    // Recurse: recursive call\n    let res = recur(n: n - 1)\n    // Return: return result\n    return n + res\n}\n
recursion.js
/* Recursion */\nfunction recur(n) {\n    // Termination condition\n    if (n === 1) return 1;\n    // Recurse: recursive call\n    const res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.ts
/* Recursion */\nfunction recur(n: number): number {\n    // Termination condition\n    if (n === 1) return 1;\n    // Recurse: recursive call\n    const res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.dart
/* Recursion */\nint recur(int n) {\n  // Termination condition\n  if (n == 1) return 1;\n  // Recurse: recursive call\n  int res = recur(n - 1);\n  // Return: return result\n  return n + res;\n}\n
recursion.rs
/* Recursion */\nfn recur(n: i32) -> i32 {\n    // Termination condition\n    if n == 1 {\n        return 1;\n    }\n    // Recurse: recursive call\n    let res = recur(n - 1);\n    // Return: return result\n    n + res\n}\n
recursion.c
/* Recursion */\nint recur(int n) {\n    // Termination condition\n    if (n == 1)\n        return 1;\n    // Recurse: recursive call\n    int res = recur(n - 1);\n    // Return: return result\n    return n + res;\n}\n
recursion.kt
/* Recursion */\nfun recur(n: Int): Int {\n    // Termination condition\n    if (n == 1)\n        return 1\n    // Descend: recursive call\n    val res = recur(n - 1)\n    // Return: return result\n    return n + res\n}\n
recursion.rb
### Recursion ###\ndef recur(n)\n  # Termination condition\n  return 1 if n == 1\n  # Recurse: recursive call\n  res = recur(n - 1)\n  # Return: return result\n  n + res\nend\n

Figure 2-3 shows the recursive process of this function.

Figure 2-3   Recursive process of the summation function

Although from a computational perspective, iteration and recursion can achieve the same results, they represent two completely different paradigms for thinking about and solving problems.

  • Iteration: Solves problems \"bottom-up\". Starting from the most basic steps, these steps are then repeatedly executed or accumulated until the task is complete.
  • Recursion: Solves problems \"top-down\". The original problem is decomposed into smaller subproblems that have the same form as the original problem. These subproblems continue to be decomposed into even smaller subproblems until reaching the base case (where the solution is known).

Taking the above summation function as an example, let the problem be \\(f(n) = 1 + 2 + \\dots + n\\).

  • Iteration: Simulates the summation process in a loop, traversing from \\(1\\) to \\(n\\), performing the summation operation in each round to obtain \\(f(n)\\).
  • Recursion: Decomposes the problem into the subproblem \\(f(n) = n + f(n-1)\\), continuously decomposing (recursively) until terminating at the base case \\(f(1) = 1\\).
","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-call-stack","level":3,"title":"1.   Call Stack","text":"

Each time a recursive function calls itself, the system allocates memory for the newly opened function to store local variables, call addresses, and other information. This leads to two consequences.

  • The function's context data is stored in a memory area called \"stack frame space\", which is not released until the function returns. Therefore, recursion usually consumes more memory space than iteration.
  • Recursive function calls incur additional overhead. Therefore, recursion is usually less time-efficient than loops.

As shown in Figure 2-4, before the termination condition is triggered, there are \\(n\\) unreturned recursive functions existing simultaneously, with a recursion depth of \\(n\\).

Figure 2-4   Recursion call depth

In practice, the recursion depth allowed by programming languages is usually limited, and excessively deep recursion may lead to stack overflow errors.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-tail-recursion","level":3,"title":"2.   Tail Recursion","text":"

Interestingly, if a function makes the recursive call as the very last step before returning, the function can be optimized by the compiler or interpreter to have space efficiency comparable to iteration. This case is called tail recursion.

  • Regular recursion: When a function returns to the previous level, it needs to continue executing code, so the system needs to save the context of the previous layer's call.
  • Tail recursion: The recursive call is the last operation before the function returns, meaning that after returning to the previous level, there is no need to continue executing other operations, so the system does not need to save the context of the previous layer's function.

Taking the calculation of \\(1 + 2 + \\dots + n\\) as an example, we can set the result variable res as a function parameter to implement tail recursion:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def tail_recur(n, res):\n    \"\"\"Tail recursion\"\"\"\n    # Termination condition\n    if n == 0:\n        return res\n    # Tail recursive call\n    return tail_recur(n - 1, res + n)\n
recursion.cpp
/* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
recursion.java
/* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
recursion.cs
/* Tail recursion */\nint TailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return TailRecur(n - 1, res + n);\n}\n
recursion.go
/* Tail recursion */\nfunc tailRecur(n int, res int) int {\n    // Termination condition\n    if n == 0 {\n        return res\n    }\n    // Tail recursive call\n    return tailRecur(n-1, res+n)\n}\n
recursion.swift
/* Tail recursion */\nfunc tailRecur(n: Int, res: Int) -> Int {\n    // Termination condition\n    if n == 0 {\n        return res\n    }\n    // Tail recursive call\n    return tailRecur(n: n - 1, res: res + n)\n}\n
recursion.js
/* Tail recursion */\nfunction tailRecur(n, res) {\n    // Termination condition\n    if (n === 0) return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
recursion.ts
/* Tail recursion */\nfunction tailRecur(n: number, res: number): number {\n    // Termination condition\n    if (n === 0) return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
recursion.dart
/* Tail recursion */\nint tailRecur(int n, int res) {\n  // Termination condition\n  if (n == 0) return res;\n  // Tail recursive call\n  return tailRecur(n - 1, res + n);\n}\n
recursion.rs
/* Tail recursion */\nfn tail_recur(n: i32, res: i32) -> i32 {\n    // Termination condition\n    if n == 0 {\n        return res;\n    }\n    // Tail recursive call\n    tail_recur(n - 1, res + n)\n}\n
recursion.c
/* Tail recursion */\nint tailRecur(int n, int res) {\n    // Termination condition\n    if (n == 0)\n        return res;\n    // Tail recursive call\n    return tailRecur(n - 1, res + n);\n}\n
recursion.kt
/* Tail recursion */\ntailrec fun tailRecur(n: Int, res: Int): Int {\n    // Add tailrec keyword to enable tail recursion optimization\n    // Termination condition\n    if (n == 0)\n        return res\n    // Tail recursive call\n    return tailRecur(n - 1, res + n)\n}\n
recursion.rb
### Tail recursion ###\ndef tail_recur(n, res)\n  # Termination condition\n  return res if n == 0\n  # Tail recursive call\n  tail_recur(n - 1, res + n)\nend\n

The execution process of tail recursion is shown in Figure 2-5. Comparing regular recursion and tail recursion, the execution point of the summation operation is different.

  • Regular recursion: The summation operation is performed during the \"ascending\" process, requiring an additional summation operation after each layer returns.
  • Tail recursion: The summation operation is performed during the \"descending\" process; the \"ascending\" process only needs to return layer by layer.

Figure 2-5   Tail recursion process

Tip

Please note that many compilers or interpreters do not support tail recursion optimization. For example, Python does not support tail recursion optimization by default, so even if a function is in tail recursive form, it may still encounter stack overflow issues.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3-recursion-tree","level":3,"title":"3.   Recursion Tree","text":"

When dealing with algorithmic problems related to \"divide and conquer\", recursion often provides a more intuitive approach and more readable code than iteration. Taking the \"Fibonacci sequence\" as an example.

Question

Given a Fibonacci sequence \\(0, 1, 1, 2, 3, 5, 8, 13, \\dots\\), find the \\(n\\)-th number in the sequence.

Let the \\(n\\)-th number of the Fibonacci sequence be \\(f(n)\\). Two conclusions can be easily obtained.

  • The first two numbers of the sequence are \\(f(1) = 0\\) and \\(f(2) = 1\\).
  • Each number in the sequence is the sum of the previous two numbers, i.e., \\(f(n) = f(n - 1) + f(n - 2)\\).

Following the recurrence relation to make recursive calls, with the first two numbers as termination conditions, we can write the recursive code. Calling fib(n) will give us the \\(n\\)-th number of the Fibonacci sequence:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def fib(n: int) -> int:\n    \"\"\"Fibonacci sequence: recursion\"\"\"\n    # Termination condition f(1) = 0, f(2) = 1\n    if n == 1 or n == 2:\n        return n - 1\n    # Recursive call f(n) = f(n-1) + f(n-2)\n    res = fib(n - 1) + fib(n - 2)\n    # Return result f(n)\n    return res\n
recursion.cpp
/* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.java
/* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.cs
/* Fibonacci sequence: recursion */\nint Fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = Fib(n - 1) + Fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.go
/* Fibonacci sequence: recursion */\nfunc fib(n int) int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    res := fib(n-1) + fib(n-2)\n    // Return result f(n)\n    return res\n}\n
recursion.swift
/* Fibonacci sequence: recursion */\nfunc fib(n: Int) -> Int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    let res = fib(n: n - 1) + fib(n: n - 2)\n    // Return result f(n)\n    return res\n}\n
recursion.js
/* Fibonacci sequence: recursion */\nfunction fib(n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.ts
/* Fibonacci sequence: recursion */\nfunction fib(n: number): number {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.dart
/* Fibonacci sequence: recursion */\nint fib(int n) {\n  // Termination condition f(1) = 0, f(2) = 1\n  if (n == 1 || n == 2) return n - 1;\n  // Recursive call f(n) = f(n-1) + f(n-2)\n  int res = fib(n - 1) + fib(n - 2);\n  // Return result f(n)\n  return res;\n}\n
recursion.rs
/* Fibonacci sequence: recursion */\nfn fib(n: i32) -> i32 {\n    // Termination condition f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1;\n    }\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    let res = fib(n - 1) + fib(n - 2);\n    // Return result\n    res\n}\n
recursion.c
/* Fibonacci sequence: recursion */\nint fib(int n) {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // Return result f(n)\n    return res;\n}\n
recursion.kt
/* Fibonacci sequence: recursion */\nfun fib(n: Int): Int {\n    // Termination condition f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1\n    // Recursive call f(n) = f(n-1) + f(n-2)\n    val res = fib(n - 1) + fib(n - 2)\n    // Return result f(n)\n    return res\n}\n
recursion.rb
### Fibonacci sequence: recursion ###\ndef fib(n)\n  # Termination condition f(1) = 0, f(2) = 1\n  return n - 1 if n == 1 || n == 2\n  # Recursive call f(n) = f(n-1) + f(n-2)\n  res = fib(n - 1) + fib(n - 2)\n  # Return result f(n)\n  res\nend\n

Observing the above code, we recursively call two functions within the function, meaning that one call produces two call branches. As shown in Figure 2-6, such continuous recursive calling will eventually produce a recursion tree with \\(n\\) levels.

Figure 2-6   Recursion tree of the Fibonacci sequence

Fundamentally, recursion embodies the paradigm of \"decomposing a problem into smaller subproblems\", and this divide-and-conquer strategy is crucial.

  • From an algorithmic perspective, many important algorithmic strategies such as searching, sorting, backtracking, divide and conquer, and dynamic programming directly or indirectly apply this way of thinking.
  • From a data structure perspective, recursion is naturally suited for handling problems related to linked lists, trees, and graphs, because they are well-suited for analysis using divide-and-conquer thinking.
","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223-comparison-of-the-two","level":2,"title":"2.2.3   Comparison of the Two","text":"

Summarizing the above content, as shown in Table 2-1, iteration and recursion differ in implementation, performance, and applicability.

Table 2-1   Comparison of iteration and recursion characteristics

Iteration Recursion Implementation Loop structure Function calls itself Time efficiency Generally more efficient, no function call overhead Each function call incurs overhead Memory usage Usually uses a fixed amount of memory space Accumulated function calls may use a large amount of stack frame space Suitable problems Suitable for simple loop tasks, with intuitive and readable code Suitable for subproblem decomposition, such as trees, graphs, divide and conquer, backtracking, etc., with concise and clear code structure

Tip

If you find the following content difficult to understand, you can review it after reading the \"Stack\" chapter.

What is the intrinsic relationship between iteration and recursion? Taking the above recursive function as an example, the summation operation is performed during the \"ascending\" phase of recursion. This means that the function called first actually completes its summation operation last, and this working mechanism is similar to the \"last-in, first-out\" principle of stacks.

In fact, recursive terminology such as \"call stack\" and \"stack frame space\" already hints at the close relationship between recursion and stacks.

  1. Descend: When a function is called, the system allocates a new stack frame on the \"call stack\" for that function to store the function's local variables, parameters, return address, and other data.
  2. Ascend: When the function completes execution and returns, the corresponding stack frame is removed from the \"call stack\", restoring the execution environment of the previous function.

Therefore, we can use an explicit stack to simulate the behavior of the call stack, thus transforming recursion into iterative form:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def for_loop_recur(n: int) -> int:\n    \"\"\"Simulate recursion using iteration\"\"\"\n    # Use an explicit stack to simulate the system call stack\n    stack = []\n    res = 0\n    # Recurse: recursive call\n    for i in range(n, 0, -1):\n        # Simulate \"recurse\" with \"push\"\n        stack.append(i)\n    # Return: return result\n    while stack:\n        # Simulate \"return\" with \"pop\"\n        res += stack.pop()\n    # res = 1+2+3+...+n\n    return res\n
recursion.cpp
/* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    stack<int> stack;\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (!stack.empty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.top();\n        stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.java
/* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    Stack<Integer> stack = new Stack<>();\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (!stack.isEmpty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.cs
/* Simulate recursion using iteration */\nint ForLoopRecur(int n) {\n    // Use an explicit stack to simulate the system call stack\n    Stack<int> stack = new();\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.Push(i);\n    }\n    // Return: return result\n    while (stack.Count > 0) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.Pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.go
/* Simulate recursion using iteration */\nfunc forLoopRecur(n int) int {\n    // Use an explicit stack to simulate the system call stack\n    stack := list.New()\n    res := 0\n    // Recurse: recursive call\n    for i := n; i > 0; i-- {\n        // Simulate \"recurse\" with \"push\"\n        stack.PushBack(i)\n    }\n    // Return: return result\n    for stack.Len() != 0 {\n        // Simulate \"return\" with \"pop\"\n        res += stack.Back().Value.(int)\n        stack.Remove(stack.Back())\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.swift
/* Simulate recursion using iteration */\nfunc forLoopRecur(n: Int) -> Int {\n    // Use an explicit stack to simulate the system call stack\n    var stack: [Int] = []\n    var res = 0\n    // Recurse: recursive call\n    for i in (1 ... n).reversed() {\n        // Simulate \"recurse\" with \"push\"\n        stack.append(i)\n    }\n    // Return: return result\n    while !stack.isEmpty {\n        // Simulate \"return\" with \"pop\"\n        res += stack.removeLast()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.js
/* Simulate recursion using iteration */\nfunction forLoopRecur(n) {\n    // Use an explicit stack to simulate the system call stack\n    const stack = [];\n    let res = 0;\n    // Recurse: recursive call\n    for (let i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (stack.length) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.ts
/* Simulate recursion using iteration */\nfunction forLoopRecur(n: number): number {\n    // Use an explicit stack to simulate the system call stack\n    const stack: number[] = [];\n    let res: number = 0;\n    // Recurse: recursive call\n    for (let i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while (stack.length) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.dart
/* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n  // Use an explicit stack to simulate the system call stack\n  List<int> stack = [];\n  int res = 0;\n  // Recurse: recursive call\n  for (int i = n; i > 0; i--) {\n    // Simulate \"recurse\" with \"push\"\n    stack.add(i);\n  }\n  // Return: return result\n  while (!stack.isEmpty) {\n    // Simulate \"return\" with \"pop\"\n    res += stack.removeLast();\n  }\n  // res = 1+2+3+...+n\n  return res;\n}\n
recursion.rs
/* Simulate recursion using iteration */\nfn for_loop_recur(n: i32) -> i32 {\n    // Use an explicit stack to simulate the system call stack\n    let mut stack = Vec::new();\n    let mut res = 0;\n    // Recurse: recursive call\n    for i in (1..=n).rev() {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i);\n    }\n    // Return: return result\n    while !stack.is_empty() {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop().unwrap();\n    }\n    // res = 1+2+3+...+n\n    res\n}\n
recursion.c
/* Simulate recursion using iteration */\nint forLoopRecur(int n) {\n    int stack[1000]; // Use a large array to simulate stack\n    int top = -1;    // Stack top index\n    int res = 0;\n    // Recurse: recursive call\n    for (int i = n; i > 0; i--) {\n        // Simulate \"recurse\" with \"push\"\n        stack[1 + top++] = i;\n    }\n    // Return: return result\n    while (top >= 0) {\n        // Simulate \"return\" with \"pop\"\n        res += stack[top--];\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.kt
/* Simulate recursion using iteration */\nfun forLoopRecur(n: Int): Int {\n    // Use an explicit stack to simulate the system call stack\n    val stack = Stack<Int>()\n    var res = 0\n    // Descend: recursive call\n    for (i in n downTo 0) {\n        // Simulate \"recurse\" with \"push\"\n        stack.push(i)\n    }\n    // Return: return result\n    while (stack.isNotEmpty()) {\n        // Simulate \"return\" with \"pop\"\n        res += stack.pop()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.rb
### Use iteration to simulate recursion ###\ndef for_loop_recur(n)\n  # Use an explicit stack to simulate the system call stack\n  stack = []\n  res = 0\n\n  # Recurse: recursive call\n  for i in n.downto(0)\n    # Simulate \"recurse\" with \"push\"\n    stack << i\n  end\n  # Return: return result\n  while !stack.empty?\n    res += stack.pop\n  end\n\n  # res = 1+2+3+...+n\n  res\nend\n

Observing the above code, when recursion is transformed into iteration, the code becomes more complex. Although iteration and recursion can be converted into each other in many cases, it may not be worthwhile to do so for the following two reasons.

  • The transformed code may be more difficult to understand and less readable.
  • For some complex problems, simulating the behavior of the system call stack can be very difficult.

In summary, choosing between iteration and recursion depends on the nature of the specific problem. In programming practice, it is crucial to weigh the pros and cons of both and choose the appropriate method based on the context.

","path":["Chapter 2. Complexity Analysis","2.2   Iteration and Recursion"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   Algorithm Efficiency Evaluation","text":"

In algorithm design, we pursue the following two levels of objectives sequentially.

  1. Finding a solution to the problem: The algorithm must reliably obtain the correct solution within the specified input range.
  2. Seeking the optimal solution: Multiple solutions may exist for the same problem, and we hope to find an algorithm that is as efficient as possible.

In other words, under the premise of being able to solve the problem, algorithm efficiency has become the primary evaluation criterion for measuring the quality of algorithms. It includes the following two dimensions.

  • Time efficiency: The length of time the algorithm runs.
  • Space efficiency: The size of memory space the algorithm occupies.

In short, our goal is to design data structures and algorithms that are \"both fast and memory-efficient\". Effectively evaluating algorithm efficiency is crucial, because only in this way can we compare various algorithms and guide the algorithm design and optimization process.

Efficiency evaluation methods are mainly divided into two types: actual testing and theoretical estimation.

","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211-actual-testing","level":2,"title":"2.1.1   Actual Testing","text":"

Suppose we now have algorithm A and algorithm B, both of which can solve the same problem, and we need to compare the efficiency of these two algorithms. The most direct method is to find a computer, run these two algorithms, and monitor and record their running time and memory usage. This evaluation approach can reflect the real situation, but it also has considerable limitations.

On one hand, it is difficult to eliminate interference factors from the testing environment. Hardware configuration affects the performance of algorithms. For example, if an algorithm has a high degree of parallelism, it is more suitable for running on multi-core CPUs; if an algorithm has intensive memory operations, it will perform better on high-performance memory. In other words, the test results of an algorithm on different machines may be inconsistent. This means we need to test on various machines and calculate average efficiency, which is impractical.

On the other hand, conducting complete testing is very resource-intensive. As the input data volume changes, the algorithm will exhibit different efficiencies. For example, when the input data volume is small, the running time of algorithm A is shorter than algorithm B; but when the input data volume is large, the test results may be exactly the opposite. Therefore, to obtain convincing conclusions, we need to test input data of various scales, which requires a large amount of computational resources.

","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#212-theoretical-estimation","level":2,"title":"2.1.2   Theoretical Estimation","text":"

Since actual testing has considerable limitations, we can consider evaluating algorithm efficiency through calculations alone. This estimation method is called asymptotic complexity analysis, or complexity analysis for short.

Complexity analysis can reflect the relationship between the time and space resources required for algorithm execution and the input data scale. It describes the growth trend of the time and space required for algorithm execution as the input data scale increases. This definition is somewhat convoluted, so we can break it down into three key points to understand.

  • \"Time and space resources\" correspond to time complexity and space complexity, respectively.
  • \"As the input data scale increases\" means that complexity reflects the relationship between algorithm running efficiency and input data scale.
  • \"Growth trend of time and space\" indicates that complexity analysis focuses not on the specific values of running time or occupied space, but on how \"fast\" time or space grows.

Complexity analysis overcomes the drawbacks of the actual testing method, reflected in the following aspects.

  • It does not need to actually run the code, making it more environmentally friendly and energy-efficient.
  • It is independent of the testing environment, and the analysis results are applicable to all running platforms.
  • It can reflect algorithm efficiency at different data volumes, especially algorithm performance at large data volumes.

Tip

If you are still confused about the concept of complexity, don't worry—we will introduce it in detail in subsequent chapters.

Complexity analysis provides us with a \"ruler\" for evaluating algorithm efficiency, allowing us to measure the time and space resources required to execute a certain algorithm and compare the efficiency between different algorithms.

Complexity is a mathematical concept that may be relatively abstract for beginners, with a relatively high learning difficulty. From this perspective, complexity analysis may not be very suitable as the first content to be introduced. However, when we discuss the characteristics of a certain data structure or algorithm, it is difficult to avoid analyzing its running speed and space usage.

In summary, it is recommended that before diving deep into data structures and algorithms, you first establish a preliminary understanding of complexity analysis so that you can complete complexity analysis of simple algorithms.

","path":["Chapter 2. Complexity Analysis","2.1   Algorithm Efficiency Evaluation"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/","level":1,"title":"2.4   Space Complexity","text":"

Space complexity measures the growth trend of memory space occupied by an algorithm as the data size increases. This concept is very similar to time complexity, except that \"running time\" is replaced with \"occupied memory space\".

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#241-algorithm-related-space","level":2,"title":"2.4.1   Algorithm-Related Space","text":"

The memory space used by an algorithm during execution mainly includes the following types.

  • Input space: Used to store the input data of the algorithm.
  • Temporary space: Used to store variables, objects, function contexts, and other data during the algorithm's execution.
  • Output space: Used to store the output data of the algorithm.

In general, the scope of space complexity statistics is \"temporary space\" plus \"output space\".

Temporary space can be further divided into three parts.

  • Temporary data: Used to save various constants, variables, objects, etc., during the algorithm's execution.
  • Stack frame space: Used to save the context data of called functions. The system creates a stack frame at the top of the stack each time a function is called, and the stack frame space is released after the function returns.
  • Instruction space: Used to save compiled program instructions, which are usually ignored in actual statistics.

When analyzing the space complexity of a program, we usually count three parts: temporary data, stack frame space, and output data, as shown in the following figure.

Figure 2-15   Algorithm-related space

The related code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class Node:\n    \"\"\"Class\"\"\"\n    def __init__(self, x: int):\n        self.val: int = x              # Node value\n        self.next: Node | None = None  # Reference to the next node\n\ndef function() -> int:\n    \"\"\"Function\"\"\"\n    # Perform some operations...\n    return 0\n\ndef algorithm(n) -> int:  # Input data\n    A = 0                 # Temporary data (constant, usually represented by uppercase letters)\n    b = 0                 # Temporary data (variable)\n    node = Node(0)        # Temporary data (object)\n    c = function()        # Stack frame space (function call)\n    return A + b + c      # Output data\n
/* Structure */\nstruct Node {\n    int val;\n    Node *next;\n    Node(int x) : val(x), next(nullptr) {}\n};\n\n/* Function */\nint func() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) {        // Input data\n    const int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node* node = new Node(0); // Temporary data (object)\n    int c = func();           // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
/* Class */\nclass Node {\n    int val;\n    Node next;\n    Node(int x) { val = x; }\n}\n\n/* Function */\nint function() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) {        // Input data\n    final int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node node = new Node(0);  // Temporary data (object)\n    int c = function();       // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
/* Class */\nclass Node(int x) {\n    int val = x;\n    Node next;\n}\n\n/* Function */\nint Function() {\n    // Perform some operations...\n    return 0;\n}\n\nint Algorithm(int n) {        // Input data\n    const int a = 0;          // Temporary data (constant)\n    int b = 0;                // Temporary data (variable)\n    Node node = new(0);       // Temporary data (object)\n    int c = Function();       // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
/* Structure */\ntype node struct {\n    val  int\n    next *node\n}\n\n/* Create node structure */\nfunc newNode(val int) *node {\n    return &node{val: val}\n}\n\n/* Function */\nfunc function() int {\n    // Perform some operations...\n    return 0\n}\n\nfunc algorithm(n int) int { // Input data\n    const a = 0             // Temporary data (constant)\n    b := 0                  // Temporary data (variable)\n    newNode(0)              // Temporary data (object)\n    c := function()         // Stack frame space (function call)\n    return a + b + c        // Output data\n}\n
/* Class */\nclass Node {\n    var val: Int\n    var next: Node?\n\n    init(x: Int) {\n        val = x\n    }\n}\n\n/* Function */\nfunc function() -> Int {\n    // Perform some operations...\n    return 0\n}\n\nfunc algorithm(n: Int) -> Int { // Input data\n    let a = 0             // Temporary data (constant)\n    var b = 0             // Temporary data (variable)\n    let node = Node(x: 0) // Temporary data (object)\n    let c = function()    // Stack frame space (function call)\n    return a + b + c      // Output data\n}\n
/* Class */\nclass Node {\n    val;\n    next;\n    constructor(val) {\n        this.val = val === undefined ? 0 : val; // Node value\n        this.next = null;                       // Reference to the next node\n    }\n}\n\n/* Function */\nfunction constFunc() {\n    // Perform some operations\n    return 0;\n}\n\nfunction algorithm(n) {       // Input data\n    const a = 0;              // Temporary data (constant)\n    let b = 0;                // Temporary data (variable)\n    const node = new Node(0); // Temporary data (object)\n    const c = constFunc();    // Stack frame space (function call)\n    return a + b + c;         // Output data\n}\n
/* Class */\nclass Node {\n    val: number;\n    next: Node | null;\n    constructor(val?: number) {\n        this.val = val === undefined ? 0 : val; // Node value\n        this.next = null;                       // Reference to the next node\n    }\n}\n\n/* Function */\nfunction constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n\nfunction algorithm(n: number): number { // Input data\n    const a = 0;                        // Temporary data (constant)\n    let b = 0;                          // Temporary data (variable)\n    const node = new Node(0);           // Temporary data (object)\n    const c = constFunc();              // Stack frame space (function call)\n    return a + b + c;                   // Output data\n}\n
/* Class */\nclass Node {\n  int val;\n  Node next;\n  Node(this.val, [this.next]);\n}\n\n/* Function */\nint function() {\n  // Perform some operations...\n  return 0;\n}\n\nint algorithm(int n) {  // Input data\n  const int a = 0;      // Temporary data (constant)\n  int b = 0;            // Temporary data (variable)\n  Node node = Node(0);  // Temporary data (object)\n  int c = function();   // Stack frame space (function call)\n  return a + b + c;     // Output data\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Structure */\nstruct Node {\n    val: i32,\n    next: Option<Rc<RefCell<Node>>>,\n}\n\n/* Create Node structure */\nimpl Node {\n    fn new(val: i32) -> Self {\n        Self { val: val, next: None }\n    }\n}\n\n/* Function */\nfn function() -> i32 {\n    // Perform some operations...\n    return 0;\n}\n\nfn algorithm(n: i32) -> i32 {       // Input data\n    const a: i32 = 0;               // Temporary data (constant)\n    let mut b = 0;                  // Temporary data (variable)\n    let node = Node::new(0);        // Temporary data (object)\n    let c = function();             // Stack frame space (function call)\n    return a + b + c;               // Output data\n}\n
/* Function */\nint func() {\n    // Perform some operations...\n    return 0;\n}\n\nint algorithm(int n) { // Input data\n    const int a = 0;   // Temporary data (constant)\n    int b = 0;         // Temporary data (variable)\n    int c = func();    // Stack frame space (function call)\n    return a + b + c;  // Output data\n}\n
/* Class */\nclass Node(var _val: Int) {\n    var next: Node? = null\n}\n\n/* Function */\nfun function(): Int {\n    // Perform some operations...\n    return 0\n}\n\nfun algorithm(n: Int): Int { // Input data\n    val a = 0                // Temporary data (constant)\n    var b = 0                // Temporary data (variable)\n    val node = Node(0)       // Temporary data (object)\n    val c = function()       // Stack frame space (function call)\n    return a + b + c         // Output data\n}\n
### Class ###\nclass Node\n    attr_accessor :val      # Node value\n    attr_accessor :next     # Reference to the next node\n\n    def initialize(x)\n        @val = x\n    end\nend\n\n### Function ###\ndef function\n    # Perform some operations...\n    0\nend\n\n### Algorithm ###\ndef algorithm(n)        # Input data\n    a = 0               # Temporary data (constant)\n    b = 0               # Temporary data (variable)\n    node = Node.new(0)  # Temporary data (object)\n    c = function        # Stack frame space (function call)\n    a + b + c           # Output data\nend\n
","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#242-calculation-method","level":2,"title":"2.4.2   Calculation Method","text":"

The calculation method for space complexity is roughly the same as for time complexity, except that the statistical object is changed from \"number of operations\" to \"size of space used\".

Unlike time complexity, we usually only focus on the worst-case space complexity. This is because memory space is a hard requirement, and we must ensure that sufficient memory space is reserved for all input data.

Observe the following code. The \"worst case\" in worst-case space complexity has two meanings.

  1. Based on the worst input data: When \\(n < 10\\), the space complexity is \\(O(1)\\); but when \\(n > 10\\), the initialized array nums occupies \\(O(n)\\) space, so the worst-case space complexity is \\(O(n)\\).
  2. Based on the peak memory during algorithm execution: For example, before executing the last line, the program occupies \\(O(1)\\) space; when initializing the array nums, the program occupies \\(O(n)\\) space, so the worst-case space complexity is \\(O(n)\\).
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 0               # O(1)\n    b = [0] * 10000     # O(1)\n    if n > 10:\n        nums = [0] * n  # O(n)\n
void algorithm(int n) {\n    int a = 0;               // O(1)\n    vector<int> b(10000);    // O(1)\n    if (n > 10)\n        vector<int> nums(n); // O(n)\n}\n
void algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10)\n        int[] nums = new int[n]; // O(n)\n}\n
void Algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10) {\n        int[] nums = new int[n]; // O(n)\n    }\n}\n
func algorithm(n int) {\n    a := 0                      // O(1)\n    b := make([]int, 10000)     // O(1)\n    var nums []int\n    if n > 10 {\n        nums := make([]int, n)  // O(n)\n    }\n    fmt.Println(a, b, nums)\n}\n
func algorithm(n: Int) {\n    let a = 0 // O(1)\n    let b = Array(repeating: 0, count: 10000) // O(1)\n    if n > 10 {\n        let nums = Array(repeating: 0, count: n) // O(n)\n    }\n}\n
function algorithm(n) {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
function algorithm(n: number): void {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
void algorithm(int n) {\n  int a = 0;                            // O(1)\n  List<int> b = List.filled(10000, 0);  // O(1)\n  if (n > 10) {\n    List<int> nums = List.filled(n, 0); // O(n)\n  }\n}\n
fn algorithm(n: i32) {\n    let a = 0;                              // O(1)\n    let b = [0; 10000];                     // O(1)\n    if n > 10 {\n        let nums = vec![0; n as usize];     // O(n)\n    }\n}\n
void algorithm(int n) {\n    int a = 0;               // O(1)\n    int b[10000];            // O(1)\n    if (n > 10)\n        int nums[n] = {0};   // O(n)\n}\n
fun algorithm(n: Int) {\n    val a = 0                    // O(1)\n    val b = IntArray(10000)      // O(1)\n    if (n > 10) {\n        val nums = IntArray(n)   // O(n)\n    }\n}\n
def algorithm(n)\n    a = 0                           # O(1)\n    b = Array.new(10000)            # O(1)\n    nums = Array.new(n) if n > 10   # O(n)\nend\n

In recursive functions, it is necessary to count the stack frame space. Observe the following code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def function() -> int:\n    # Perform some operations\n    return 0\n\ndef loop(n: int):\n    \"\"\"Loop has space complexity of O(1)\"\"\"\n    for _ in range(n):\n        function()\n\ndef recur(n: int):\n    \"\"\"Recursion has space complexity of O(n)\"\"\"\n    if n == 1:\n        return\n    return recur(n - 1)\n
int func() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
int function() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
int Function() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid Loop(int n) {\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nint Recur(int n) {\n    if (n == 1) return 1;\n    return Recur(n - 1);\n}\n
func function() int {\n    // Perform some operations\n    return 0\n}\n\n/* Loop has space complexity of O(1) */\nfunc loop(n int) {\n    for i := 0; i < n; i++ {\n        function()\n    }\n}\n\n/* Recursion has space complexity of O(n) */\nfunc recur(n int) {\n    if n == 1 {\n        return\n    }\n    recur(n - 1)\n}\n
@discardableResult\nfunc function() -> Int {\n    // Perform some operations\n    return 0\n}\n\n/* Loop has space complexity of O(1) */\nfunc loop(n: Int) {\n    for _ in 0 ..< n {\n        function()\n    }\n}\n\n/* Recursion has space complexity of O(n) */\nfunc recur(n: Int) {\n    if n == 1 {\n        return\n    }\n    recur(n: n - 1)\n}\n
function constFunc() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfunction loop(n) {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfunction recur(n) {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
function constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfunction loop(n: number): void {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfunction recur(n: number): void {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
int function() {\n  // Perform some operations\n  return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n  for (int i = 0; i < n; i++) {\n    function();\n  }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n  if (n == 1) return;\n  recur(n - 1);\n}\n
fn function() -> i32 {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nfn loop(n: i32) {\n    for i in 0..n {\n        function();\n    }\n}\n/* Recursion has space complexity of O(n) */\nfn recur(n: i32) {\n    if n == 1 {\n        return;\n    }\n    recur(n - 1);\n}\n
int func() {\n    // Perform some operations\n    return 0;\n}\n/* Loop has space complexity of O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* Recursion has space complexity of O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
fun function(): Int {\n    // Perform some operations\n    return 0\n}\n/* Loop has space complexity of O(1) */\nfun loop(n: Int) {\n    for (i in 0..<n) {\n        function()\n    }\n}\n/* Recursion has space complexity of O(n) */\nfun recur(n: Int) {\n    if (n == 1) return\n    return recur(n - 1)\n}\n
def function\n    # Perform some operations\n    0\nend\n\n### Loop has space complexity of O(1) ###\ndef loop(n)\n    (0...n).each { function }\nend\n\n### Recursion has space complexity of O(n) ###\ndef recur(n)\n    return if n == 1\n    recur(n - 1)\nend\n

The time complexity of both functions loop() and recur() is \\(O(n)\\), but their space complexities are different.

  • The function loop() calls function() \\(n\\) times in a loop. In each iteration, function() returns and releases its stack frame space, so the space complexity remains \\(O(1)\\).
  • The recursive function recur() has \\(n\\) unreturned recur() instances existing simultaneously during execution, thus occupying \\(O(n)\\) stack frame space.
","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243-common-types","level":2,"title":"2.4.3   Common Types","text":"

Let the input data size be \\(n\\). The following figure shows common types of space complexity (arranged from low to high).

\\[ \\begin{aligned} O(1) < O(\\log n) < O(n) < O(n^2) < O(2^n) \\newline \\text{Constant} < \\text{Logarithmic} < \\text{Linear} < \\text{Quadratic} < \\text{Exponential} \\end{aligned} \\]

Figure 2-16   Common types of space complexity

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#1-constant-order-o1","level":3,"title":"1.   Constant Order \\(O(1)\\)","text":"

Constant order is common in constants, variables, and objects whose quantity is independent of the input data size \\(n\\).

It should be noted that memory occupied by initializing variables or calling functions in a loop is released when entering the next iteration, so it does not accumulate space, and the space complexity remains \\(O(1)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def function() -> int:\n    \"\"\"Function\"\"\"\n    # Perform some operations\n    return 0\n\ndef constant(n: int):\n    \"\"\"Constant order\"\"\"\n    # Constants, variables, objects occupy O(1) space\n    a = 0\n    nums = [0] * 10000\n    node = ListNode(0)\n    # Variables in the loop occupy O(1) space\n    for _ in range(n):\n        c = 0\n    # Functions in the loop occupy O(1) space\n    for _ in range(n):\n        function()\n
space_complexity.cpp
/* Function */\nint func() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    const int a = 0;\n    int b = 0;\n    vector<int> nums(10000);\n    ListNode node(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
space_complexity.java
/* Function */\nint function() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    final int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n
space_complexity.cs
/* Function */\nint Function() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid Constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new(0);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n
space_complexity.go
/* Function */\nfunc function() int {\n    // Perform some operations...\n    return 0\n}\n\n/* Constant order */\nfunc spaceConstant(n int) {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0\n    b := 0\n    nums := make([]int, 10000)\n    node := newNode(0)\n    // Variables in the loop occupy O(1) space\n    var c int\n    for i := 0; i < n; i++ {\n        c = 0\n    }\n    // Functions in the loop occupy O(1) space\n    for i := 0; i < n; i++ {\n        function()\n    }\n    b += 0\n    c += 0\n    nums[0] = 0\n    node.val = 0\n}\n
space_complexity.swift
/* Function */\n@discardableResult\nfunc function() -> Int {\n    // Perform some operations\n    return 0\n}\n\n/* Constant order */\nfunc constant(n: Int) {\n    // Constants, variables, objects occupy O(1) space\n    let a = 0\n    var b = 0\n    let nums = Array(repeating: 0, count: 10000)\n    let node = ListNode(x: 0)\n    // Variables in the loop occupy O(1) space\n    for _ in 0 ..< n {\n        let c = 0\n    }\n    // Functions in the loop occupy O(1) space\n    for _ in 0 ..< n {\n        function()\n    }\n}\n
space_complexity.js
/* Function */\nfunction constFunc() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nfunction constant(n) {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
space_complexity.ts
/* Function */\nfunction constFunc(): number {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nfunction constant(n: number): void {\n    // Constants, variables, objects occupy O(1) space\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // Variables in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
space_complexity.dart
/* Function */\nint function() {\n  // Perform some operations\n  return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n  // Constants, variables, objects occupy O(1) space\n  final int a = 0;\n  int b = 0;\n  List<int> nums = List.filled(10000, 0);\n  ListNode node = ListNode(0);\n  // Variables in the loop occupy O(1) space\n  for (var i = 0; i < n; i++) {\n    int c = 0;\n  }\n  // Functions in the loop occupy O(1) space\n  for (var i = 0; i < n; i++) {\n    function();\n  }\n}\n
space_complexity.rs
/* Function */\nfn function() -> i32 {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\n#[allow(unused)]\nfn constant(n: i32) {\n    // Constants, variables, objects occupy O(1) space\n    const A: i32 = 0;\n    let b = 0;\n    let nums = vec![0; 10000];\n    let node = ListNode::new(0);\n    // Variables in the loop occupy O(1) space\n    for i in 0..n {\n        let c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for i in 0..n {\n        function();\n    }\n}\n
space_complexity.c
/* Function */\nint func() {\n    // Perform some operations\n    return 0;\n}\n\n/* Constant order */\nvoid constant(int n) {\n    // Constants, variables, objects occupy O(1) space\n    const int a = 0;\n    int b = 0;\n    int nums[1000];\n    ListNode *node = newListNode(0);\n    free(node);\n    // Variables in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // Functions in the loop occupy O(1) space\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
space_complexity.kt
/* Function */\nfun function(): Int {\n    // Perform some operations\n    return 0\n}\n\n/* Constant order */\nfun constant(n: Int) {\n    // Constants, variables, objects occupy O(1) space\n    val a = 0\n    var b = 0\n    val nums = Array(10000) { 0 }\n    val node = ListNode(0)\n    // Variables in the loop occupy O(1) space\n    for (i in 0..<n) {\n        val c = 0\n    }\n    // Functions in the loop occupy O(1) space\n    for (i in 0..<n) {\n        function()\n    }\n}\n
space_complexity.rb
### Function ###\ndef function\n  # Perform some operations\n  0\nend\n\n### Constant time ###\ndef constant(n)\n  # Constants, variables, objects occupy O(1) space\n  a = 0\n  nums = [0] * 10000\n  node = ListNode.new\n\n  # Variables in the loop occupy O(1) space\n  (0...n).each { c = 0 }\n  # Functions in the loop occupy O(1) space\n  (0...n).each { function }\nend\n
","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#2-linear-order-on","level":3,"title":"2.   Linear Order \\(O(n)\\)","text":"

Linear order is common in arrays, linked lists, stacks, queues, etc., where the number of elements is proportional to \\(n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def linear(n: int):\n    \"\"\"Linear order\"\"\"\n    # A list of length n occupies O(n) space\n    nums = [0] * n\n    # A hash table of length n occupies O(n) space\n    hmap = dict[int, str]()\n    for i in range(n):\n        hmap[i] = str(i)\n
space_complexity.cpp
/* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    vector<int> nums(n);\n    // A list of length n occupies O(n) space\n    vector<ListNode> nodes;\n    for (int i = 0; i < n; i++) {\n        nodes.push_back(ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\n    unordered_map<int, string> map;\n    for (int i = 0; i < n; i++) {\n        map[i] = to_string(i);\n    }\n}\n
space_complexity.java
/* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    int[] nums = new int[n];\n    // A list of length n occupies O(n) space\n    List<ListNode> nodes = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        nodes.add(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\n    Map<Integer, String> map = new HashMap<>();\n    for (int i = 0; i < n; i++) {\n        map.put(i, String.valueOf(i));\n    }\n}\n
space_complexity.cs
/* Linear order */\nvoid Linear(int n) {\n    // Array of length n uses O(n) space\n    int[] nums = new int[n];\n    // A list of length n occupies O(n) space\n    List<ListNode> nodes = [];\n    for (int i = 0; i < n; i++) {\n        nodes.Add(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\n    Dictionary<int, string> map = [];\n    for (int i = 0; i < n; i++) {\n        map.Add(i, i.ToString());\n    }\n}\n
space_complexity.go
/* Linear order */\nfunc spaceLinear(n int) {\n    // Array of length n uses O(n) space\n    _ = make([]int, n)\n    // A list of length n occupies O(n) space\n    var nodes []*node\n    for i := 0; i < n; i++ {\n        nodes = append(nodes, newNode(i))\n    }\n    // A hash table of length n occupies O(n) space\n    m := make(map[int]string, n)\n    for i := 0; i < n; i++ {\n        m[i] = strconv.Itoa(i)\n    }\n}\n
space_complexity.swift
/* Linear order */\nfunc linear(n: Int) {\n    // Array of length n uses O(n) space\n    let nums = Array(repeating: 0, count: n)\n    // A list of length n occupies O(n) space\n    let nodes = (0 ..< n).map { ListNode(x: $0) }\n    // A hash table of length n occupies O(n) space\n    let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, \"\\($0)\") })\n}\n
space_complexity.js
/* Linear order */\nfunction linear(n) {\n    // Array of length n uses O(n) space\n    const nums = new Array(n);\n    // A list of length n occupies O(n) space\n    const nodes = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
space_complexity.ts
/* Linear order */\nfunction linear(n: number): void {\n    // Array of length n uses O(n) space\n    const nums = new Array(n);\n    // A list of length n occupies O(n) space\n    const nodes: ListNode[] = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // A hash table of length n occupies O(n) space\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
space_complexity.dart
/* Linear order */\nvoid linear(int n) {\n  // Array of length n uses O(n) space\n  List<int> nums = List.filled(n, 0);\n  // A list of length n occupies O(n) space\n  List<ListNode> nodes = [];\n  for (var i = 0; i < n; i++) {\n    nodes.add(ListNode(i));\n  }\n  // A hash table of length n occupies O(n) space\n  Map<int, String> map = HashMap();\n  for (var i = 0; i < n; i++) {\n    map.putIfAbsent(i, () => i.toString());\n  }\n}\n
space_complexity.rs
/* Linear order */\n#[allow(unused)]\nfn linear(n: i32) {\n    // Array of length n uses O(n) space\n    let mut nums = vec![0; n as usize];\n    // A list of length n occupies O(n) space\n    let mut nodes = Vec::new();\n    for i in 0..n {\n        nodes.push(ListNode::new(i))\n    }\n    // A hash table of length n occupies O(n) space\n    let mut map = HashMap::new();\n    for i in 0..n {\n        map.insert(i, i.to_string());\n    }\n}\n
space_complexity.c
/* Hash table */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Implemented using uthash.h\n} HashTable;\n\n/* Linear order */\nvoid linear(int n) {\n    // Array of length n uses O(n) space\n    int *nums = malloc(sizeof(int) * n);\n    free(nums);\n\n    // A list of length n occupies O(n) space\n    ListNode **nodes = malloc(sizeof(ListNode *) * n);\n    for (int i = 0; i < n; i++) {\n        nodes[i] = newListNode(i);\n    }\n    // Memory release\n    for (int i = 0; i < n; i++) {\n        free(nodes[i]);\n    }\n    free(nodes);\n\n    // A hash table of length n occupies O(n) space\n    HashTable *h = NULL;\n    for (int i = 0; i < n; i++) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = i;\n        tmp->val = i;\n        HASH_ADD_INT(h, key, tmp);\n    }\n\n    // Memory release\n    HashTable *curr, *tmp;\n    HASH_ITER(hh, h, curr, tmp) {\n        HASH_DEL(h, curr);\n        free(curr);\n    }\n}\n
space_complexity.kt
/* Linear order */\nfun linear(n: Int) {\n    // Array of length n uses O(n) space\n    val nums = Array(n) { 0 }\n    // A list of length n occupies O(n) space\n    val nodes = mutableListOf<ListNode>()\n    for (i in 0..<n) {\n        nodes.add(ListNode(i))\n    }\n    // A hash table of length n occupies O(n) space\n    val map = mutableMapOf<Int, String>()\n    for (i in 0..<n) {\n        map[i] = i.toString()\n    }\n}\n
space_complexity.rb
### Linear time ###\ndef linear(n)\n  # A list of length n occupies O(n) space\n  nums = Array.new(n, 0)\n\n  # A hash table of length n occupies O(n) space\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n

As shown in the following figure, the recursion depth of this function is \\(n\\), meaning that there are \\(n\\) unreturned linear_recur() functions existing simultaneously, using \\(O(n)\\) stack frame space:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def linear_recur(n: int):\n    \"\"\"Linear order (recursive implementation)\"\"\"\n    print(\"Recursion n =\", n)\n    if n == 1:\n        return\n    linear_recur(n - 1)\n
space_complexity.cpp
/* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    cout << \"Recursion n = \" << n << endl;\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.java
/* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    System.out.println(\"Recursion n = \" + n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.cs
/* Linear order (recursive implementation) */\nvoid LinearRecur(int n) {\n    Console.WriteLine(\"Recursion n = \" + n);\n    if (n == 1) return;\n    LinearRecur(n - 1);\n}\n
space_complexity.go
/* Linear order (recursive implementation) */\nfunc spaceLinearRecur(n int) {\n    fmt.Println(\"Recursion n =\", n)\n    if n == 1 {\n        return\n    }\n    spaceLinearRecur(n - 1)\n}\n
space_complexity.swift
/* Linear order (recursive implementation) */\nfunc linearRecur(n: Int) {\n    print(\"Recursion n = \\(n)\")\n    if n == 1 {\n        return\n    }\n    linearRecur(n: n - 1)\n}\n
space_complexity.js
/* Linear order (recursive implementation) */\nfunction linearRecur(n) {\n    console.log(`Recursion n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
space_complexity.ts
/* Linear order (recursive implementation) */\nfunction linearRecur(n: number): void {\n    console.log(`Recursion n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
space_complexity.dart
/* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n  print('Recursion n = $n');\n  if (n == 1) return;\n  linearRecur(n - 1);\n}\n
space_complexity.rs
/* Linear order (recursive implementation) */\nfn linear_recur(n: i32) {\n    println!(\"Recursion n = {}\", n);\n    if n == 1 {\n        return;\n    };\n    linear_recur(n - 1);\n}\n
space_complexity.c
/* Linear order (recursive implementation) */\nvoid linearRecur(int n) {\n    printf(\"Recursion n = %d\\r\\n\", n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.kt
/* Linear order (recursive implementation) */\nfun linearRecur(n: Int) {\n    println(\"Recursion n = $n\")\n    if (n == 1)\n        return\n    linearRecur(n - 1)\n}\n
space_complexity.rb
### Linear space (recursive) ###\ndef linear_recur(n)\n  puts \"Recursion n = #{n}\"\n  return if n == 1\n  linear_recur(n - 1)\nend\n

Figure 2-17   Linear order space complexity generated by recursive function

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#3-quadratic-order-on2","level":3,"title":"3.   Quadratic Order \\(O(n^2)\\)","text":"

Quadratic order is common in matrices and graphs, where the number of elements is quadratically related to \\(n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def quadratic(n: int):\n    \"\"\"Quadratic order\"\"\"\n    # A 2D list occupies O(n^2) space\n    num_matrix = [[0] * n for _ in range(n)]\n
space_complexity.cpp
/* Exponential order */\nvoid quadratic(int n) {\n    // 2D list uses O(n^2) space\n    vector<vector<int>> numMatrix;\n    for (int i = 0; i < n; i++) {\n        vector<int> tmp;\n        for (int j = 0; j < n; j++) {\n            tmp.push_back(0);\n        }\n        numMatrix.push_back(tmp);\n    }\n}\n
space_complexity.java
/* Exponential order */\nvoid quadratic(int n) {\n    // Matrix uses O(n^2) space\n    int[][] numMatrix = new int[n][n];\n    // 2D list uses O(n^2) space\n    List<List<Integer>> numList = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<Integer> tmp = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            tmp.add(0);\n        }\n        numList.add(tmp);\n    }\n}\n
space_complexity.cs
/* Exponential order */\nvoid Quadratic(int n) {\n    // Matrix uses O(n^2) space\n    int[,] numMatrix = new int[n, n];\n    // 2D list uses O(n^2) space\n    List<List<int>> numList = [];\n    for (int i = 0; i < n; i++) {\n        List<int> tmp = [];\n        for (int j = 0; j < n; j++) {\n            tmp.Add(0);\n        }\n        numList.Add(tmp);\n    }\n}\n
space_complexity.go
/* Exponential order */\nfunc spaceQuadratic(n int) {\n    // Matrix uses O(n^2) space\n    numMatrix := make([][]int, n)\n    for i := 0; i < n; i++ {\n        numMatrix[i] = make([]int, n)\n    }\n}\n
space_complexity.swift
/* Exponential order */\nfunc quadratic(n: Int) {\n    // 2D list uses O(n^2) space\n    let numList = Array(repeating: Array(repeating: 0, count: n), count: n)\n}\n
space_complexity.js
/* Exponential order */\nfunction quadratic(n) {\n    // Matrix uses O(n^2) space\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 2D list uses O(n^2) space\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
space_complexity.ts
/* Exponential order */\nfunction quadratic(n: number): void {\n    // Matrix uses O(n^2) space\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 2D list uses O(n^2) space\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
space_complexity.dart
/* Exponential order */\nvoid quadratic(int n) {\n  // Matrix uses O(n^2) space\n  List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));\n  // 2D list uses O(n^2) space\n  List<List<int>> numList = [];\n  for (var i = 0; i < n; i++) {\n    List<int> tmp = [];\n    for (int j = 0; j < n; j++) {\n      tmp.add(0);\n    }\n    numList.add(tmp);\n  }\n}\n
space_complexity.rs
/* Exponential order */\n#[allow(unused)]\nfn quadratic(n: i32) {\n    // Matrix uses O(n^2) space\n    let num_matrix = vec![vec![0; n as usize]; n as usize];\n    // 2D list uses O(n^2) space\n    let mut num_list = Vec::new();\n    for i in 0..n {\n        let mut tmp = Vec::new();\n        for j in 0..n {\n            tmp.push(0);\n        }\n        num_list.push(tmp);\n    }\n}\n
space_complexity.c
/* Exponential order */\nvoid quadratic(int n) {\n    // 2D list uses O(n^2) space\n    int **numMatrix = malloc(sizeof(int *) * n);\n    for (int i = 0; i < n; i++) {\n        int *tmp = malloc(sizeof(int) * n);\n        for (int j = 0; j < n; j++) {\n            tmp[j] = 0;\n        }\n        numMatrix[i] = tmp;\n    }\n\n    // Memory release\n    for (int i = 0; i < n; i++) {\n        free(numMatrix[i]);\n    }\n    free(numMatrix);\n}\n
space_complexity.kt
/* Exponential order */\nfun quadratic(n: Int) {\n    // Matrix uses O(n^2) space\n    val numMatrix = arrayOfNulls<Array<Int>?>(n)\n    // 2D list uses O(n^2) space\n    val numList = mutableListOf<MutableList<Int>>()\n    for (i in 0..<n) {\n        val tmp = mutableListOf<Int>()\n        for (j in 0..<n) {\n            tmp.add(0)\n        }\n        numList.add(tmp)\n    }\n}\n
space_complexity.rb
### Quadratic time ###\ndef quadratic(n)\n  # 2D list uses O(n^2) space\n  Array.new(n) { Array.new(n, 0) }\nend\n

As shown in the following figure, the recursion depth of this function is \\(n\\), and an array is initialized in each recursive function with lengths of \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\), with an average length of \\(n / 2\\), thus occupying \\(O(n^2)\\) space overall:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def quadratic_recur(n: int) -> int:\n    \"\"\"Quadratic order (recursive implementation)\"\"\"\n    if n <= 0:\n        return 0\n    # Array nums length is n, n-1, ..., 2, 1\n    nums = [0] * n\n    return quadratic_recur(n - 1)\n
space_complexity.cpp
/* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    vector<int> nums(n);\n    cout << \"In recursion n = \" << n << \", nums length = \" << nums.size() << endl;\n    return quadraticRecur(n - 1);\n}\n
space_complexity.java
/* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    // Array nums has length n, n-1, ..., 2, 1\n    int[] nums = new int[n];\n    System.out.println(\"In recursion n = \" + n + \", nums length = \" + nums.length);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.cs
/* Quadratic order (recursive implementation) */\nint QuadraticRecur(int n) {\n    if (n <= 0) return 0;\n    int[] nums = new int[n];\n    Console.WriteLine(\"Recursion n = \" + n + \", nums length = \" + nums.Length);\n    return QuadraticRecur(n - 1);\n}\n
space_complexity.go
/* Quadratic order (recursive implementation) */\nfunc spaceQuadraticRecur(n int) int {\n    if n <= 0 {\n        return 0\n    }\n    nums := make([]int, n)\n    fmt.Printf(\"In recursion n = %d, nums length = %d \\n\", n, len(nums))\n    return spaceQuadraticRecur(n - 1)\n}\n
space_complexity.swift
/* Quadratic order (recursive implementation) */\n@discardableResult\nfunc quadraticRecur(n: Int) -> Int {\n    if n <= 0 {\n        return 0\n    }\n    // Array nums has length n, n-1, ..., 2, 1\n    let nums = Array(repeating: 0, count: n)\n    print(\"In recursion n = \\(n), nums length = \\(nums.count)\")\n    return quadraticRecur(n: n - 1)\n}\n
space_complexity.js
/* Quadratic order (recursive implementation) */\nfunction quadraticRecur(n) {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`In recursion n = ${n}, nums length = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.ts
/* Quadratic order (recursive implementation) */\nfunction quadraticRecur(n: number): number {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`In recursion n = ${n}, nums length = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.dart
/* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n  if (n <= 0) return 0;\n  List<int> nums = List.filled(n, 0);\n  print('In recursion n = $n, nums length = ${nums.length}');\n  return quadraticRecur(n - 1);\n}\n
space_complexity.rs
/* Quadratic order (recursive implementation) */\nfn quadratic_recur(n: i32) -> i32 {\n    if n <= 0 {\n        return 0;\n    };\n    // Array nums has length n, n-1, ..., 2, 1\n    let nums = vec![0; n as usize];\n    println!(\"In recursion n = {}, nums length = {}\", n, nums.len());\n    return quadratic_recur(n - 1);\n}\n
space_complexity.c
/* Quadratic order (recursive implementation) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    int *nums = malloc(sizeof(int) * n);\n    printf(\"In recursion n = %d, nums length = %d\\r\\n\", n, n);\n    int res = quadraticRecur(n - 1);\n    free(nums);\n    return res;\n}\n
space_complexity.kt
/* Quadratic order (recursive implementation) */\ntailrec fun quadraticRecur(n: Int): Int {\n    if (n <= 0)\n        return 0\n    // Array nums has length n, n-1, ..., 2, 1\n    val nums = Array(n) { 0 }\n    println(\"In recursion n = $n, nums length = ${nums.size}\")\n    return quadraticRecur(n - 1)\n}\n
space_complexity.rb
### Quadratic space (recursive) ###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # Array nums has length n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n

Figure 2-18   Quadratic order space complexity generated by recursive function

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#4-exponential-order-o2n","level":3,"title":"4.   Exponential Order \\(O(2^n)\\)","text":"

Exponential order is common in binary trees. Observe the following figure: a \"full binary tree\" with \\(n\\) levels has \\(2^n - 1\\) nodes, occupying \\(O(2^n)\\) space:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def build_tree(n: int) -> TreeNode | None:\n    \"\"\"Exponential order (build full binary tree)\"\"\"\n    if n == 0:\n        return None\n    root = TreeNode(0)\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n    return root\n
space_complexity.cpp
/* Driver Code */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return nullptr;\n    TreeNode *root = new TreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.java
/* Driver Code */\nTreeNode buildTree(int n) {\n    if (n == 0)\n        return null;\n    TreeNode root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.cs
/* Driver Code */\nTreeNode? BuildTree(int n) {\n    if (n == 0) return null;\n    TreeNode root = new(0) {\n        left = BuildTree(n - 1),\n        right = BuildTree(n - 1)\n    };\n    return root;\n}\n
space_complexity.go
/* Driver Code */\nfunc buildTree(n int) *TreeNode {\n    if n == 0 {\n        return nil\n    }\n    root := NewTreeNode(0)\n    root.Left = buildTree(n - 1)\n    root.Right = buildTree(n - 1)\n    return root\n}\n
space_complexity.swift
/* Driver Code */\nfunc buildTree(n: Int) -> TreeNode? {\n    if n == 0 {\n        return nil\n    }\n    let root = TreeNode(x: 0)\n    root.left = buildTree(n: n - 1)\n    root.right = buildTree(n: n - 1)\n    return root\n}\n
space_complexity.js
/* Driver Code */\nfunction buildTree(n) {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.ts
/* Driver Code */\nfunction buildTree(n: number): TreeNode | null {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.dart
/* Driver Code */\nTreeNode? buildTree(int n) {\n  if (n == 0) return null;\n  TreeNode root = TreeNode(0);\n  root.left = buildTree(n - 1);\n  root.right = buildTree(n - 1);\n  return root;\n}\n
space_complexity.rs
/* Driver Code */\nfn build_tree(n: i32) -> Option<Rc<RefCell<TreeNode>>> {\n    if n == 0 {\n        return None;\n    };\n    let root = TreeNode::new(0);\n    root.borrow_mut().left = build_tree(n - 1);\n    root.borrow_mut().right = build_tree(n - 1);\n    return Some(root);\n}\n
space_complexity.c
/* Driver Code */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return NULL;\n    TreeNode *root = newTreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.kt
/* Driver Code */\nfun buildTree(n: Int): TreeNode? {\n    if (n == 0)\n        return null\n    val root = TreeNode(0)\n    root.left = buildTree(n - 1)\n    root.right = buildTree(n - 1)\n    return root\n}\n
space_complexity.rb
### Exponential space (build full binary tree) ###\ndef build_tree(n)\n  return if n == 0\n\n  TreeNode.new.tap do |root|\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n  end\nend\n

Figure 2-19   Exponential order space complexity generated by full binary tree

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#5-logarithmic-order-olog-n","level":3,"title":"5.   Logarithmic Order \\(O(\\log n)\\)","text":"

Logarithmic order is common in divide-and-conquer algorithms. For example, merge sort: given an input array of length \\(n\\), each recursion divides the array in half from the midpoint, forming a recursion tree of height \\(\\log n\\), using \\(O(\\log n)\\) stack frame space.

Another example is converting a number to a string. Given a positive integer \\(n\\), it has \\(\\lfloor \\log_{10} n \\rfloor + 1\\) digits, i.e., the corresponding string length is \\(\\lfloor \\log_{10} n \\rfloor + 1\\), so the space complexity is \\(O(\\log_{10} n + 1) = O(\\log n)\\).

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#244-trading-time-for-space","level":2,"title":"2.4.4   Trading Time for Space","text":"

Ideally, we hope that both the time complexity and space complexity of an algorithm can reach optimal. However, in practice, optimizing both time complexity and space complexity simultaneously is usually very difficult.

Reducing time complexity usually comes at the cost of increasing space complexity, and vice versa. The approach of sacrificing memory space to improve algorithm execution speed is called \"trading space for time\"; conversely, it is called \"trading time for space\".

The choice of which approach depends on which aspect we value more. In most cases, time is more precious than space, so \"trading space for time\" is usually the more common strategy. Of course, when the data volume is very large, controlling space complexity is also very important.

","path":["Chapter 2. Complexity Analysis","2.4   Space Complexity"],"tags":[]},{"location":"chapter_computational_complexity/summary/","level":1,"title":"2.5   Summary","text":"","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"

Algorithm Efficiency Assessment

  • Time efficiency and space efficiency are the two primary evaluation metrics for measuring algorithm performance.
  • We can evaluate algorithm efficiency through actual testing, but it is difficult to eliminate the influence of the testing environment, and it consumes substantial computational resources.
  • Complexity analysis can eliminate the drawbacks of actual testing, with results applicable to all running platforms, and it can reveal algorithm efficiency under different data scales.

Time Complexity

  • Time complexity is used to measure the trend of algorithm runtime as data volume increases. It can effectively evaluate algorithm efficiency, but may fail in certain situations, such as when the input data volume is small or when time complexities are identical, making it impossible to precisely compare algorithm efficiency.
  • Worst-case time complexity is represented using Big \\(O\\) notation, corresponding to the asymptotic upper bound of a function, reflecting the growth level of the number of operations \\(T(n)\\) as \\(n\\) approaches positive infinity.
  • Deriving time complexity involves two steps: first, counting the number of operations, then determining the asymptotic upper bound.
  • Common time complexities arranged from low to high include \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n \\log n)\\), \\(O(n^2)\\), \\(O(2^n)\\), and \\(O(n!)\\).
  • The time complexity of some algorithms is not fixed, but rather depends on the distribution of input data. Time complexity is divided into worst-case, best-case, and average-case time complexity. Best-case time complexity is rarely used because input data generally needs to satisfy strict conditions to achieve the best case.
  • Average time complexity reflects the algorithm's runtime efficiency under random data input, and is closest to the algorithm's performance in practical applications. Calculating average time complexity requires statistical analysis of input data distribution and the combined mathematical expectation.

Space Complexity

  • Space complexity serves a similar purpose to time complexity, used to measure the trend of algorithm memory usage as data volume increases.
  • The memory space related to algorithm execution can be divided into input space, temporary space, and output space. Typically, input space is not included in space complexity calculations. Temporary space can be divided into temporary data, stack frame space, and instruction space, where stack frame space usually affects space complexity only in recursive functions.
  • We typically only focus on worst-case space complexity, which is the space complexity of an algorithm under worst-case input data and worst-case runtime.
  • Common space complexities arranged from low to high include \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n^2)\\), and \\(O(2^n)\\).
","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Is the space complexity of tail recursion \\(O(1)\\)?

Theoretically, the space complexity of tail recursive functions can be optimized to \\(O(1)\\). However, most programming languages (such as Java, Python, C++, Go, C#, etc.) do not support automatic tail recursion optimization, so the space complexity is generally considered to be \\(O(n)\\).

Q: What is the difference between the terms function and method?

A function can be executed independently, with all parameters passed explicitly. A method is associated with an object, is implicitly passed to the object that invokes it, and can operate on data contained in class instances.

The following examples use several common programming languages for illustration.

  • C is a procedural programming language without object-oriented concepts, so it only has functions. However, we can simulate object-oriented programming by creating structures (struct), and functions associated with structures are equivalent to methods in other programming languages.
  • Java and C# are object-oriented programming languages where code blocks (methods) are typically part of a class. Static methods behave like functions because they are bound to the class and cannot access specific instance variables.
  • C++ and Python support both procedural programming (functions) and object-oriented programming (methods).

Q: Does the diagram for \"common space complexity types\" reflect the absolute size of occupied space?

No, the diagram shows space complexity, which reflects growth trends rather than the absolute size of occupied space.

Assuming \\(n = 8\\), you might find that the values of each curve do not correspond to the functions. This is because each curve contains a constant term used to compress the value range into a visually comfortable range.

In practice, because we generally do not know what the \"constant term\" complexity of each method is, we usually cannot select the optimal solution for \\(n = 8\\) based on complexity alone. But for \\(n = 8^5\\), the choice is straightforward, as the growth trend already dominates.

Q: Are there situations where algorithms are designed to sacrifice time (or space) based on actual use cases?

In practical applications, most situations choose to sacrifice space for time. For example, with database indexes, we typically choose to build B+ trees or hash indexes, occupying substantial memory space in exchange for efficient queries of \\(O(\\log n)\\) or even \\(O(1)\\).

In scenarios where space resources are precious, time may be sacrificed for space. For example, in embedded development, device memory is precious, and engineers may forgo using hash tables and choose to use array sequential search to save memory usage, at the cost of slower searches.

","path":["Chapter 2. Complexity Analysis","2.5   Summary"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   Time Complexity","text":"

Runtime can intuitively and accurately reflect the efficiency of an algorithm. If we want to accurately estimate the runtime of a piece of code, how should we proceed?

  1. Determine the running platform, including hardware configuration, programming language, system environment, etc., as these factors all affect code execution efficiency.
  2. Evaluate the runtime required for various computational operations, for example, an addition operation + requires 1 ns, a multiplication operation * requires 10 ns, a print operation print() requires 5 ns, etc.
  3. Count all computational operations in the code, and sum the execution times of all operations to obtain the runtime.

For example, in the following code, the input data size is \\(n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# On a certain running platform\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # Loop n times\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
// On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
// On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
// On a certain running platform\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
// On a certain running platform\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // Loop n times\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
// On a certain running platform\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // Loop n times\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
// On a certain running platform\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // Loop n times\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
// On a certain running platform\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // Loop n times\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
// On a certain running platform\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // Loop n times\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
// On a certain running platform\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // Loop n times\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
// On a certain running platform\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // Loop n times\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
// On a certain running platform\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // Loop n times\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
# On a certain running platform\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # Loop n times\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

According to the above method, the algorithm's runtime can be obtained as \\((6n + 12)\\) ns:

\\[ 1 + 1 + 10 + (1 + 5) \\times n = 6n + 12 \\]

In reality, however, counting an algorithm's runtime is neither reasonable nor realistic. First, we do not want to tie the estimated time to the running platform, because algorithms need to run on various different platforms. Second, it is difficult to know the runtime of each type of operation, which brings great difficulty to the estimation process.

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#231-counting-time-growth-trends","level":2,"title":"2.3.1   Counting Time Growth Trends","text":"

Time complexity analysis does not count the algorithm's runtime, but rather counts the growth trend of the algorithm's runtime as the data volume increases.

The concept of \"time growth trend\" is rather abstract; let us understand it through an example. Suppose the input data size is \\(n\\), and given three algorithms A, B, and C:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# Time complexity of algorithm A: constant order\ndef algorithm_A(n: int):\n    print(0)\n# Time complexity of algorithm B: linear order\ndef algorithm_B(n: int):\n    for _ in range(n):\n        print(0)\n# Time complexity of algorithm C: constant order\ndef algorithm_C(n: int):\n    for _ in range(1000000):\n        print(0)\n
// Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    cout << 0 << endl;\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        cout << 0 << endl;\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        cout << 0 << endl;\n    }\n}\n
// Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    System.out.println(0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        System.out.println(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        System.out.println(0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nvoid AlgorithmA(int n) {\n    Console.WriteLine(0);\n}\n// Time complexity of algorithm B: linear order\nvoid AlgorithmB(int n) {\n    for (int i = 0; i < n; i++) {\n        Console.WriteLine(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid AlgorithmC(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        Console.WriteLine(0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nfunc algorithm_A(n int) {\n    fmt.Println(0)\n}\n// Time complexity of algorithm B: linear order\nfunc algorithm_B(n int) {\n    for i := 0; i < n; i++ {\n        fmt.Println(0)\n    }\n}\n// Time complexity of algorithm C: constant order\nfunc algorithm_C(n int) {\n    for i := 0; i < 1000000; i++ {\n        fmt.Println(0)\n    }\n}\n
// Time complexity of algorithm A: constant order\nfunc algorithmA(n: Int) {\n    print(0)\n}\n\n// Time complexity of algorithm B: linear order\nfunc algorithmB(n: Int) {\n    for _ in 0 ..< n {\n        print(0)\n    }\n}\n\n// Time complexity of algorithm C: constant order\nfunc algorithmC(n: Int) {\n    for _ in 0 ..< 1_000_000 {\n        print(0)\n    }\n}\n
// Time complexity of algorithm A: constant order\nfunction algorithm_A(n) {\n    console.log(0);\n}\n// Time complexity of algorithm B: linear order\nfunction algorithm_B(n) {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfunction algorithm_C(n) {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nfunction algorithm_A(n: number): void {\n    console.log(0);\n}\n// Time complexity of algorithm B: linear order\nfunction algorithm_B(n: number): void {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfunction algorithm_C(n: number): void {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nvoid algorithmA(int n) {\n  print(0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithmB(int n) {\n  for (int i = 0; i < n; i++) {\n    print(0);\n  }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithmC(int n) {\n  for (int i = 0; i < 1000000; i++) {\n    print(0);\n  }\n}\n
// Time complexity of algorithm A: constant order\nfn algorithm_A(n: i32) {\n    println!(\"{}\", 0);\n}\n// Time complexity of algorithm B: linear order\nfn algorithm_B(n: i32) {\n    for _ in 0..n {\n        println!(\"{}\", 0);\n    }\n}\n// Time complexity of algorithm C: constant order\nfn algorithm_C(n: i32) {\n    for _ in 0..1000000 {\n        println!(\"{}\", 0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nvoid algorithm_A(int n) {\n    printf(\"%d\", 0);\n}\n// Time complexity of algorithm B: linear order\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        printf(\"%d\", 0);\n    }\n}\n// Time complexity of algorithm C: constant order\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        printf(\"%d\", 0);\n    }\n}\n
// Time complexity of algorithm A: constant order\nfun algoritm_A(n: Int) {\n    println(0)\n}\n// Time complexity of algorithm B: linear order\nfun algorithm_B(n: Int) {\n    for (i in 0..<n){\n        println(0)\n    }\n}\n// Time complexity of algorithm C: constant order\nfun algorithm_C(n: Int) {\n    for (i in 0..<1000000) {\n        println(0)\n    }\n}\n
# Time complexity of algorithm A: constant order\ndef algorithm_A(n)\n    puts 0\nend\n\n# Time complexity of algorithm B: linear order\ndef algorithm_B(n)\n    (0...n).each { puts 0 }\nend\n\n# Time complexity of algorithm C: constant order\ndef algorithm_C(n)\n    (0...1_000_000).each { puts 0 }\nend\n

Figure 2-7 shows the time complexity of the above three algorithm functions.

  • Algorithm A has only \\(1\\) print operation, and the algorithm's runtime does not grow as \\(n\\) increases. We call the time complexity of this algorithm \"constant order\".
  • In algorithm B, the print operation needs to loop \\(n\\) times, and the algorithm's runtime grows linearly as \\(n\\) increases. The time complexity of this algorithm is called \"linear order\".
  • In algorithm C, the print operation needs to loop \\(1000000\\) times. Although the runtime is very long, it is independent of the input data size \\(n\\). Therefore, the time complexity of C is the same as A, still \"constant order\".

Figure 2-7   Time growth trends of algorithms A, B, and C

Compared to directly counting the algorithm's runtime, what are the characteristics of time complexity analysis?

  • Time complexity can effectively evaluate algorithm efficiency. For example, the runtime of algorithm B grows linearly; when \\(n > 1\\) it is slower than algorithm A, and when \\(n > 1000000\\) it is slower than algorithm C. In fact, as long as the input data size \\(n\\) is sufficiently large, an algorithm with \"constant order\" complexity will always be superior to one with \"linear order\" complexity, which is precisely the meaning of time growth trend.
  • The derivation method for time complexity is simpler. Obviously, the running platform and the types of computational operations are both unrelated to the growth trend of the algorithm's runtime. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same \"unit time\", thus simplifying \"counting computational operation runtime\" to \"counting the number of computational operations\", which greatly reduces the difficulty of estimation.
  • Time complexity also has certain limitations. For example, although algorithms A and C have the same time complexity, their actual runtimes differ significantly. Similarly, although algorithm B has a higher time complexity than C, when the input data size \\(n\\) is small, algorithm B is clearly superior to algorithm C. In such cases, it is often difficult to judge the efficiency of algorithms based solely on time complexity. Of course, despite the above issues, complexity analysis remains the most effective and commonly used method for evaluating algorithm efficiency.
","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#232-asymptotic-upper-bound-of-functions","level":2,"title":"2.3.2   Asymptotic Upper Bound of Functions","text":"

Given a function with input size \\(n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 1      # +1\n    a = a + 1  # +1\n    a = a * 2  # +1\n    # Loop n times\n    for i in range(n):  # +1\n        print(0)        # +1\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Loop n times\n    for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\n        cout << 0 << endl;    // +1\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Loop n times\n    for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\n        System.out.println(0);    // +1\n    }\n}\n
void Algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Loop n times\n    for (int i = 0; i < n; i++) {   // +1 (i++ is executed each round)\n        Console.WriteLine(0);   // +1\n    }\n}\n
func algorithm(n int) {\n    a := 1      // +1\n    a = a + 1   // +1\n    a = a * 2   // +1\n    // Loop n times\n    for i := 0; i < n; i++ {   // +1\n        fmt.Println(a)         // +1\n    }\n}\n
func algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // Loop n times\n    for _ in 0 ..< n { // +1\n        print(0) // +1\n    }\n}\n
function algorithm(n) {\n    var a = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // Loop n times\n    for(let i = 0; i < n; i++){ // +1 (i++ is executed each round)\n        console.log(0); // +1\n    }\n}\n
function algorithm(n: number): void{\n    var a: number = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // Loop n times\n    for(let i = 0; i < n; i++){ // +1 (i++ is executed each round)\n        console.log(0); // +1\n    }\n}\n
void algorithm(int n) {\n  int a = 1; // +1\n  a = a + 1; // +1\n  a = a * 2; // +1\n  // Loop n times\n  for (int i = 0; i < n; i++) { // +1 (i++ is executed each round)\n    print(0); // +1\n  }\n}\n
fn algorithm(n: i32) {\n    let mut a = 1;   // +1\n    a = a + 1;      // +1\n    a = a * 2;      // +1\n\n    // Loop n times\n    for _ in 0..n { // +1 (i++ is executed each round)\n        println!(\"{}\", 0); // +1\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // Loop n times\n    for (int i = 0; i < n; i++) {   // +1 (i++ is executed each round)\n        printf(\"%d\", 0);            // +1\n    }\n}\n
fun algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // Loop n times\n    for (i in 0..<n) { // +1 (i++ is executed each round)\n        println(0) // +1\n    }\n}\n
def algorithm(n)\n    a = 1       # +1\n    a = a + 1   # +1\n    a = a * 2   # +1\n    # Loop n times\n    (0...n).each do # +1\n        puts 0      # +1\n    end\nend\n

Let the number of operations of the algorithm be a function of the input data size \\(n\\), denoted as \\(T(n)\\). Then the number of operations of the above function is:

\\[ T(n) = 3 + 2n \\]

\\(T(n)\\) is a linear function, indicating that its runtime growth trend is linear, and therefore its time complexity is linear order.

We denote the time complexity of linear order as \\(O(n)\\). This mathematical symbol is called big-\\(O\\) notation, representing the asymptotic upper bound of the function \\(T(n)\\).

Time complexity analysis essentially calculates the asymptotic upper bound of \"the number of operations \\(T(n)\\)\", which has a clear mathematical definition.

Asymptotic upper bound of functions

If there exist positive real numbers \\(c\\) and \\(n_0\\) such that for all \\(n > n_0\\), we have \\(T(n) \\leq c \\cdot f(n)\\), then \\(f(n)\\) can be considered as an asymptotic upper bound of \\(T(n)\\), denoted as \\(T(n) = O(f(n))\\).

As shown in Figure 2-8, calculating the asymptotic upper bound is to find a function \\(f(n)\\) such that when \\(n\\) tends to infinity, \\(T(n)\\) and \\(f(n)\\) are at the same growth level, differing only by a constant coefficient \\(c\\).

Figure 2-8   Asymptotic upper bound of a function

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#233-derivation-method","level":2,"title":"2.3.3   Derivation Method","text":"

The asymptotic upper bound has a bit of mathematical flavor. If you feel you haven't fully understood it, don't worry. We can first master the derivation method, and gradually grasp its mathematical meaning through continuous practice.

According to the definition, after determining \\(f(n)\\), we can obtain the time complexity \\(O(f(n))\\). So how do we determine the asymptotic upper bound \\(f(n)\\)? Overall, it is divided into two steps: first count the number of operations, then determine the asymptotic upper bound.

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-step-1-count-the-number-of-operations","level":3,"title":"1.   Step 1: Count the Number of Operations","text":"

For code, count from top to bottom line by line. However, since the constant coefficient \\(c\\) in \\(c \\cdot f(n)\\) above can be of any size, coefficients and constant terms in the number of operations \\(T(n)\\) can all be ignored. According to this principle, the following counting simplification techniques can be summarized.

  1. Ignore constants in \\(T(n)\\). Because they are all independent of \\(n\\), they do not affect time complexity.
  2. Omit all coefficients. For example, looping \\(2n\\) times, \\(5n + 1\\) times, etc., can all be simplified as \\(n\\) times, because the coefficient before \\(n\\) does not affect time complexity.
  3. Use multiplication for nested loops. The total number of operations equals the product of the number of operations in the outer and inner loops, with each layer of loop still able to apply techniques 1. and 2. separately.

Given a function, we can use the above techniques to count the number of operations:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 1      # +0 (Technique 1)\n    a = a + n  # +0 (Technique 1)\n    # +n (Technique 2)\n    for i in range(5 * n + 1):\n        print(0)\n    # +n*n (Technique 3)\n    for i in range(2 * n):\n        for j in range(n + 1):\n            print(0)\n
void algorithm(int n) {\n    int a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        cout << 0 << endl;\n    }\n    // +n*n (Technique 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            cout << 0 << endl;\n        }\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        System.out.println(0);\n    }\n    // +n*n (Technique 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            System.out.println(0);\n        }\n    }\n}\n
void Algorithm(int n) {\n    int a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        Console.WriteLine(0);\n    }\n    // +n*n (Technique 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            Console.WriteLine(0);\n        }\n    }\n}\n
func algorithm(n int) {\n    a := 1     // +0 (Technique 1)\n    a = a + n  // +0 (Technique 1)\n    // +n (Technique 2)\n    for i := 0; i < 5 * n + 1; i++ {\n        fmt.Println(0)\n    }\n    // +n*n (Technique 3)\n    for i := 0; i < 2 * n; i++ {\n        for j := 0; j < n + 1; j++ {\n            fmt.Println(0)\n        }\n    }\n}\n
func algorithm(n: Int) {\n    var a = 1 // +0 (Technique 1)\n    a = a + n // +0 (Technique 1)\n    // +n (Technique 2)\n    for _ in 0 ..< (5 * n + 1) {\n        print(0)\n    }\n    // +n*n (Technique 3)\n    for _ in 0 ..< (2 * n) {\n        for _ in 0 ..< (n + 1) {\n            print(0)\n        }\n    }\n}\n
function algorithm(n) {\n    let a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (Technique 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
function algorithm(n: number): void {\n    let a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n (Technique 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
void algorithm(int n) {\n  int a = 1; // +0 (Technique 1)\n  a = a + n; // +0 (Technique 1)\n  // +n (Technique 2)\n  for (int i = 0; i < 5 * n + 1; i++) {\n    print(0);\n  }\n  // +n*n (Technique 3)\n  for (int i = 0; i < 2 * n; i++) {\n    for (int j = 0; j < n + 1; j++) {\n      print(0);\n    }\n  }\n}\n
fn algorithm(n: i32) {\n    let mut a = 1;     // +0 (Technique 1)\n    a = a + n;        // +0 (Technique 1)\n\n    // +n (Technique 2)\n    for i in 0..(5 * n + 1) {\n        println!(\"{}\", 0);\n    }\n\n    // +n*n (Technique 3)\n    for i in 0..(2 * n) {\n        for j in 0..(n + 1) {\n            println!(\"{}\", 0);\n        }\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +0 (Technique 1)\n    a = a + n;  // +0 (Technique 1)\n    // +n (Technique 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        printf(\"%d\", 0);\n    }\n    // +n*n (Technique 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            printf(\"%d\", 0);\n        }\n    }\n}\n
fun algorithm(n: Int) {\n    var a = 1   // +0 (Technique 1)\n    a = a + n   // +0 (Technique 1)\n    // +n (Technique 2)\n    for (i in 0..<5 * n + 1) {\n        println(0)\n    }\n    // +n*n (Technique 3)\n    for (i in 0..<2 * n) {\n        for (j in 0..<n + 1) {\n            println(0)\n        }\n    }\n}\n
def algorithm(n)\n    a = 1       # +0 (Technique 1)\n    a = a + n   # +0 (Technique 1)\n    # +n (Technique 2)\n    (0...(5 * n + 1)).each do { puts 0 }\n    # +n*n (Technique 3)\n    (0...(2 * n)).each do\n        (0...(n + 1)).each do { puts 0 }\n    end\nend\n

The following formula shows the counting results before and after using the above techniques; both derive a time complexity of \\(O(n^2)\\).

\\[ \\begin{aligned} T(n) & = 2n(n + 1) + (5n + 1) + 2 & \\text{Complete count (-.-|||)} \\newline & = 2n^2 + 7n + 3 \\newline T(n) & = n^2 + n & \\text{Simplified count (o.O)} \\end{aligned} \\]","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-step-2-determine-the-asymptotic-upper-bound","level":3,"title":"2.   Step 2: Determine the Asymptotic Upper Bound","text":"

Time complexity is determined by the highest-order term in \\(T(n)\\). This is because as \\(n\\) tends to infinity, the highest-order term will play a dominant role, and the influence of other terms can be ignored.

Table 2-2 shows some examples, where some exaggerated values are used to emphasize the conclusion that \"coefficients cannot shake the order\". When \\(n\\) tends to infinity, these constants become insignificant.

Table 2-2   Time complexities corresponding to different numbers of operations

Number of Operations \\(T(n)\\) Time Complexity \\(O(f(n))\\) \\(100000\\) \\(O(1)\\) \\(3n + 2\\) \\(O(n)\\) \\(2n^2 + 3n + 2\\) \\(O(n^2)\\) \\(n^3 + 10000n^2\\) \\(O(n^3)\\) \\(2^n + 10000n^{10000}\\) \\(O(2^n)\\)","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#234-common-types","level":2,"title":"2.3.4   Common Types","text":"

Let the input data size be \\(n\\). Common time complexity types are shown in Figure 2-9 (arranged in order from low to high).

\\[ \\begin{aligned} O(1) < O(\\log n) < O(n) < O(n \\log n) < O(n^2) < O(2^n) < O(n!) \\newline \\text{Constant order} < \\text{Logarithmic order} < \\text{Linear order} < \\text{Linearithmic order} < \\text{Quadratic order} < \\text{Exponential order} < \\text{Factorial order} \\end{aligned} \\]

Figure 2-9   Common time complexity types

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-constant-order-o1","level":3,"title":"1.   Constant Order \\(O(1)\\)","text":"

The number of operations in constant order is independent of the input data size \\(n\\), meaning it does not change as \\(n\\) changes.

In the following function, although the number of operations size may be large, since it is independent of the input data size \\(n\\), the time complexity remains \\(O(1)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def constant(n: int) -> int:\n    \"\"\"Constant order\"\"\"\n    count = 0\n    size = 100000\n    for _ in range(size):\n        count += 1\n    return count\n
time_complexity.cpp
/* Constant order */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.java
/* Constant order */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.cs
/* Constant order */\nint Constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.go
/* Constant order */\nfunc constant(n int) int {\n    count := 0\n    size := 100000\n    for i := 0; i < size; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* Constant order */\nfunc constant(n: Int) -> Int {\n    var count = 0\n    let size = 100_000\n    for _ in 0 ..< size {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* Constant order */\nfunction constant(n) {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
time_complexity.ts
/* Constant order */\nfunction constant(n: number): number {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
time_complexity.dart
/* Constant order */\nint constant(int n) {\n  int count = 0;\n  int size = 100000;\n  for (var i = 0; i < size; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* Constant order */\nfn constant(n: i32) -> i32 {\n    _ = n;\n    let mut count = 0;\n    let size = 100_000;\n    for _ in 0..size {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* Constant order */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    int i = 0;\n    for (int i = 0; i < size; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* Constant order */\nfun constant(n: Int): Int {\n    var count = 0\n    val size = 100000\n    for (i in 0..<size)\n        count++\n    return count\n}\n
time_complexity.rb
### Constant time ###\ndef constant(n)\n  count = 0\n  size = 100000\n\n  (0...size).each { count += 1 }\n\n  count\nend\n
","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-linear-order-on","level":3,"title":"2.   Linear Order \\(O(n)\\)","text":"

The number of operations in linear order grows linearly relative to the input data size \\(n\\). Linear order typically appears in single-layer loops:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def linear(n: int) -> int:\n    \"\"\"Linear order\"\"\"\n    count = 0\n    for _ in range(n):\n        count += 1\n    return count\n
time_complexity.cpp
/* Linear order */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.java
/* Linear order */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.cs
/* Linear order */\nint Linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.go
/* Linear order */\nfunc linear(n int) int {\n    count := 0\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* Linear order */\nfunc linear(n: Int) -> Int {\n    var count = 0\n    for _ in 0 ..< n {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* Linear order */\nfunction linear(n) {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
time_complexity.ts
/* Linear order */\nfunction linear(n: number): number {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
time_complexity.dart
/* Linear order */\nint linear(int n) {\n  int count = 0;\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* Linear order */\nfn linear(n: i32) -> i32 {\n    let mut count = 0;\n    for _ in 0..n {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* Linear order */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* Linear order */\nfun linear(n: Int): Int {\n    var count = 0\n    for (i in 0..<n)\n        count++\n    return count\n}\n
time_complexity.rb
### Linear time ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n

Operations such as traversing arrays and traversing linked lists have a time complexity of \\(O(n)\\), where \\(n\\) is the length of the array or linked list:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def array_traversal(nums: list[int]) -> int:\n    \"\"\"Linear order (traversing array)\"\"\"\n    count = 0\n    # Number of iterations is proportional to the array length\n    for num in nums:\n        count += 1\n    return count\n
time_complexity.cpp
/* Linear order (traversing array) */\nint arrayTraversal(vector<int> &nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* Linear order (traversing array) */\nint arrayTraversal(int[] nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* Linear order (traversing array) */\nint ArrayTraversal(int[] nums) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    foreach (int num in nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* Linear order (traversing array) */\nfunc arrayTraversal(nums []int) int {\n    count := 0\n    // Number of iterations is proportional to the array length\n    for range nums {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* Linear order (traversing array) */\nfunc arrayTraversal(nums: [Int]) -> Int {\n    var count = 0\n    // Number of iterations is proportional to the array length\n    for _ in nums {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* Linear order (traversing array) */\nfunction arrayTraversal(nums) {\n    let count = 0;\n    // Number of iterations is proportional to the array length\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* Linear order (traversing array) */\nfunction arrayTraversal(nums: number[]): number {\n    let count = 0;\n    // Number of iterations is proportional to the array length\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* Linear order (traversing array) */\nint arrayTraversal(List<int> nums) {\n  int count = 0;\n  // Number of iterations is proportional to the array length\n  for (var _num in nums) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* Linear order (traversing array) */\nfn array_traversal(nums: &[i32]) -> i32 {\n    let mut count = 0;\n    // Number of iterations is proportional to the array length\n    for _ in nums {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* Linear order (traversing array) */\nint arrayTraversal(int *nums, int n) {\n    int count = 0;\n    // Number of iterations is proportional to the array length\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* Linear order (traversing array) */\nfun arrayTraversal(nums: IntArray): Int {\n    var count = 0\n    // Number of iterations is proportional to the array length\n    for (num in nums) {\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### Linear time (array traversal) ###\ndef array_traversal(nums)\n  count = 0\n\n  # Number of iterations is proportional to the array length\n  for num in nums\n    count += 1\n  end\n\n  count\nend\n

It is worth noting that the input data size \\(n\\) should be determined according to the type of input data. For example, in the first example, the variable \\(n\\) is the input data size; in the second example, the array length \\(n\\) is the data size.

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#3-quadratic-order-on2","level":3,"title":"3.   Quadratic Order \\(O(n^2)\\)","text":"

The number of operations in quadratic order grows quadratically relative to the input data size \\(n\\). Quadratic order typically appears in nested loops, where both the outer and inner loops have a time complexity of \\(O(n)\\), resulting in an overall time complexity of \\(O(n^2)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def quadratic(n: int) -> int:\n    \"\"\"Quadratic order\"\"\"\n    count = 0\n    # Number of iterations is quadratically related to the data size n\n    for i in range(n):\n        for j in range(n):\n            count += 1\n    return count\n
time_complexity.cpp
/* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.java
/* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.cs
/* Exponential order */\nint Quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.go
/* Exponential order */\nfunc quadratic(n int) int {\n    count := 0\n    // Number of iterations is quadratically related to the data size n\n    for i := 0; i < n; i++ {\n        for j := 0; j < n; j++ {\n            count++\n        }\n    }\n    return count\n}\n
time_complexity.swift
/* Exponential order */\nfunc quadratic(n: Int) -> Int {\n    var count = 0\n    // Number of iterations is quadratically related to the data size n\n    for _ in 0 ..< n {\n        for _ in 0 ..< n {\n            count += 1\n        }\n    }\n    return count\n}\n
time_complexity.js
/* Exponential order */\nfunction quadratic(n) {\n    let count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.ts
/* Exponential order */\nfunction quadratic(n: number): number {\n    let count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.dart
/* Exponential order */\nint quadratic(int n) {\n  int count = 0;\n  // Number of iterations is quadratically related to the data size n\n  for (int i = 0; i < n; i++) {\n    for (int j = 0; j < n; j++) {\n      count++;\n    }\n  }\n  return count;\n}\n
time_complexity.rs
/* Exponential order */\nfn quadratic(n: i32) -> i32 {\n    let mut count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for _ in 0..n {\n        for _ in 0..n {\n            count += 1;\n        }\n    }\n    count\n}\n
time_complexity.c
/* Exponential order */\nint quadratic(int n) {\n    int count = 0;\n    // Number of iterations is quadratically related to the data size n\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.kt
/* Exponential order */\nfun quadratic(n: Int): Int {\n    var count = 0\n    // Number of iterations is quadratically related to the data size n\n    for (i in 0..<n) {\n        for (j in 0..<n) {\n            count++\n        }\n    }\n    return count\n}\n
time_complexity.rb
### Quadratic time ###\ndef quadratic(n)\n  count = 0\n\n  # Number of iterations is quadratically related to the data size n\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n

Figure 2-10 compares constant order, linear order, and quadratic order time complexities.

Figure 2-10   Time complexities of constant, linear, and quadratic orders

Taking bubble sort as an example, the outer loop executes \\(n - 1\\) times, and the inner loop executes \\(n-1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), \\(1\\) times, averaging \\(n / 2\\) times, resulting in a time complexity of \\(O((n - 1) n / 2) = O(n^2)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def bubble_sort(nums: list[int]) -> int:\n    \"\"\"Quadratic order (bubble sort)\"\"\"\n    count = 0  # Counter\n    # Outer loop: unsorted range is [0, i]\n    for i in range(len(nums) - 1, 0, -1):\n        # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                tmp: int = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3  # Element swap includes 3 unit operations\n    return count\n
time_complexity.cpp
/* Quadratic order (bubble sort) */\nint bubbleSort(vector<int> &nums) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.java
/* Quadratic order (bubble sort) */\nint bubbleSort(int[] nums) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.cs
/* Quadratic order (bubble sort) */\nint BubbleSort(int[] nums) {\n    int count = 0;  // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                count += 3;  // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.go
/* Quadratic order (bubble sort) */\nfunc bubbleSort(nums []int) int {\n    count := 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                tmp := nums[j]\n                nums[j] = nums[j+1]\n                nums[j+1] = tmp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
time_complexity.swift
/* Quadratic order (bubble sort) */\nfunc bubbleSort(nums: inout [Int]) -> Int {\n    var count = 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
time_complexity.js
/* Quadratic order (bubble sort) */\nfunction bubbleSort(nums) {\n    let count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.ts
/* Quadratic order (bubble sort) */\nfunction bubbleSort(nums: number[]): number {\n    let count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.dart
/* Quadratic order (bubble sort) */\nint bubbleSort(List<int> nums) {\n  int count = 0; // Counter\n  // Outer loop: unsorted range is [0, i]\n  for (var i = nums.length - 1; i > 0; i--) {\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (var j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        count += 3; // Element swap includes 3 unit operations\n      }\n    }\n  }\n  return count;\n}\n
time_complexity.rs
/* Quadratic order (bubble sort) */\nfn bubble_sort(nums: &mut [i32]) -> i32 {\n    let mut count = 0; // Counter\n\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    count\n}\n
time_complexity.c
/* Quadratic order (bubble sort) */\nint bubbleSort(int *nums, int n) {\n    int count = 0; // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (int i = n - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.kt
/* Quadratic order (bubble sort) */\nfun bubbleSort(nums: IntArray): Int {\n    var count = 0 // Counter\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                count += 3 // Element swap includes 3 unit operations\n            }\n        }\n    }\n    return count\n}\n
time_complexity.rb
### Quadratic time (bubble sort) ###\ndef bubble_sort(nums)\n  count = 0  # Counter\n\n  # Outer loop: unsorted range is [0, i]\n  for i in (nums.length - 1).downto(0)\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # Element swap includes 3 unit operations\n      end\n    end\n  end\n\n  count\nend\n
","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#4-exponential-order-o2n","level":3,"title":"4.   Exponential Order \\(O(2^n)\\)","text":"

Biological \"cell division\" is a typical example of exponential order growth: the initial state is \\(1\\) cell, after one round of division it becomes \\(2\\), after two rounds it becomes \\(4\\), and so on; after \\(n\\) rounds of division there are \\(2^n\\) cells.

Figure 2-11 and the following code simulate the cell division process, with a time complexity of \\(O(2^n)\\). Note that the input \\(n\\) represents the number of division rounds, and the return value count represents the total number of divisions.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def exponential(n: int) -> int:\n    \"\"\"Exponential order (loop implementation)\"\"\"\n    count = 0\n    base = 1\n    # Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in range(n):\n        for _ in range(base):\n            count += 1\n        base *= 2\n    # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n
time_complexity.cpp
/* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.java
/* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.cs
/* Exponential order (loop implementation) */\nint Exponential(int n) {\n    int count = 0, bas = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.go
/* Exponential order (loop implementation) */\nfunc exponential(n int) int {\n    count, base := 0, 1\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for i := 0; i < n; i++ {\n        for j := 0; j < base; j++ {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.swift
/* Exponential order (loop implementation) */\nfunc exponential(n: Int) -> Int {\n    var count = 0\n    var base = 1\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in 0 ..< n {\n        for _ in 0 ..< base {\n            count += 1\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.js
/* Exponential order (loop implementation) */\nfunction exponential(n) {\n    let count = 0,\n        base = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.ts
/* Exponential order (loop implementation) */\nfunction exponential(n: number): number {\n    let count = 0,\n        base = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.dart
/* Exponential order (loop implementation) */\nint exponential(int n) {\n  int count = 0, base = 1;\n  // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n  for (var i = 0; i < n; i++) {\n    for (var j = 0; j < base; j++) {\n      count++;\n    }\n    base *= 2;\n  }\n  // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  return count;\n}\n
time_complexity.rs
/* Exponential order (loop implementation) */\nfn exponential(n: i32) -> i32 {\n    let mut count = 0;\n    let mut base = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in 0..n {\n        for _ in 0..base {\n            count += 1\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    count\n}\n
time_complexity.c
/* Exponential order (loop implementation) */\nint exponential(int n) {\n    int count = 0;\n    int bas = 1;\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.kt
/* Exponential order (loop implementation) */\nfun exponential(n: Int): Int {\n    var count = 0\n    var base = 1\n    // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n    for (i in 0..<n) {\n        for (j in 0..<base) {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.rb
### Exponential time (iterative) ###\ndef exponential(n)\n  count, base = 0, 1\n\n  # Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1)\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n

Figure 2-11   Time complexity of exponential order

In actual algorithms, exponential order often appears in recursive functions. For example, in the following code, it recursively splits in two, stopping after \\(n\\) splits:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def exp_recur(n: int) -> int:\n    \"\"\"Exponential order (recursive implementation)\"\"\"\n    if n == 1:\n        return 1\n    return exp_recur(n - 1) + exp_recur(n - 1) + 1\n
time_complexity.cpp
/* Exponential order (recursive implementation) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.java
/* Exponential order (recursive implementation) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.cs
/* Exponential order (recursive implementation) */\nint ExpRecur(int n) {\n    if (n == 1) return 1;\n    return ExpRecur(n - 1) + ExpRecur(n - 1) + 1;\n}\n
time_complexity.go
/* Exponential order (recursive implementation) */\nfunc expRecur(n int) int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n-1) + expRecur(n-1) + 1\n}\n
time_complexity.swift
/* Exponential order (recursive implementation) */\nfunc expRecur(n: Int) -> Int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n: n - 1) + expRecur(n: n - 1) + 1\n}\n
time_complexity.js
/* Exponential order (recursive implementation) */\nfunction expRecur(n) {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.ts
/* Exponential order (recursive implementation) */\nfunction expRecur(n: number): number {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.dart
/* Exponential order (recursive implementation) */\nint expRecur(int n) {\n  if (n == 1) return 1;\n  return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.rs
/* Exponential order (recursive implementation) */\nfn exp_recur(n: i32) -> i32 {\n    if n == 1 {\n        return 1;\n    }\n    exp_recur(n - 1) + exp_recur(n - 1) + 1\n}\n
time_complexity.c
/* Exponential order (recursive implementation) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.kt
/* Exponential order (recursive implementation) */\nfun expRecur(n: Int): Int {\n    if (n == 1) {\n        return 1\n    }\n    return expRecur(n - 1) + expRecur(n - 1) + 1\n}\n
time_complexity.rb
### Exponential time (recursive) ###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n

Exponential order growth is very rapid and is common in exhaustive methods (brute force search, backtracking, etc.). For problems with large data scales, exponential order is unacceptable and typically requires dynamic programming or greedy algorithms to solve.

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#5-logarithmic-order-olog-n","level":3,"title":"5.   Logarithmic Order \\(O(\\log n)\\)","text":"

In contrast to exponential order, logarithmic order reflects the situation of \"reducing to half each round\". Let the input data size be \\(n\\). Since it is reduced to half each round, the number of loops is \\(\\log_2 n\\), which is the inverse function of \\(2^n\\).

Figure 2-12 and the following code simulate the process of \"reducing to half each round\", with a time complexity of \\(O(\\log_2 n)\\), abbreviated as \\(O(\\log n)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def logarithmic(n: int) -> int:\n    \"\"\"Logarithmic order (loop implementation)\"\"\"\n    count = 0\n    while n > 1:\n        n = n / 2\n        count += 1\n    return count\n
time_complexity.cpp
/* Logarithmic order (loop implementation) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* Logarithmic order (loop implementation) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* Logarithmic order (loop implementation) */\nint Logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n /= 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* Logarithmic order (loop implementation) */\nfunc logarithmic(n int) int {\n    count := 0\n    for n > 1 {\n        n = n / 2\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* Logarithmic order (loop implementation) */\nfunc logarithmic(n: Int) -> Int {\n    var count = 0\n    var n = n\n    while n > 1 {\n        n = n / 2\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* Logarithmic order (loop implementation) */\nfunction logarithmic(n) {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* Logarithmic order (loop implementation) */\nfunction logarithmic(n: number): number {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* Logarithmic order (loop implementation) */\nint logarithmic(int n) {\n  int count = 0;\n  while (n > 1) {\n    n = n ~/ 2;\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* Logarithmic order (loop implementation) */\nfn logarithmic(mut n: i32) -> i32 {\n    let mut count = 0;\n    while n > 1 {\n        n = n / 2;\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* Logarithmic order (loop implementation) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* Logarithmic order (loop implementation) */\nfun logarithmic(n: Int): Int {\n    var n1 = n\n    var count = 0\n    while (n1 > 1) {\n        n1 /= 2\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### Logarithmic time (iterative) ###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n

Figure 2-12   Time complexity of logarithmic order

Like exponential order, logarithmic order also commonly appears in recursive functions. The following code forms a recursion tree of height \\(\\log_2 n\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def log_recur(n: int) -> int:\n    \"\"\"Logarithmic order (recursive implementation)\"\"\"\n    if n <= 1:\n        return 0\n    return log_recur(n / 2) + 1\n
time_complexity.cpp
/* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.java
/* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.cs
/* Logarithmic order (recursive implementation) */\nint LogRecur(int n) {\n    if (n <= 1) return 0;\n    return LogRecur(n / 2) + 1;\n}\n
time_complexity.go
/* Logarithmic order (recursive implementation) */\nfunc logRecur(n int) int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n/2) + 1\n}\n
time_complexity.swift
/* Logarithmic order (recursive implementation) */\nfunc logRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n: n / 2) + 1\n}\n
time_complexity.js
/* Logarithmic order (recursive implementation) */\nfunction logRecur(n) {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.ts
/* Logarithmic order (recursive implementation) */\nfunction logRecur(n: number): number {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.dart
/* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n  if (n <= 1) return 0;\n  return logRecur(n ~/ 2) + 1;\n}\n
time_complexity.rs
/* Logarithmic order (recursive implementation) */\nfn log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 0;\n    }\n    log_recur(n / 2) + 1\n}\n
time_complexity.c
/* Logarithmic order (recursive implementation) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.kt
/* Logarithmic order (recursive implementation) */\nfun logRecur(n: Int): Int {\n    if (n <= 1)\n        return 0\n    return logRecur(n / 2) + 1\n}\n
time_complexity.rb
### Logarithmic time (recursive) ###\ndef log_recur(n)\n  return 0 unless n > 1\n  log_recur(n / 2) + 1\nend\n

Logarithmic order commonly appears in algorithms based on the divide-and-conquer strategy, embodying the algorithmic thinking of \"dividing into many\" and \"simplifying complexity\". It grows slowly and is the ideal time complexity second only to constant order.

What is the base of \\(O(\\log n)\\)?

To be precise, \"dividing into \\(m\\)\" corresponds to a time complexity of \\(O(\\log_m n)\\). And through the logarithmic base change formula, we can obtain time complexities with different bases that are equal:

\\[ O(\\log_m n) = O(\\log_k n / \\log_k m) = O(\\log_k n) \\]

That is to say, the base \\(m\\) can be converted without affecting the complexity. Therefore, we usually omit the base \\(m\\) and denote logarithmic order simply as \\(O(\\log n)\\).

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#6-linearithmic-order-on-log-n","level":3,"title":"6.   Linearithmic Order \\(O(n \\log n)\\)","text":"

Linearithmic order commonly appears in nested loops, where the time complexities of the two layers of loops are \\(O(\\log n)\\) and \\(O(n)\\) respectively. The relevant code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def linear_log_recur(n: int) -> int:\n    \"\"\"Linearithmic order\"\"\"\n    if n <= 1:\n        return 1\n    # Divide into two, the scale of subproblems is reduced by half\n    count = linear_log_recur(n // 2) + linear_log_recur(n // 2)\n    # Current subproblem contains n operations\n    for _ in range(n):\n        count += 1\n    return count\n
time_complexity.cpp
/* Linearithmic order */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* Linearithmic order */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* Linearithmic order */\nint LinearLogRecur(int n) {\n    if (n <= 1) return 1;\n    int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* Linearithmic order */\nfunc linearLogRecur(n int) int {\n    if n <= 1 {\n        return 1\n    }\n    count := linearLogRecur(n/2) + linearLogRecur(n/2)\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* Linearithmic order */\nfunc linearLogRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 1\n    }\n    var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2)\n    for _ in stride(from: 0, to: n, by: 1) {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* Linearithmic order */\nfunction linearLogRecur(n) {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* Linearithmic order */\nfunction linearLogRecur(n: number): number {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* Linearithmic order */\nint linearLogRecur(int n) {\n  if (n <= 1) return 1;\n  int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2);\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* Linearithmic order */\nfn linear_log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 1;\n    }\n    let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2);\n    for _ in 0..n {\n        count += 1;\n    }\n    return count;\n}\n
time_complexity.c
/* Linearithmic order */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* Linearithmic order */\nfun linearLogRecur(n: Int): Int {\n    if (n <= 1)\n        return 1\n    var count = linearLogRecur(n / 2) + linearLogRecur(n / 2)\n    for (i in 0..<n) {\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### Linearithmic time ###\ndef linear_log_recur(n)\n  return 1 unless n > 1\n\n  count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n  (0...n).each { count += 1 }\n\n  count\nend\n

Figure 2-13 shows how linearithmic order is generated. Each level of the binary tree has a total of \\(n\\) operations, and the tree has \\(\\log_2 n + 1\\) levels, resulting in a time complexity of \\(O(n \\log n)\\).

Figure 2-13   Time complexity of linearithmic order

Mainstream sorting algorithms typically have a time complexity of \\(O(n \\log n)\\), such as quicksort, merge sort, and heap sort.

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#7-factorial-order-on","level":3,"title":"7.   Factorial Order \\(O(n!)\\)","text":"

Factorial order corresponds to the mathematical \"permutation\" problem. Given \\(n\\) distinct elements, find all possible permutation schemes; the number of schemes is:

\\[ n! = n \\times (n - 1) \\times (n - 2) \\times \\dots \\times 2 \\times 1 \\]

Factorials are typically implemented using recursion. As shown in Figure 2-14 and the following code, the first level splits into \\(n\\) branches, the second level splits into \\(n - 1\\) branches, and so on, until the \\(n\\)-th level when splitting stops:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def factorial_recur(n: int) -> int:\n    \"\"\"Factorial order (recursive implementation)\"\"\"\n    if n == 0:\n        return 1\n    count = 0\n    # Split from 1 into n\n    for _ in range(n):\n        count += factorial_recur(n - 1)\n    return count\n
time_complexity.cpp
/* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.java
/* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.cs
/* Factorial order (recursive implementation) */\nint FactorialRecur(int n) {\n    if (n == 0) return 1;\n    int count = 0;\n    // Split from 1 into n\n    for (int i = 0; i < n; i++) {\n        count += FactorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.go
/* Factorial order (recursive implementation) */\nfunc factorialRecur(n int) int {\n    if n == 0 {\n        return 1\n    }\n    count := 0\n    // Split from 1 into n\n    for i := 0; i < n; i++ {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
time_complexity.swift
/* Factorial order (recursive implementation) */\nfunc factorialRecur(n: Int) -> Int {\n    if n == 0 {\n        return 1\n    }\n    var count = 0\n    // Split from 1 into n\n    for _ in 0 ..< n {\n        count += factorialRecur(n: n - 1)\n    }\n    return count\n}\n
time_complexity.js
/* Factorial order (recursive implementation) */\nfunction factorialRecur(n) {\n    if (n === 0) return 1;\n    let count = 0;\n    // Split from 1 into n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.ts
/* Factorial order (recursive implementation) */\nfunction factorialRecur(n: number): number {\n    if (n === 0) return 1;\n    let count = 0;\n    // Split from 1 into n\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.dart
/* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n  if (n == 0) return 1;\n  int count = 0;\n  // Split from 1 into n\n  for (var i = 0; i < n; i++) {\n    count += factorialRecur(n - 1);\n  }\n  return count;\n}\n
time_complexity.rs
/* Factorial order (recursive implementation) */\nfn factorial_recur(n: i32) -> i32 {\n    if n == 0 {\n        return 1;\n    }\n    let mut count = 0;\n    // Split from 1 into n\n    for _ in 0..n {\n        count += factorial_recur(n - 1);\n    }\n    count\n}\n
time_complexity.c
/* Factorial order (recursive implementation) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.kt
/* Factorial order (recursive implementation) */\nfun factorialRecur(n: Int): Int {\n    if (n == 0)\n        return 1\n    var count = 0\n    // Split from 1 into n\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
time_complexity.rb
### Factorial time (recursive) ###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # Split from 1 into n\n  (0...n).each { count += factorial_recur(n - 1) }\n\n  count\nend\n

Figure 2-14   Time complexity of factorial order

Note that because when \\(n \\geq 4\\) we always have \\(n! > 2^n\\), factorial order grows faster than exponential order, and is also unacceptable for large \\(n\\).

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#235-worst-best-and-average-time-complexities","level":2,"title":"2.3.5   Worst, Best, and Average Time Complexities","text":"

The time efficiency of an algorithm is often not fixed, but is related to the distribution of the input data. Suppose we input an array nums of length \\(n\\), where nums consists of numbers from \\(1\\) to \\(n\\), with each number appearing only once, but the element order is randomly shuffled. The task is to return the index of element \\(1\\). We can draw the following conclusions.

  • When nums = [?, ?, ..., 1], i.e., when the last element is \\(1\\), it requires a complete traversal of the array, reaching worst-case time complexity \\(O(n)\\).
  • When nums = [1, ?, ?, ...], i.e., when the first element is \\(1\\), no matter how long the array is, there is no need to continue traversing, reaching best-case time complexity \\(\\Omega(1)\\).

The \"worst-case time complexity\" corresponds to the function's asymptotic upper bound, denoted using big-\\(O\\) notation. Correspondingly, the \"best-case time complexity\" corresponds to the function's asymptotic lower bound, denoted using \\(\\Omega\\) notation:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby worst_best_time_complexity.py
def random_numbers(n: int) -> list[int]:\n    \"\"\"Generate an array with elements: 1, 2, ..., n, shuffled in order\"\"\"\n    # Generate array nums =: 1, 2, 3, ..., n\n    nums = [i for i in range(1, n + 1)]\n    # Randomly shuffle array elements\n    random.shuffle(nums)\n    return nums\n\ndef find_one(nums: list[int]) -> int:\n    \"\"\"Find the index of number 1 in array nums\"\"\"\n    for i in range(len(nums)):\n        # When element 1 is at the head of the array, best time complexity O(1) is achieved\n        # When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1:\n            return i\n    return -1\n
worst_best_time_complexity.cpp
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nvector<int> randomNumbers(int n) {\n    vector<int> nums(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Use system time to generate random seed\n    unsigned seed = chrono::system_clock::now().time_since_epoch().count();\n    // Randomly shuffle array elements\n    shuffle(nums.begin(), nums.end(), default_random_engine(seed));\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(vector<int> &nums) {\n    for (int i = 0; i < nums.size(); i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.java
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint[] randomNumbers(int n) {\n    Integer[] nums = new Integer[n];\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\n    Collections.shuffle(Arrays.asList(nums));\n    // Integer[] -> int[]\n    int[] res = new int[n];\n    for (int i = 0; i < n; i++) {\n        res[i] = nums[i];\n    }\n    return res;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(int[] nums) {\n    for (int i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.cs
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint[] RandomNumbers(int n) {\n    int[] nums = new int[n];\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n\n    // Randomly shuffle array elements\n    for (int i = 0; i < nums.Length; i++) {\n        int index = new Random().Next(i, nums.Length);\n        (nums[i], nums[index]) = (nums[index], nums[i]);\n    }\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint FindOne(int[] nums) {\n    for (int i = 0; i < nums.Length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.go
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunc randomNumbers(n int) []int {\n    nums := make([]int, n)\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for i := 0; i < n; i++ {\n        nums[i] = i + 1\n    }\n    // Randomly shuffle array elements\n    rand.Shuffle(len(nums), func(i, j int) {\n        nums[i], nums[j] = nums[j], nums[i]\n    })\n    return nums\n}\n\n/* Find the index of number 1 in array nums */\nfunc findOne(nums []int) int {\n    for i := 0; i < len(nums); i++ {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
worst_best_time_complexity.swift
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunc randomNumbers(n: Int) -> [Int] {\n    // Generate array nums = { 1, 2, 3, ..., n }\n    var nums = Array(1 ... n)\n    // Randomly shuffle array elements\n    nums.shuffle()\n    return nums\n}\n\n/* Find the index of number 1 in array nums */\nfunc findOne(nums: [Int]) -> Int {\n    for i in nums.indices {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
worst_best_time_complexity.js
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunction randomNumbers(n) {\n    const nums = Array(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nfunction findOne(nums) {\n    for (let i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
worst_best_time_complexity.ts
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfunction randomNumbers(n: number): number[] {\n    const nums = Array(n);\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nfunction findOne(nums: number[]): number {\n    for (let i = 0; i < nums.length; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
worst_best_time_complexity.dart
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nList<int> randomNumbers(int n) {\n  final nums = List.filled(n, 0);\n  // Generate array nums = { 1, 2, 3, ..., n }\n  for (var i = 0; i < n; i++) {\n    nums[i] = i + 1;\n  }\n  // Randomly shuffle array elements\n  nums.shuffle();\n\n  return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(List<int> nums) {\n  for (var i = 0; i < nums.length; i++) {\n    // When element 1 is at the head of the array, best time complexity O(1) is achieved\n    // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n    if (nums[i] == 1) return i;\n  }\n\n  return -1;\n}\n
worst_best_time_complexity.rs
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfn random_numbers(n: i32) -> Vec<i32> {\n    // Generate array nums = { 1, 2, 3, ..., n }\n    let mut nums = (1..=n).collect::<Vec<i32>>();\n    // Randomly shuffle array elements\n    nums.shuffle(&mut thread_rng());\n    nums\n}\n\n/* Find the index of number 1 in array nums */\nfn find_one(nums: &[i32]) -> Option<usize> {\n    for i in 0..nums.len() {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if nums[i] == 1 {\n            return Some(i);\n        }\n    }\n    None\n}\n
worst_best_time_complexity.c
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nint *randomNumbers(int n) {\n    // Allocate heap memory (create 1D variable-length array: n elements of type int)\n    int *nums = (int *)malloc(n * sizeof(int));\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // Randomly shuffle array elements\n    for (int i = n - 1; i > 0; i--) {\n        int j = rand() % (i + 1);\n        int temp = nums[i];\n        nums[i] = nums[j];\n        nums[j] = temp;\n    }\n    return nums;\n}\n\n/* Find the index of number 1 in array nums */\nint findOne(int *nums, int n) {\n    for (int i = 0; i < n; i++) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.kt
/* Generate an array with elements { 1, 2, ..., n }, order shuffled */\nfun randomNumbers(n: Int): Array<Int?> {\n    val nums = IntArray(n)\n    // Generate array nums = { 1, 2, 3, ..., n }\n    for (i in 0..<n) {\n        nums[i] = i + 1\n    }\n    // Randomly shuffle array elements\n    nums.shuffle()\n    val res = arrayOfNulls<Int>(n)\n    for (i in 0..<n) {\n        res[i] = nums[i]\n    }\n    return res\n}\n\n/* Find the index of number 1 in array nums */\nfun findOne(nums: Array<Int?>): Int {\n    for (i in nums.indices) {\n        // When element 1 is at the head of the array, best time complexity O(1) is achieved\n        // When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n        if (nums[i] == 1)\n            return i\n    }\n    return -1\n}\n
worst_best_time_complexity.rb
### Generate array with elements: 1, 2, ..., n, shuffled ###\ndef random_numbers(n)\n  # Generate array nums =: 1, 2, 3, ..., n\n  nums = Array.new(n) { |i| i + 1 }\n  # Randomly shuffle array elements\n  nums.shuffle!\nend\n\n### Find index of number 1 in array nums ###\ndef find_one(nums)\n  for i in 0...nums.length\n    # When element 1 is at the head of the array, best time complexity O(1) is achieved\n    # When element 1 is at the tail of the array, worst time complexity O(n) is achieved\n    return i if nums[i] == 1\n  end\n\n  -1\nend\n

It is worth noting that we rarely use best-case time complexity in practice, because it can usually only be achieved with a very small probability and may be somewhat misleading. The worst-case time complexity is more practical because it gives a safety value for efficiency, allowing us to use the algorithm with confidence.

From the above example, we can see that both worst-case and best-case time complexities only occur under \"special data distributions\", which may have a very small probability of occurrence and may not truly reflect the algorithm's running efficiency. In contrast, average time complexity can reflect the algorithm's running efficiency under random input data, denoted using the \\(\\Theta\\) notation.

For some algorithms, we can simply derive the average case under random data distribution. For example, in the above example, since the input array is shuffled, the probability of element \\(1\\) appearing at any index is equal, so the algorithm's average number of loops is half the array length \\(n / 2\\), giving an average time complexity of \\(\\Theta(n / 2) = \\Theta(n)\\).

But for more complex algorithms, calculating average time complexity is often quite difficult, because it is hard to analyze the overall mathematical expectation under data distribution. In this case, we usually use worst-case time complexity as the criterion for judging algorithm efficiency.

Why is the \\(\\Theta\\) symbol rarely seen?

This may be because the \\(O\\) symbol is too catchy, so we often use it to represent average time complexity. But strictly speaking, this practice is not standard. In this book and other materials, if you encounter expressions like \"average time complexity \\(O(n)\\)\", please understand it directly as \\(\\Theta(n)\\).

","path":["Chapter 2. Complexity Analysis","2.3   Time Complexity"],"tags":[]},{"location":"chapter_data_structure/","level":1,"title":"Chapter 3.   Data Structures","text":"

Abstract

Data structure is like a sturdy and diverse framework.

It provides a blueprint for the orderly organization of data, upon which algorithms come to life.

","path":["Chapter 3. Data Structures","Chapter 3.   Data Structures"],"tags":[]},{"location":"chapter_data_structure/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 3.1   Classification of Data Structures
  • 3.2   Basic Data Types
  • 3.3   Number Encoding *
  • 3.4   Character Encoding *
  • 3.5   Summary
","path":["Chapter 3. Data Structures","Chapter 3.   Data Structures"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   Basic Data Types","text":"

When we talk about data in computers, we think of various forms such as text, images, videos, audio, 3D models, and more. Although these data are organized in different ways, they are all composed of various basic data types.

Basic data types are types that the CPU can directly operate on, and they are directly used in algorithms, mainly including the following.

  • Integer types byte, short, int, long.
  • Floating-point types float, double, used to represent decimal numbers.
  • Character type char, used to represent letters, punctuation marks, and even emojis in various languages.
  • Boolean type bool, used to represent \"yes\" and \"no\" judgments.

Basic data types are stored in binary form in computers. One binary bit is \\(1\\) bit. In most modern operating systems, \\(1\\) byte consists of \\(8\\) bits.

The range of values for basic data types depends on the size of the space they occupy. Below is an example using Java.

  • Integer type byte occupies \\(1\\) byte = \\(8\\) bits, and can represent \\(2^{8}\\) numbers.
  • Integer type int occupies \\(4\\) bytes = \\(32\\) bits, and can represent \\(2^{32}\\) numbers.

The following table lists the space occupied, value ranges, and default values of various basic data types in Java. You don't need to memorize this table; a general understanding is sufficient, and you can refer to it when needed.

Table 3-1   Space occupied and value ranges of basic data types

Type Symbol Space Occupied Minimum Value Maximum Value Default Value Integer byte 1 byte \\(-2^7\\) (\\(-128\\)) \\(2^7 - 1\\) (\\(127\\)) \\(0\\) short 2 bytes \\(-2^{15}\\) \\(2^{15} - 1\\) \\(0\\) int 4 bytes \\(-2^{31}\\) \\(2^{31} - 1\\) \\(0\\) long 8 bytes \\(-2^{63}\\) \\(2^{63} - 1\\) \\(0\\) Float float 4 bytes \\(1.175 \\times 10^{-38}\\) \\(3.403 \\times 10^{38}\\) \\(0.0\\text{f}\\) double 8 bytes \\(2.225 \\times 10^{-308}\\) \\(1.798 \\times 10^{308}\\) \\(0.0\\) Character char 2 bytes \\(0\\) \\(2^{16} - 1\\) \\(0\\) Boolean bool 1 byte \\(\\text{false}\\) \\(\\text{true}\\) \\(\\text{false}\\)

Please note that the above table is specific to Java's basic data types. Each programming language has its own data type definitions, and their space occupied, value ranges, and default values may vary.

  • In Python, the integer type int can be of any size, limited only by available memory; the floating-point type float is double-precision 64-bit; there is no char type, a single character is actually a string str of length 1.
  • C and C++ do not explicitly specify the size of basic data types, which varies by implementation and platform. The above table follows the LP64 data model, which is used in Unix 64-bit operating systems including Linux and macOS.
  • The size of character char is 1 byte in C and C++, and in most programming languages it depends on the specific character encoding method, as detailed in the \"Character Encoding\" section.
  • Even though representing a boolean value requires only 1 bit (\\(0\\) or \\(1\\)), it is usually stored as 1 byte in memory. This is because modern computer CPUs typically use 1 byte as the minimum addressable memory unit.

So, what is the relationship between basic data types and data structures? We know that data structures are ways of organizing and storing data in computers. The subject of this statement is \"structure\", not \"data\".

If we want to represent \"a row of numbers\", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and order relationships of numbers, but the content stored—whether integer int, floating-point float, or character char—is unrelated to the \"data structure\".

In other words, basic data types provide the \"content type\" of data, while data structures provide the \"organization method\" of data. For example, in the following code, we use the same data structure (array) to store and represent different basic data types, including int, float, char, bool, etc.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# Initialize arrays using various basic data types\nnumbers: list[int] = [0] * 5\ndecimals: list[float] = [0.0] * 5\n# In Python, characters are actually strings of length 1\ncharacters: list[str] = ['0'] * 5\nbools: list[bool] = [False] * 5\n# Python lists can freely store various basic data types and object references\ndata = [0, 0.0, 'a', False, ListNode(0)]\n
// Initialize arrays using various basic data types\nint numbers[5];\nfloat decimals[5];\nchar characters[5];\nbool bools[5];\n
// Initialize arrays using various basic data types\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nboolean[] bools = new boolean[5];\n
// Initialize arrays using various basic data types\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nbool[] bools = new bool[5];\n
// Initialize arrays using various basic data types\nvar numbers = [5]int{}\nvar decimals = [5]float64{}\nvar characters = [5]byte{}\nvar bools = [5]bool{}\n
// Initialize arrays using various basic data types\nlet numbers = Array(repeating: 0, count: 5)\nlet decimals = Array(repeating: 0.0, count: 5)\nlet characters: [Character] = Array(repeating: \"a\", count: 5)\nlet bools = Array(repeating: false, count: 5)\n
// JavaScript arrays can freely store various basic data types and objects\nconst array = [0, 0.0, 'a', false];\n
// Initialize arrays using various basic data types\nconst numbers: number[] = [];\nconst characters: string[] = [];\nconst bools: boolean[] = [];\n
// Initialize arrays using various basic data types\nList<int> numbers = List.filled(5, 0);\nList<double> decimals = List.filled(5, 0.0);\nList<String> characters = List.filled(5, 'a');\nList<bool> bools = List.filled(5, false);\n
// Initialize arrays using various basic data types\nlet numbers: Vec<i32> = vec![0; 5];\nlet decimals: Vec<f32> = vec![0.0; 5];\nlet characters: Vec<char> = vec!['0'; 5];\nlet bools: Vec<bool> = vec![false; 5];\n
// Initialize arrays using various basic data types\nint numbers[10];\nfloat decimals[10];\nchar characters[10];\nbool bools[10];\n
// Initialize arrays using various basic data types\nval numbers = IntArray(5)\nval decinals = FloatArray(5)\nval characters = CharArray(5)\nval bools = BooleanArray(5)\n
# Ruby lists can freely store various basic data types and object references\ndata = [0, 0.0, 'a', false, ListNode(0)]\n
Visualized Execution

https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["Chapter 3. Data Structures","3.2   Basic Data Types"],"tags":[]},{"location":"chapter_data_structure/character_encoding/","level":1,"title":"3.4   Character Encoding *","text":"

In computers, all data is stored in binary form, and character char is no exception. To represent characters, we need to establish a \"character set\" that defines a one-to-one correspondence between each character and binary numbers. With a character set, computers can convert binary numbers to characters by looking up the table.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#341-ascii-character-set","level":2,"title":"3.4.1   Ascii Character Set","text":"

ASCII code is the earliest character set, with the full name American Standard Code for Information Interchange. It uses 7 binary bits (the lower 7 bits of one byte) to represent a character, and can represent a maximum of 128 different characters. As shown in Figure 3-6, ASCII code includes uppercase and lowercase English letters, numbers 0 ~ 9, some punctuation marks, and some control characters (such as newline and tab).

Figure 3-6   ASCII code

However, ASCII code can only represent English. With the globalization of computers, a character set called EASCII that can represent more languages emerged. It expands from the 7-bit basis of ASCII to 8 bits, and can represent 256 different characters.

Worldwide, a batch of EASCII character sets suitable for different regions have appeared successively. The first 128 characters of these character sets are unified as ASCII code, and the last 128 characters are defined differently to adapt to the needs of different languages.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#342-gbk-character-set","level":2,"title":"3.4.2   Gbk Character Set","text":"

Later, people found that EASCII code still cannot meet the character quantity requirements of many languages. For example, there are nearly one hundred thousand Chinese characters, and several thousand are used daily. In 1980, the China National Standardization Administration released the GB2312 character set, which included 6,763 Chinese characters, basically meeting the needs for computer processing of Chinese characters.

However, GB2312 cannot handle some rare characters and traditional Chinese characters. The GBK character set is an extension based on GB2312, which includes a total of 21,886 Chinese characters. In the GBK encoding scheme, ASCII characters are represented using one byte, and Chinese characters are represented using two bytes.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode-character-set","level":2,"title":"3.4.3   Unicode Character Set","text":"

With the vigorous development of computer technology, character sets and encoding standards flourished, which brought many problems. On the one hand, these character sets generally only define characters for specific languages and cannot work normally in multilingual environments. On the other hand, multiple character set standards exist for the same language, and if two computers use different encoding standards, garbled characters will appear during information transmission.

Researchers of that era thought: If a sufficiently complete character set is released that includes all languages and symbols in the world, wouldn't it be possible to solve cross-language environment and garbled character problems? Driven by this idea, a large and comprehensive character set, Unicode, was born.

Unicode is called \"统一码\" (Unified Code) in Chinese and can theoretically accommodate over one million characters. It is committed to including characters from around the world into a unified character set, providing a universal character set to handle and display various language texts, reducing garbled character problems caused by different encoding standards.

Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages. In the vast Unicode character set, commonly used characters occupy 2 bytes, and some rare characters occupy 3 bytes or even 4 bytes.

Unicode is a universal character set that essentially assigns a number (called a \"code point\") to each character, but it does not specify how to store these character code points in computers. We can't help but ask: when Unicode code points of multiple lengths appear simultaneously in a text, how does the system parse the characters? For example, given an encoding with a length of 2 bytes, how does the system determine whether it is one 2-byte character or two 1-byte characters?

For the above problem, a straightforward solution is to store all characters as equal-length encodings. As shown in Figure 3-7, each character in \"Hello\" occupies 1 byte, and each character in \"算法\" (algorithm) occupies 2 bytes. We can encode all characters in \"Hello 算法\" as 2 bytes in length by padding the high bits with 0. In this way, the system can parse one character every 2 bytes and restore the content of this phrase.

Figure 3-7   Unicode encoding example

However, ASCII code has already proven to us that encoding English only requires 1 byte. If the above scheme is adopted, the size of English text will be twice that under ASCII encoding, which is very wasteful of memory space. Therefore, we need a more efficient Unicode encoding method.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#344-utf-8-encoding","level":2,"title":"3.4.4   Utf-8 Encoding","text":"

Currently, UTF-8 has become the most widely used Unicode encoding method internationally. It is a variable-length encoding that uses 1 to 4 bytes to represent a character, depending on the complexity of the character. ASCII characters only require 1 byte, Latin and Greek letters require 2 bytes, commonly used Chinese characters require 3 bytes, and some other rare characters require 4 bytes.

The encoding rules of UTF-8 are not complicated and can be divided into the following two cases.

  • For 1-byte characters, set the highest bit to \\(0\\), and set the remaining 7 bits to the Unicode code point. It is worth noting that ASCII characters occupy the first 128 code points in the Unicode character set. That is to say, UTF-8 encoding is backward compatible with ASCII code. This means we can use UTF-8 to parse very old ASCII code text.
  • For characters with a length of \\(n\\) bytes (where \\(n > 1\\)), set the highest \\(n\\) bits of the first byte to \\(1\\), and set the \\((n + 1)\\)-th bit to \\(0\\); starting from the second byte, set the highest 2 bits of each byte to \\(10\\); use all remaining bits to fill in the Unicode code point of the character.

Figure 3-8 shows the UTF-8 encoding corresponding to \"Hello算法\". It can be observed that since the highest \\(n\\) bits are all set to \\(1\\), the system can parse the length of the character as \\(n\\) by reading the number of highest bits that are \\(1\\).

But why set the highest 2 bits of all other bytes to \\(10\\)? In fact, this \\(10\\) can serve as a check symbol. Assuming the system starts parsing text from an incorrect byte, the \\(10\\) at the beginning of the byte can help the system quickly determine an anomaly.

The reason for using \\(10\\) as a check symbol is that under UTF-8 encoding rules, it is impossible for a character's highest two bits to be \\(10\\). This conclusion can be proven by contradiction: assuming the highest two bits of a character are \\(10\\), it means the length of the character is \\(1\\), corresponding to ASCII code. However, the highest bit of ASCII code should be \\(0\\), which contradicts the assumption.

Figure 3-8   UTF-8 encoding example

In addition to UTF-8, common encoding methods also include the following two.

  • UTF-16 encoding: Uses 2 or 4 bytes to represent a character. All ASCII characters and commonly used non-English characters are represented with 2 bytes; a few characters need to use 4 bytes. For 2-byte characters, UTF-16 encoding is equal to the Unicode code point.
  • UTF-32 encoding: Every character uses 4 bytes. This means that UTF-32 takes up more space than UTF-8 and UTF-16, especially for text with a high proportion of ASCII characters.

From the perspective of storage space occupation, using UTF-8 to represent English characters is very efficient because it only requires 1 byte; using UTF-16 encoding for some non-English characters (such as Chinese) will be more efficient because it only requires 2 bytes, while UTF-8 may require 3 bytes.

From a compatibility perspective, UTF-8 has the best universality, and many tools and libraries support UTF-8 first.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#345-character-encoding-in-programming-languages","level":2,"title":"3.4.5   Character Encoding in Programming Languages","text":"

For most past programming languages, strings during program execution use fixed-length encodings such as UTF-16 or UTF-32. Under fixed-length encoding, we can treat strings as arrays for processing, and this approach has the following advantages.

  • Random access: UTF-16 encoded strings can be easily accessed randomly. UTF-8 is a variable-length encoding. To find the \\(i\\)-th character, we need to traverse from the beginning of the string to the \\(i\\)-th character, which requires \\(O(n)\\) time.
  • Character counting: Similar to random access, calculating the length of a UTF-16 encoded string is also an \\(O(1)\\) operation. However, calculating the length of a UTF-8 encoded string requires traversing the entire string.
  • String operations: Many string operations (such as splitting, joining, inserting, deleting, etc.) on UTF-16 encoded strings are easier to perform. Performing these operations on UTF-8 encoded strings usually requires additional calculations to ensure that invalid UTF-8 encoding is not generated.

In fact, the design of character encoding schemes for programming languages is a very interesting topic involving many factors.

  • Java's String type uses UTF-16 encoding, with each character occupying 2 bytes. This is because at the beginning of Java language design, people believed that 16 bits were sufficient to represent all possible characters. However, this was an incorrect judgment. Later, the Unicode specification expanded beyond 16 bits, so characters in Java may now be represented by a pair of 16-bit values (called \"surrogate pairs\").
  • The strings of JavaScript and TypeScript use UTF-16 encoding for reasons similar to Java. When Netscape first introduced the JavaScript language in 1995, Unicode was still in its early stages of development, and at that time, using 16-bit encoding was sufficient to represent all Unicode characters.
  • C# uses UTF-16 encoding mainly because the .NET platform was designed by Microsoft, and many of Microsoft's technologies (including the Windows operating system) extensively use UTF-16 encoding.

Due to the underestimation of character quantities by the above programming languages, they had to adopt the \"surrogate pair\" method to represent Unicode characters with lengths exceeding 16 bits. This is a reluctant compromise. On the one hand, in strings containing surrogate pairs, one character may occupy 2 bytes or 4 bytes, thus losing the advantage of fixed-length encoding. On the other hand, handling surrogate pairs requires additional code, which increases the complexity and difficulty of debugging in programming.

For the above reasons, some programming languages have proposed different encoding schemes.

  • Python's str uses Unicode encoding and adopts a flexible string representation where the stored character length depends on the largest Unicode code point in the string. If all characters in the string are ASCII characters, each character occupies 1 byte; if there are characters exceeding the ASCII range but all within the Basic Multilingual Plane (BMP), each character occupies 2 bytes; if there are characters exceeding the BMP, each character occupies 4 bytes.
  • Go language's string type uses UTF-8 encoding internally. Go language also provides the rune type, which is used to represent a single Unicode code point.
  • Rust language's str and String types use UTF-8 encoding internally. Rust also provides the char type for representing a single Unicode code point.

It should be noted that the above discussion is about how strings are stored in programming languages, which is different from how strings are stored in files or transmitted over networks. In file storage or network transmission, we usually encode strings into UTF-8 format to achieve optimal compatibility and space efficiency.

","path":["Chapter 3. Data Structures","3.4   Character Encoding *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   Classification of Data Structures","text":"

Common data structures include arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. They can be classified from two dimensions: \"logical structure\" and \"physical structure\".

","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311-logical-structure-linear-and-non-linear","level":2,"title":"3.1.1   Logical Structure: Linear and Non-Linear","text":"

Logical structure reveals the logical relationships between data elements. In arrays and linked lists, data is arranged in a certain order, embodying the linear relationship between data; while in trees, data is arranged hierarchically from top to bottom, showing the derived relationship between \"ancestors\" and \"descendants\"; graphs are composed of nodes and edges, reflecting complex network relationships.

As shown in Figure 3-1, logical structures can be divided into two major categories: \"linear\" and \"non-linear\". Linear structures are more intuitive, indicating that data is linearly arranged in logical relationships; non-linear structures are the opposite, arranged non-linearly.

  • Linear data structures: Arrays, linked lists, stacks, queues, hash tables, where elements have a one-to-one sequential relationship.
  • Non-linear data structures: Trees, heaps, graphs, hash tables.

Non-linear data structures can be further divided into tree structures and network structures.

  • Tree structures: Trees, heaps, hash tables, where elements have a one-to-many relationship.
  • Network structures: Graphs, where elements have a many-to-many relationship.

Figure 3-1   Linear and non-linear data structures

","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312-physical-structure-contiguous-and-dispersed","level":2,"title":"3.1.2   Physical Structure: Contiguous and Dispersed","text":"

When an algorithm program runs, the data being processed is mainly stored in memory. Figure 3-2 shows a computer memory stick, where each black square contains a memory space. We can imagine memory as a huge Excel spreadsheet, where each cell can store a certain amount of data.

The system accesses data at the target location through memory addresses. As shown in Figure 3-2, the computer assigns a number to each cell in the spreadsheet according to specific rules, ensuring that each memory space has a unique memory address. With these addresses, the program can access data in memory.

Figure 3-2   Memory stick, memory space, memory address

Tip

It is worth noting that comparing memory to an Excel spreadsheet is a simplified analogy. The actual working mechanism of memory is quite complex, involving concepts such as address space, memory management, cache mechanisms, virtual memory, and physical memory.

Memory is a shared resource for all programs. When a block of memory is occupied by a program, it usually cannot be used by other programs at the same time. Therefore, in the design of data structures and algorithms, memory resources are an important consideration. For example, the peak memory occupied by an algorithm should not exceed the remaining free memory of the system; if there is a lack of contiguous large memory blocks, then the data structure chosen must be able to be stored in dispersed memory spaces.

As shown in Figure 3-3, physical structure reflects the way data is stored in computer memory, and can be divided into contiguous space storage (arrays) and dispersed space storage (linked lists). The two physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency.

Figure 3-3   Contiguous space storage and dispersed space storage

It is worth noting that all data structures are implemented based on arrays, linked lists, or a combination of both. For example, stacks and queues can be implemented using either arrays or linked lists; while the implementation of hash tables may include both arrays and linked lists.

  • Can be implemented based on arrays: Stacks, queues, hash tables, trees, heaps, graphs, matrices, tensors (arrays with dimensions \\(\\geq 3\\)), etc.
  • Can be implemented based on linked lists: Stacks, queues, hash tables, trees, heaps, graphs, etc.

After initialization, linked lists can still adjust their length during program execution, so they are also called \"dynamic data structures\". After initialization, the length of arrays cannot be changed, so they are also called \"static data structures\". It is worth noting that arrays can achieve length changes by reallocating memory, thus possessing a certain degree of \"dynamism\".

Tip

If you find it difficult to understand physical structure, it is recommended to read the next chapter first, and then review this section.

","path":["Chapter 3. Data Structures","3.1   Classification of Data Structures"],"tags":[]},{"location":"chapter_data_structure/number_encoding/","level":1,"title":"3.3   Number Encoding *","text":"

Tip

In this book, chapters marked with an asterisk * are optional readings. If you are short on time or find them challenging, you may skip these initially and return to them after completing the essential chapters.

","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#331-sign-magnitude-1s-complement-and-2s-complement","level":2,"title":"3.3.1   Sign-Magnitude, 1's Complement, and 2's Complement","text":"

In the table from the previous section, we found that all integer types can represent one more negative number than positive numbers. For example, the byte range is \\([-128, 127]\\). This phenomenon is counterintuitive, and its underlying reason involves knowledge of sign-magnitude, 1's complement, and 2's complement.

First, it should be noted that numbers are stored in computers in the form of \"2's complement\". Before analyzing the reasons for this, let's first define these three concepts.

  • Sign-magnitude: We treat the highest bit of the binary representation of a number as the sign bit, where \\(0\\) represents a positive number and \\(1\\) represents a negative number, and the remaining bits represent the value of the number.
  • 1's complement: The 1's complement of a positive number is the same as its sign-magnitude. For a negative number, the 1's complement is obtained by inverting all bits except the sign bit of its sign-magnitude.
  • 2's complement: The 2's complement of a positive number is the same as its sign-magnitude. For a negative number, the 2's complement is obtained by adding \\(1\\) to its 1's complement.

Figure 3-4 shows the conversion methods among sign-magnitude, 1's complement, and 2's complement.

Figure 3-4   Conversions among sign-magnitude, 1's complement, and 2's complement

Sign-magnitude, although the most intuitive, has some limitations. On one hand, the sign-magnitude of negative numbers cannot be directly used in operations. For example, calculating \\(1 + (-2)\\) in sign-magnitude yields \\(-3\\), which is clearly incorrect.

\\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline & = 1000 \\; 0011 \\newline & \\rightarrow -3 \\end{aligned} \\]

To solve this problem, computers introduced 1's complement. If we first convert sign-magnitude to 1's complement and calculate \\(1 + (-2)\\) in 1's complement, then convert the result back to sign-magnitude, we can obtain the correct result of \\(-1\\).

\\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 \\; \\text{(Sign-magnitude)} + 1000 \\; 0010 \\; \\text{(Sign-magnitude)} \\newline & = 0000 \\; 0001 \\; \\text{(1's complement)} + 1111 \\; 1101 \\; \\text{(1's complement)} \\newline & = 1111 \\; 1110 \\; \\text{(1's complement)} \\newline & = 1000 \\; 0001 \\; \\text{(Sign-magnitude)} \\newline & \\rightarrow -1 \\end{aligned} \\]

On the other hand, the sign-magnitude of the number zero has two representations, \\(+0\\) and \\(-0\\). This means that the number zero corresponds to two different binary encodings, which may cause ambiguity. For example, in conditional judgments, if we don't distinguish between positive zero and negative zero, it may lead to incorrect judgment results. If we want to handle the ambiguity of positive and negative zero, we need to introduce additional judgment operations, which may reduce the computational efficiency of the computer.

\\[ \\begin{aligned} +0 & \\rightarrow 0000 \\; 0000 \\newline -0 & \\rightarrow 1000 \\; 0000 \\end{aligned} \\]

Like sign-magnitude, 1's complement also has the problem of positive and negative zero ambiguity. Therefore, computers further introduced 2's complement. Let's first observe the conversion process of negative zero from sign-magnitude to 1's complement to 2's complement:

\\[ \\begin{aligned} -0 \\rightarrow \\; & 1000 \\; 0000 \\; \\text{(Sign-magnitude)} \\newline = \\; & 1111 \\; 1111 \\; \\text{(1's complement)} \\newline = 1 \\; & 0000 \\; 0000 \\; \\text{(2's complement)} \\newline \\end{aligned} \\]

Adding \\(1\\) to the 1's complement of negative zero produces a carry, but since the byte type has a length of only 8 bits, the \\(1\\) that overflows to the 9th bit is discarded. That is to say, the 2's complement of negative zero is \\(0000 \\; 0000\\), which is the same as the 2's complement of positive zero. This means that in 2's complement representation, there is only one zero, and the positive and negative zero ambiguity is thus resolved.

One last question remains: the range of the byte type is \\([-128, 127]\\), and how is the extra negative number \\(-128\\) obtained? We notice that all integers in the interval \\([-127, +127]\\) have corresponding sign-magnitude, 1's complement, and 2's complement, and sign-magnitude and 2's complement can be converted to each other.

However, the 2's complement \\(1000 \\; 0000\\) is an exception, and it does not have a corresponding sign-magnitude. According to the conversion method, we get that the sign-magnitude of this 2's complement is \\(0000 \\; 0000\\). This is clearly contradictory because this sign-magnitude represents the number \\(0\\), and its 2's complement should be itself. The computer specifies that this special 2's complement \\(1000 \\; 0000\\) represents \\(-128\\). In fact, the result of calculating \\((-1) + (-127)\\) in 2's complement is \\(-128\\).

\\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\; 1111 \\; \\text{(Sign-magnitude)} + 1000 \\; 0001 \\; \\text{(Sign-magnitude)} \\newline & = 1000 \\; 0000 \\; \\text{(1's complement)} + 1111 \\; 1110 \\; \\text{(1's complement)} \\newline & = 1000 \\; 0001 \\; \\text{(2's complement)} + 1111 \\; 1111 \\; \\text{(2's complement)} \\newline & = 1000 \\; 0000 \\; \\text{(2's complement)} \\newline & \\rightarrow -128 \\end{aligned} \\]

You may have noticed that all the above calculations are addition operations. This hints at an important fact: the hardware circuits inside computers are mainly designed based on addition operations. This is because addition operations are simpler to implement in hardware compared to other operations (such as multiplication, division, and subtraction), easier to parallelize, and have faster operation speeds.

Please note that this does not mean that computers can only perform addition. By combining addition with some basic logical operations, computers can implement various other mathematical operations. For example, calculating the subtraction \\(a - b\\) can be converted to calculating the addition \\(a + (-b)\\); calculating multiplication and division can be converted to calculating multiple additions or subtractions.

Now we can summarize the reasons why computers use 2's complement: based on 2's complement representation, computers can use the same circuits and operations to handle the addition of positive and negative numbers, without the need to design special hardware circuits to handle subtraction, and without the need to specially handle the ambiguity problem of positive and negative zero. This greatly simplifies hardware design and improves operational efficiency.

The design of 2's complement is very ingenious. Due to space limitations, we will stop here. Interested readers are encouraged to explore further.

","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332-floating-point-number-encoding","level":2,"title":"3.3.2   Floating-Point Number Encoding","text":"

Careful readers may have noticed: int and float have the same length, both are 4 bytes, but why does float have a much larger range than int? This is very counterintuitive because it stands to reason that float needs to represent decimals, so the range should be smaller.

In fact, this is because floating-point number float uses a different representation method. Let's denote a 32-bit binary number as:

\\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

According to the IEEE 754 standard, a 32-bit float consists of the following three parts.

  • Sign bit \\(\\mathrm{S}\\): occupies 1 bit, corresponding to \\(b_{31}\\).
  • Exponent bit \\(\\mathrm{E}\\): occupies 8 bits, corresponding to \\(b_{30} b_{29} \\ldots b_{23}\\).
  • Fraction bit \\(\\mathrm{N}\\): occupies 23 bits, corresponding to \\(b_{22} b_{21} \\ldots b_0\\).

The calculation method for the value corresponding to the binary float is:

\\[ \\text {val} = (-1)^{b_{31}} \\times 2^{\\left(b_{30} b_{29} \\ldots b_{23}\\right)_2-127} \\times\\left(1 . b_{22} b_{21} \\ldots b_0\\right)_2 \\]

Converted to decimal, the calculation formula is:

\\[ \\text {val}=(-1)^{\\mathrm{S}} \\times 2^{\\mathrm{E} -127} \\times (1 + \\mathrm{N}) \\]

The range of each component is:

\\[ \\begin{aligned} \\mathrm{S} \\in & \\{ 0, 1\\}, \\quad \\mathrm{E} \\in \\{ 1, 2, \\dots, 254 \\} \\newline (1 + \\mathrm{N}) = & (1 + \\sum_{i=1}^{23} b_{23-i} 2^{-i}) \\subset [1, 2 - 2^{-23}] \\end{aligned} \\]

Figure 3-5   Calculation example of float under IEEE 754 standard

Observing Figure 3-5, given example data \\(\\mathrm{S} = 0\\), \\(\\mathrm{E} = 124\\), \\(\\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\\), we have:

\\[ \\text { val } = (-1)^0 \\times 2^{124 - 127} \\times (1 + 0.375) = 0.171875 \\]

Now we can answer the initial question: the representation of float includes an exponent bit, resulting in a range far greater than int. According to the above calculation, the maximum positive number that float can represent is \\(2^{254 - 127} \\times (2 - 2^{-23}) \\approx 3.4 \\times 10^{38}\\), and the minimum negative number can be obtained by switching the sign bit.

Although floating-point number float expands the range, its side effect is sacrificing precision. The integer type int uses all 32 bits to represent numbers, and the numbers are evenly distributed; however, due to the existence of the exponent bit, the larger the value of floating-point number float, the larger the difference between two adjacent numbers tends to be.

As shown in Table 3-2, exponent bits \\(\\mathrm{E} = 0\\) and \\(\\mathrm{E} = 255\\) have special meanings, used to represent zero, infinity, \\(\\mathrm{NaN}\\), etc.

Table 3-2   Meaning of exponent bits

Exponent Bit E Fraction Bit \\(\\mathrm{N} = 0\\) Fraction Bit \\(\\mathrm{N} \\ne 0\\) Calculation Formula \\(0\\) \\(\\pm 0\\) Subnormal Number \\((-1)^{\\mathrm{S}} \\times 2^{-126} \\times (0.\\mathrm{N})\\) \\(1, 2, \\dots, 254\\) Normal Number Normal Number \\((-1)^{\\mathrm{S}} \\times 2^{(\\mathrm{E} -127)} \\times (1.\\mathrm{N})\\) \\(255\\) \\(\\pm \\infty\\) \\(\\mathrm{NaN}\\)

It is worth noting that subnormal numbers significantly improve the precision of floating-point numbers. The smallest positive normal number is \\(2^{-126}\\), and the smallest positive subnormal number is \\(2^{-126} \\times 2^{-23}\\).

Double-precision double also uses a representation method similar to float, which will not be elaborated here.

","path":["Chapter 3. Data Structures","3.3   Number Encoding *"],"tags":[]},{"location":"chapter_data_structure/summary/","level":1,"title":"3.5   Summary","text":"","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_data_structure/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Data structures can be classified from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data elements, while physical structure describes how data is stored in computer memory.
  • Common logical structures include linear, tree, and network structures. We typically classify data structures as linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures.
  • When a program runs, data is stored in computer memory. Each memory space has a corresponding memory address, and the program accesses data through these memory addresses.
  • Physical structures are primarily divided into contiguous space storage (arrays) and dispersed space storage (linked lists). All data structures are implemented using arrays, linked lists, or a combination of both.
  • Basic data types in computers include integers byte, short, int, long, floating-point numbers float, double, characters char, and booleans bool. Their value ranges depend on the size of space they occupy and their representation method.
  • Sign-magnitude, 1's complement, and 2's complement are three methods for encoding numbers in computers, and they can be converted into each other. The most significant bit of sign-magnitude is the sign bit, and the remaining bits represent the value of the number.
  • Integers are stored in computers in 2's complement form. Under 2's complement representation, computers can treat the addition of positive and negative numbers uniformly, without needing to design special hardware circuits for subtraction, and there is no ambiguity of positive and negative zero.
  • The encoding of floating-point numbers consists of 1 sign bit, 8 exponent bits, and 23 fraction bits. Due to the exponent bits, the range of floating-point numbers is much larger than that of integers, at the cost of sacrificing precision.
  • ASCII is the earliest English character set, with a length of 1 byte, containing a total of 127 characters. GBK is a commonly used Chinese character set, containing over 20,000 Chinese characters. Unicode is committed to providing a complete character set standard, collecting characters from various languages around the world, thereby solving the garbled text problem caused by inconsistent character encoding methods.
  • UTF-8 is the most popular Unicode encoding method, with excellent universality. It is a variable-length encoding method with good scalability, effectively improving storage space efficiency. UTF-16 and UTF-32 are fixed-length encoding methods. When encoding Chinese characters, UTF-16 occupies less space than UTF-8. Programming languages such as Java and C# use UTF-16 encoding by default.
","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_data_structure/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Why do hash tables contain both linear and non-linear data structures?

The underlying structure of a hash table is an array. To resolve hash collisions, we may use \"chaining\" (discussed in the subsequent \"Hash Collision\" section): each bucket in the array points to a linked list, which may be converted to a tree (usually a red-black tree) when the list length exceeds a certain threshold.

From a storage perspective, the underlying structure of a hash table is an array, where each bucket slot may contain a value, a linked list, or a tree. Therefore, hash tables may contain both linear data structures (arrays, linked lists) and non-linear data structures (trees).

Q: Is the length of the char type 1 byte?

The length of the char type is determined by the encoding method used by the programming language. For example, Java, JavaScript, TypeScript, and C# all use UTF-16 encoding (to store Unicode code points), so the char type has a length of 2 bytes.

Q: Is there ambiguity in referring to array-based data structures as \"static data structures\"? Stacks can also perform \"dynamic\" operations such as push and pop.

Stacks can indeed implement dynamic data operations, but the data structure is still \"static\" (fixed length). Although array-based data structures can dynamically add or remove elements, their capacity is fixed. If the data volume exceeds the pre-allocated size, a new larger array needs to be created, and the contents of the old array must be copied to the new array.

Q: When constructing a stack (queue), its size is not specified. Why are they \"static data structures\"?

In high-level programming languages, we do not need to manually specify the initial capacity of a stack (queue); this work is automatically completed within the class. For example, the initial capacity of Java's ArrayList is typically 10. Additionally, the expansion operation is also automatically implemented. See the subsequent \"List\" section for details.

Q: The method of converting sign-magnitude to 2's complement is \"first negate then add 1\". So converting 2's complement to sign-magnitude should be the inverse operation \"first subtract 1 then negate\". However, 2's complement can also be converted to sign-magnitude through \"first negate then add 1\". Why is this?

This is because the mutual conversion between sign-magnitude and 2's complement is actually the process of computing the \"complement\". Let us first define the complement: assuming \\(a + b = c\\), then we say that \\(a\\) is the complement of \\(b\\) to \\(c\\), and conversely, \\(b\\) is the complement of \\(a\\) to \\(c\\).

Given an \\(n = 4\\) bit binary number \\(0010\\), if we treat this number as sign-magnitude (ignoring the sign bit), then its 2's complement can be obtained through \"first negate then add 1\":

\\[ 0010 \\rightarrow 1101 \\rightarrow 1110 \\]

We find that the sum of sign-magnitude and 2's complement is \\(0010 + 1110 = 10000\\), which means the 2's complement \\(1110\\) is the \"complement\" of sign-magnitude \\(0010\\) to \\(10000\\). This means the above \"first negate then add 1\" is actually the process of computing the complement to \\(10000\\).

So, what is the \"complement\" of 2's complement \\(1110\\) to \\(10000\\)? We can still use \"first negate then add 1\" to obtain it:

\\[ 1110 \\rightarrow 0001 \\rightarrow 0010 \\]

In other words, sign-magnitude and 2's complement are each other's \"complement\" to \\(10000\\), so \"sign-magnitude to 2's complement\" and \"2's complement to sign-magnitude\" can be implemented using the same operation (first negate then add 1).

Of course, we can also use the inverse operation to find the sign-magnitude of 2's complement \\(1110\\), that is, \"first subtract 1 then negate\":

\\[ 1110 \\rightarrow 1101 \\rightarrow 0010 \\]

In summary, both \"first negate then add 1\" and \"first subtract 1 then negate\" are computing the complement to \\(10000\\), and they are equivalent.

Essentially, the \"negate\" operation is actually finding the complement to \\(1111\\) (because sign-magnitude + 1's complement = 1111 always holds); and adding 1 to the 1's complement yields the 2's complement, which is the complement to \\(10000\\).

The above uses \\(n = 4\\) as an example, and it can be generalized to binary numbers of any number of bits.

","path":["Chapter 3. Data Structures","3.5   Summary"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"Chapter 12.   Divide and Conquer","text":"

Abstract

Difficult problems are decomposed layer by layer, with each decomposition making them simpler.

Divide and conquer reveals an important truth: start with simplicity, and nothing remains complex.

","path":["Chapter 12. Divide and Conquer","Chapter 12.   Divide and Conquer"],"tags":[]},{"location":"chapter_divide_and_conquer/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 12.1   Divide and Conquer Algorithms
  • 12.2   Divide and Conquer Search Strategy
  • 12.3   Building a Binary Tree Problem
  • 12.4   Hanoi Tower Problem
  • 12.5   Summary
","path":["Chapter 12. Divide and Conquer","Chapter 12.   Divide and Conquer"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/","level":1,"title":"12.2   Divide and Conquer Search Strategy","text":"

We have already learned that search algorithms are divided into two major categories.

  • Brute-force search: Implemented by traversing the data structure, with a time complexity of \\(O(n)\\).
  • Adaptive search: Utilizes unique data organization forms or prior information, with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\).

In fact, search algorithms with time complexity of \\(O(\\log n)\\) are typically implemented based on the divide and conquer strategy, such as binary search and trees.

  • Each step of binary search divides the problem (searching for a target element in an array) into a smaller problem (searching for the target element in half of the array), continuing until the array is empty or the target element is found.
  • Trees are representative of the divide and conquer idea. In data structures such as binary search trees, AVL trees, and heaps, the time complexity of various operations is \\(O(\\log n)\\).

The divide and conquer strategy of binary search is as follows.

  • The problem can be decomposed: Binary search recursively decomposes the original problem (searching in an array) into subproblems (searching in half of the array), achieved by comparing the middle element with the target element.
  • Subproblems are independent: In binary search, each round only processes one subproblem, which is not affected by other subproblems.
  • Solutions of subproblems do not need to be merged: Binary search aims to find a specific element, so there is no need to merge the solutions of subproblems. When a subproblem is solved, the original problem is also solved.

Divide and conquer can improve search efficiency because brute-force search can only eliminate one option per round, while divide and conquer search can eliminate half of the options per round.

","path":["Chapter 12. Divide and Conquer","12.2   Divide and Conquer Search Strategy"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/#1-implementing-binary-search-based-on-divide-and-conquer","level":3,"title":"1.   Implementing Binary Search Based on Divide and Conquer","text":"

In previous sections, binary search was implemented based on iteration. Now we implement it based on divide and conquer (recursion).

Question

Given a sorted array nums of length \\(n\\), where all elements are unique, find the element target.

From a divide and conquer perspective, we denote the subproblem corresponding to the search interval \\([i, j]\\) as \\(f(i, j)\\).

Starting from the original problem \\(f(0, n-1)\\), perform binary search through the following steps.

  1. Calculate the midpoint \\(m\\) of the search interval \\([i, j]\\), and use it to eliminate half of the search interval.
  2. Recursively solve the subproblem reduced by half in size, which could be \\(f(i, m-1)\\) or \\(f(m+1, j)\\).
  3. Repeat steps 1. and 2. until target is found or the interval is empty and return.

Figure 12-4 shows the divide and conquer process of binary search for element \\(6\\) in an array.

Figure 12-4   Divide and conquer process of binary search

In the implementation code, we declare a recursive function dfs() to solve the problem \\(f(i, j)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_recur.py
def dfs(nums: list[int], target: int, i: int, j: int) -> int:\n    \"\"\"Binary search: problem f(i, j)\"\"\"\n    # If the interval is empty, it means there is no target element, return -1\n    if i > j:\n        return -1\n    # Calculate the midpoint index m\n    m = (i + j) // 2\n    if nums[m] < target:\n        # Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j)\n    elif nums[m] > target:\n        # Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1)\n    else:\n        # Found the target element, return its index\n        return m\n\ndef binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Binary search\"\"\"\n    n = len(nums)\n    # Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n
binary_search_recur.cpp
/* Binary search: problem f(i, j) */\nint dfs(vector<int> &nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(vector<int> &nums, int target) {\n    int n = nums.size();\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.java
/* Binary search: problem f(i, j) */\nint dfs(int[] nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(int[] nums, int target) {\n    int n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.cs
/* Binary search: problem f(i, j) */\nint DFS(int[] nums, int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return DFS(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return DFS(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint BinarySearch(int[] nums, int target) {\n    int n = nums.Length;\n    // Solve the problem f(0, n-1)\n    return DFS(nums, target, 0, n - 1);\n}\n
binary_search_recur.go
/* Binary search: problem f(i, j) */\nfunc dfs(nums []int, target, i, j int) int {\n    // If interval is empty, indicating no target element, return -1\n    if i > j {\n        return -1\n    }\n    // Calculate midpoint index\n    m := i + ((j - i) >> 1)\n    // Compare midpoint with target element\n    if nums[m] < target {\n        // If smaller, recurse on right half of array\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m+1, j)\n    } else if nums[m] > target {\n        // If larger, recurse on left half of array\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m-1)\n    } else {\n        // Found the target element, return its index\n        return m\n    }\n}\n\n/* Binary search */\nfunc binarySearch(nums []int, target int) int {\n    n := len(nums)\n    return dfs(nums, target, 0, n-1)\n}\n
binary_search_recur.swift
/* Binary search: problem f(i, j) */\nfunc dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {\n    // If the interval is empty, it means there is no target element, return -1\n    if i > j {\n        return -1\n    }\n    // Calculate the midpoint index m\n    let m = (i + j) / 2\n    if nums[m] < target {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums: nums, target: target, i: m + 1, j: j)\n    } else if nums[m] > target {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums: nums, target: target, i: i, j: m - 1)\n    } else {\n        // Found the target element, return its index\n        return m\n    }\n}\n\n/* Binary search */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Solve the problem f(0, n-1)\n    dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)\n}\n
binary_search_recur.js
/* Binary search: problem f(i, j) */\nfunction dfs(nums, target, i, j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfunction binarySearch(nums, target) {\n    const n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.ts
/* Binary search: problem f(i, j) */\nfunction dfs(nums: number[], target: number, i: number, j: number): number {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfunction binarySearch(nums: number[], target: number): number {\n    const n = nums.length;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.dart
/* Binary search: problem f(i, j) */\nint dfs(List<int> nums, int target, int i, int j) {\n  // If the interval is empty, it means there is no target element, return -1\n  if (i > j) {\n    return -1;\n  }\n  // Calculate the midpoint index m\n  int m = (i + j) ~/ 2;\n  if (nums[m] < target) {\n    // Recursion subproblem f(m+1, j)\n    return dfs(nums, target, m + 1, j);\n  } else if (nums[m] > target) {\n    // Recursion subproblem f(i, m-1)\n    return dfs(nums, target, i, m - 1);\n  } else {\n    // Found the target element, return its index\n    return m;\n  }\n}\n\n/* Binary search */\nint binarySearch(List<int> nums, int target) {\n  int n = nums.length;\n  // Solve the problem f(0, n-1)\n  return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.rs
/* Binary search: problem f(i, j) */\nfn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {\n    // If the interval is empty, it means there is no target element, return -1\n    if i > j {\n        return -1;\n    }\n    let m: i32 = i + (j - i) / 2;\n    if nums[m as usize] < target {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if nums[m as usize] > target {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    let n = nums.len() as i32;\n    // Solve the problem f(0, n-1)\n    dfs(nums, target, 0, n - 1)\n}\n
binary_search_recur.c
/* Binary search: problem f(i, j) */\nint dfs(int nums[], int target, int i, int j) {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1;\n    }\n    // Calculate the midpoint index m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // Found the target element, return its index\n        return m;\n    }\n}\n\n/* Binary search */\nint binarySearch(int nums[], int target, int numsSize) {\n    int n = numsSize;\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.kt
/* Binary search: problem f(i, j) */\nfun dfs(\n    nums: IntArray,\n    target: Int,\n    i: Int,\n    j: Int\n): Int {\n    // If the interval is empty, it means there is no target element, return -1\n    if (i > j) {\n        return -1\n    }\n    // Calculate the midpoint index m\n    val m = (i + j) / 2\n    return if (nums[m] < target) {\n        // Recursion subproblem f(m+1, j)\n        dfs(nums, target, m + 1, j)\n    } else if (nums[m] > target) {\n        // Recursion subproblem f(i, m-1)\n        dfs(nums, target, i, m - 1)\n    } else {\n        // Found the target element, return its index\n        m\n    }\n}\n\n/* Binary search */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    val n = nums.size\n    // Solve the problem f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n}\n
binary_search_recur.rb
### Binary search: problem f(i, j) ###\ndef dfs(nums, target, i, j)\n  # If the interval is empty, it means there is no target element, return -1\n  return -1 if i > j\n\n  # Calculate the midpoint index m\n  m = (i + j) / 2\n\n  if nums[m] < target\n    # Recursion subproblem f(m+1, j)\n    return dfs(nums, target, m + 1, j)\n  elsif nums[m] > target\n    # Recursion subproblem f(i, m-1)\n    return dfs(nums, target, i, m - 1)\n  else\n    # Found the target element, return its index\n    return m\n  end\nend\n\n### Binary search ###\ndef binary_search(nums, target)\n  n = nums.length\n  # Solve the problem f(0, n-1)\n  dfs(nums, target, 0, n - 1)\nend\n
","path":["Chapter 12. Divide and Conquer","12.2   Divide and Conquer Search Strategy"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/","level":1,"title":"12.3   Building a Binary Tree Problem","text":"

Question

Given the preorder traversal preorder and inorder traversal inorder of a binary tree, construct the binary tree and return the root node of the binary tree. Assume there are no duplicate node values in the binary tree (as shown in Figure 12-5).

Figure 12-5   Example data for building a binary tree

","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1-determining-if-it-is-a-divide-and-conquer-problem","level":3,"title":"1.   Determining If It Is a Divide and Conquer Problem","text":"

The original problem is defined as constructing a binary tree from preorder and inorder, which is a typical divide and conquer problem.

  • The problem can be decomposed: From a divide and conquer perspective, we can divide the original problem into two subproblems: constructing the left subtree and constructing the right subtree, plus one operation: initializing the root node. For each subtree (subproblem), we can still reuse the above division method, dividing it into smaller subtrees (subproblems) until the smallest subproblem (empty subtree) is reached.
  • Subproblems are independent: The left and right subtrees are independent of each other; there is no overlap between them. When constructing the left subtree, we only need to focus on the parts of the inorder and preorder traversals corresponding to the left subtree. The same applies to the right subtree.
  • Solutions of subproblems can be merged: Once we have the left and right subtrees (solutions of subproblems), we can link them to the root node to obtain the solution to the original problem.
","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2-how-to-divide-subtrees","level":3,"title":"2.   How to Divide Subtrees","text":"

Based on the above analysis, this problem can be solved using divide and conquer, but how do we divide the left and right subtrees through the preorder traversal preorder and inorder traversal inorder?

According to the definition, both preorder and inorder can be divided into three parts.

  • Preorder traversal: [ Root Node | Left Subtree | Right Subtree ], for example, the tree in Figure 12-5 corresponds to [ 3 | 9 | 2 1 7 ].
  • Inorder traversal: [ Left Subtree | Root Node | Right Subtree ], for example, the tree in Figure 12-5 corresponds to [ 9 | 3 | 1 2 7 ].

Using the data from the figure above as an example, we can obtain the division results through the steps shown in Figure 12-6.

  1. The first element 3 in the preorder traversal is the value of the root node.
  2. Find the index of root node 3 in inorder, and use this index to divide inorder into [ 9 | 3 | 1 2 7 ].
  3. Based on the division result of inorder, it is easy to determine that the left and right subtrees have 1 and 3 nodes respectively, allowing us to divide preorder into [ 3 | 9 | 2 1 7 ].

Figure 12-6   Dividing subtrees in preorder and inorder traversals

","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#3-describing-subtree-intervals-based-on-variables","level":3,"title":"3.   Describing Subtree Intervals Based on Variables","text":"

Based on the above division method, we have obtained the index intervals of the root node, left subtree, and right subtree in preorder and inorder. To describe these index intervals, we need to use several pointer variables.

  • Denote the index of the current tree's root node in preorder as \\(i\\).
  • Denote the index of the current tree's root node in inorder as \\(m\\).
  • Denote the index interval of the current tree in inorder as \\([l, r]\\).

As shown in Table 12-1, through these variables we can represent the index of the root node in preorder and the index intervals of the subtrees in inorder.

Table 12-1   Indices of root node and subtrees in preorder and inorder traversals

Root node index in preorder Subtree index interval in inorder Current tree \\(i\\) \\([l, r]\\) Left subtree \\(i + 1\\) \\([l, m-1]\\) Right subtree \\(i + 1 + (m - l)\\) \\([m+1, r]\\)

Please note that \\((m-l)\\) in the right subtree root node index means \"the number of nodes in the left subtree\". It is recommended to understand this in conjunction with Figure 12-7.

Figure 12-7   Index interval representation of root node and left and right subtrees

","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#4-code-implementation","level":3,"title":"4.   Code Implementation","text":"

To improve the efficiency of querying \\(m\\), we use a hash table hmap to store the mapping from elements in the inorder array to their indices:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby build_tree.py
def dfs(\n    preorder: list[int],\n    inorder_map: dict[int, int],\n    i: int,\n    l: int,\n    r: int,\n) -> TreeNode | None:\n    \"\"\"Build binary tree: divide and conquer\"\"\"\n    # Terminate when the subtree interval is empty\n    if r - l < 0:\n        return None\n    # Initialize the root node\n    root = TreeNode(preorder[i])\n    # Query m to divide the left and right subtrees\n    m = inorder_map[preorder[i]]\n    # Subproblem: build the left subtree\n    root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n    # Subproblem: build the right subtree\n    root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n    # Return the root node\n    return root\n\ndef build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:\n    \"\"\"Build binary tree\"\"\"\n    # Initialize hash map, storing the mapping from inorder elements to indices\n    inorder_map = {val: i for i, val in enumerate(inorder)}\n    root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)\n    return root\n
build_tree.cpp
/* Build binary tree: divide and conquer */\nTreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return NULL;\n    // Initialize the root node\n    TreeNode *root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    unordered_map<int, int> inorderMap;\n    for (int i = 0; i < inorder.size(); i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);\n    return root;\n}\n
build_tree.java
/* Build binary tree: divide and conquer */\nTreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return null;\n    // Initialize the root node\n    TreeNode root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode buildTree(int[] preorder, int[] inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    Map<Integer, Integer> inorderMap = new HashMap<>();\n    for (int i = 0; i < inorder.length; i++) {\n        inorderMap.put(inorder[i], i);\n    }\n    TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.cs
/* Build binary tree: divide and conquer */\nTreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return null;\n    // Initialize the root node\n    TreeNode root = new(preorder[i]);\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode? BuildTree(int[] preorder, int[] inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    Dictionary<int, int> inorderMap = [];\n    for (int i = 0; i < inorder.Length; i++) {\n        inorderMap.TryAdd(inorder[i], i);\n    }\n    TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);\n    return root;\n}\n
build_tree.go
/* Build binary tree: divide and conquer */\nfunc dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {\n    // Terminate when the subtree interval is empty\n    if r-l < 0 {\n        return nil\n    }\n    // Initialize the root node\n    root := NewTreeNode(preorder[i])\n    // Query m to divide the left and right subtrees\n    m := inorderMap[preorder[i]]\n    // Subproblem: build the left subtree\n    root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)\n    // Subproblem: build the right subtree\n    root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfunc buildTree(preorder, inorder []int) *TreeNode {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    inorderMap := make(map[int]int, len(inorder))\n    for i := 0; i < len(inorder); i++ {\n        inorderMap[inorder[i]] = i\n    }\n\n    root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)\n    return root\n}\n
build_tree.swift
/* Build binary tree: divide and conquer */\nfunc dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {\n    // Terminate when the subtree interval is empty\n    if r - l < 0 {\n        return nil\n    }\n    // Initialize the root node\n    let root = TreeNode(x: preorder[i])\n    // Query m to divide the left and right subtrees\n    let m = inorderMap[preorder[i]]!\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfunc buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }\n    return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)\n}\n
build_tree.js
/* Build binary tree: divide and conquer */\nfunction dfs(preorder, inorderMap, i, l, r) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null;\n    // Initialize the root node\n    const root = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    const m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nfunction buildTree(preorder, inorder) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    let inorderMap = new Map();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.ts
/* Build binary tree: divide and conquer */\nfunction dfs(\n    preorder: number[],\n    inorderMap: Map<number, number>,\n    i: number,\n    l: number,\n    r: number\n): TreeNode | null {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null;\n    // Initialize the root node\n    const root: TreeNode = new TreeNode(preorder[i]);\n    // Query m to divide the left and right subtrees\n    const m = inorderMap.get(preorder[i]);\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nfunction buildTree(preorder: number[], inorder: number[]): TreeNode | null {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    let inorderMap = new Map<number, number>();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.dart
/* Build binary tree: divide and conquer */\nTreeNode? dfs(\n  List<int> preorder,\n  Map<int, int> inorderMap,\n  int i,\n  int l,\n  int r,\n) {\n  // Terminate when the subtree interval is empty\n  if (r - l < 0) {\n    return null;\n  }\n  // Initialize the root node\n  TreeNode? root = TreeNode(preorder[i]);\n  // Query m to divide the left and right subtrees\n  int m = inorderMap[preorder[i]]!;\n  // Subproblem: build the left subtree\n  root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n  // Subproblem: build the right subtree\n  root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n  // Return the root node\n  return root;\n}\n\n/* Build binary tree */\nTreeNode? buildTree(List<int> preorder, List<int> inorder) {\n  // Initialize hash map, storing the mapping from inorder elements to indices\n  Map<int, int> inorderMap = {};\n  for (int i = 0; i < inorder.length; i++) {\n    inorderMap[inorder[i]] = i;\n  }\n  TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n  return root;\n}\n
build_tree.rs
/* Build binary tree: divide and conquer */\nfn dfs(\n    preorder: &[i32],\n    inorder_map: &HashMap<i32, i32>,\n    i: i32,\n    l: i32,\n    r: i32,\n) -> Option<Rc<RefCell<TreeNode>>> {\n    // Terminate when the subtree interval is empty\n    if r - l < 0 {\n        return None;\n    }\n    // Initialize the root node\n    let root = TreeNode::new(preorder[i as usize]);\n    // Query m to divide the left and right subtrees\n    let m = inorder_map.get(&preorder[i as usize]).unwrap();\n    // Subproblem: build the left subtree\n    root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);\n    // Subproblem: build the right subtree\n    root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);\n    // Return the root node\n    Some(root)\n}\n\n/* Build binary tree */\nfn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    let mut inorder_map: HashMap<i32, i32> = HashMap::new();\n    for i in 0..inorder.len() {\n        inorder_map.insert(inorder[i], i as i32);\n    }\n    let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);\n    root\n}\n
build_tree.c
/* Build binary tree: divide and conquer */\nTreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0)\n        return NULL;\n    // Initialize the root node\n    TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));\n    root->val = preorder[i];\n    root->left = NULL;\n    root->right = NULL;\n    // Query m to divide the left and right subtrees\n    int m = inorderMap[preorder[i]];\n    // Subproblem: build the left subtree\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);\n    // Subproblem: build the right subtree\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);\n    // Return the root node\n    return root;\n}\n\n/* Build binary tree */\nTreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);\n    for (int i = 0; i < inorderSize; i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);\n    free(inorderMap);\n    return root;\n}\n
build_tree.kt
/* Build binary tree: divide and conquer */\nfun dfs(\n    preorder: IntArray,\n    inorderMap: Map<Int?, Int?>,\n    i: Int,\n    l: Int,\n    r: Int\n): TreeNode? {\n    // Terminate when the subtree interval is empty\n    if (r - l < 0) return null\n    // Initialize the root node\n    val root = TreeNode(preorder[i])\n    // Query m to divide the left and right subtrees\n    val m = inorderMap[preorder[i]]!!\n    // Subproblem: build the left subtree\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)\n    // Subproblem: build the right subtree\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)\n    // Return the root node\n    return root\n}\n\n/* Build binary tree */\nfun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {\n    // Initialize hash map, storing the mapping from inorder elements to indices\n    val inorderMap = HashMap<Int?, Int?>()\n    for (i in inorder.indices) {\n        inorderMap[inorder[i]] = i\n    }\n    val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)\n    return root\n}\n
build_tree.rb
### Build binary tree: divide and conquer ###\ndef dfs(preorder, inorder_map, i, l, r)\n  # Terminate when the subtree interval is empty\n  return if r - l < 0\n\n  # Initialize the root node\n  root = TreeNode.new(preorder[i])\n  # Query m to divide the left and right subtrees\n  m = inorder_map[preorder[i]]\n  # Subproblem: build the left subtree\n  root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n  # Subproblem: build the right subtree\n  root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n\n  # Return the root node\n  root\nend\n\n### Build binary tree ###\ndef build_tree(preorder, inorder)\n  # Initialize hash map, storing the mapping from inorder elements to indices\n  inorder_map = {}\n  inorder.each_with_index { |val, i| inorder_map[val] = i }\n  dfs(preorder, inorder_map, 0, 0, inorder.length - 1)\nend\n

Figure 12-8 shows the recursive process of building the binary tree. Each node is established during the downward \"recursion\" process, while each edge (reference) is established during the upward \"return\" process.

<1><2><3><4><5><6><7><8><9>

Figure 12-8   Recursive process of building a binary tree

The division results of the preorder traversal preorder and inorder traversal inorder within each recursive function are shown in Figure 12-9.

Figure 12-9   Division results in each recursive function

Let the number of nodes in the tree be \\(n\\). Initializing each node (executing one recursive function dfs()) takes \\(O(1)\\) time. Therefore, the overall time complexity is \\(O(n)\\).

The hash table stores the mapping from inorder elements to their indices, with a space complexity of \\(O(n)\\). In the worst case, when the binary tree degenerates into a linked list, the recursion depth reaches \\(n\\), using \\(O(n)\\) stack frame space. Therefore, the overall space complexity is \\(O(n)\\).

","path":["Chapter 12. Divide and Conquer","12.3   Building a Binary Tree Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/","level":1,"title":"12.1   Divide and Conquer Algorithms","text":"

Divide and conquer is a very important and common algorithm strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: \"divide\" and \"conquer\".

  1. Divide (partition phase): Recursively divide the original problem into two or more subproblems until the smallest subproblem is reached.
  2. Conquer (merge phase): Starting from the smallest subproblems with known solutions, merge the solutions of subproblems from bottom to top to construct the solution to the original problem.

As shown in Figure 12-1, \"merge sort\" is one of the typical applications of the divide and conquer strategy.

  1. Divide: Recursively divide the original array (original problem) into two subarrays (subproblems) until the subarray has only one element (smallest subproblem).
  2. Conquer: Merge the sorted subarrays (solutions to subproblems) from bottom to top to obtain a sorted original array (solution to the original problem).

Figure 12-1   Divide and conquer strategy of merge sort

","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1211-how-to-determine-divide-and-conquer-problems","level":2,"title":"12.1.1   How to Determine Divide and Conquer Problems","text":"

Whether a problem is suitable for solving with divide and conquer can usually be determined based on the following criteria.

  1. The problem can be decomposed: The original problem can be divided into smaller, similar subproblems, and can be recursively divided in the same way.
  2. Subproblems are independent: There is no overlap between subproblems, they are independent of each other and can be solved independently.
  3. Solutions of subproblems can be merged: The solution to the original problem is obtained by merging the solutions of subproblems.

Clearly, merge sort satisfies these three criteria.

  1. The problem can be decomposed: Recursively divide the array (original problem) into two subarrays (subproblems).
  2. Subproblems are independent: Each subarray can be sorted independently (subproblems can be solved independently).
  3. Solutions of subproblems can be merged: Two sorted subarrays (solutions of subproblems) can be merged into one sorted array (solution of the original problem).
","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212-improving-efficiency-through-divide-and-conquer","level":2,"title":"12.1.2   Improving Efficiency Through Divide and Conquer","text":"

Divide and conquer can not only effectively solve algorithmic problems but often also improve algorithm efficiency. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy.

This raises the question: Why can divide and conquer improve algorithm efficiency, and what is the underlying logic? In other words, why is dividing a large problem into multiple subproblems, solving the subproblems, and merging their solutions more efficient than directly solving the original problem? This question can be discussed from two aspects: operation count and parallel computation.

","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1-operation-count-optimization","level":3,"title":"1.   Operation Count Optimization","text":"

Taking \"bubble sort\" as an example, processing an array of length \\(n\\) requires \\(O(n^2)\\) time. Suppose we divide the array into two subarrays from the midpoint as shown in Figure 12-2, the division requires \\(O(n)\\) time, sorting each subarray requires \\(O((n / 2)^2)\\) time, and merging the two subarrays requires \\(O(n)\\) time, resulting in an overall time complexity of:

\\[ O(n + (\\frac{n}{2})^2 \\times 2 + n) = O(\\frac{n^2}{2} + 2n) \\]

Figure 12-2   Bubble sort before and after array division

Next, we compute the following inequality, where the left and right sides represent the total number of operations before and after division, respectively:

\\[ \\begin{aligned} n^2 & > \\frac{n^2}{2} + 2n \\newline n^2 - \\frac{n^2}{2} - 2n & > 0 \\newline n(n - 4) & > 0 \\end{aligned} \\]

This means that when \\(n > 4\\), the number of operations after division is smaller, and sorting efficiency should be higher. Note that the time complexity after division is still quadratic \\(O(n^2)\\), but the constant term in the complexity has become smaller.

Going further, what if we continuously divide the subarrays from their midpoints into two subarrays until the subarrays have only one element? This approach is actually \"merge sort\", with a time complexity of \\(O(n \\log n)\\).

Thinking further, what if we set multiple division points and evenly divide the original array into \\(k\\) subarrays? This situation is very similar to \"bucket sort\", which is well-suited for sorting massive amounts of data, with a theoretical time complexity of \\(O(n + k)\\).

","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#2-parallel-computation-optimization","level":3,"title":"2.   Parallel Computation Optimization","text":"

We know that the subproblems generated by divide and conquer are independent of each other, so they can typically be solved in parallel. This means divide and conquer can not only reduce the time complexity of algorithms, but also benefits from parallel optimization by operating systems.

Parallel optimization is particularly effective in multi-core or multi-processor environments, as the system can simultaneously handle multiple subproblems, making fuller use of computing resources and significantly reducing overall runtime.

For example, in the \"bucket sort\" shown in Figure 12-3, we evenly distribute massive data into various buckets, and the sorting tasks for all buckets can be distributed to various computing units. After completion, the results are merged.

Figure 12-3   Parallel computation in bucket sort

","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1213-common-applications-of-divide-and-conquer","level":2,"title":"12.1.3   Common Applications of Divide and Conquer","text":"

On one hand, divide and conquer can be used to solve many classic algorithmic problems.

  • Finding the closest pair of points: This algorithm first divides the point set into two parts, then finds the closest pair of points in each part separately, and finally finds the closest pair of points that spans both parts.
  • Large integer multiplication: For example, the Karatsuba algorithm, which decomposes large integer multiplication into several smaller integer multiplications and additions.
  • Matrix multiplication: For example, the Strassen algorithm, which decomposes large matrix multiplication into multiple small matrix multiplications and additions.
  • Hanota problem: The hanota problem can be solved through recursion, which is a typical application of the divide and conquer strategy.
  • Solving inversion pairs: In a sequence, if a preceding number is greater than a following number, these two numbers form an inversion pair. Solving the inversion pair problem can utilize the divide and conquer approach with the help of merge sort.

On the other hand, divide and conquer is widely applied in the design of algorithms and data structures.

  • Binary search: Binary search divides a sorted array into two parts from the midpoint index, then decides which half to eliminate based on the comparison result between the target value and the middle element value, and performs the same binary operation on the remaining interval.
  • Merge sort: Already introduced at the beginning of this section, no further elaboration needed.
  • Quick sort: Quick sort selects a pivot value, then divides the array into two subarrays, one with elements smaller than the pivot and the other with elements larger than the pivot, then performs the same division operation on these two parts until the subarrays have only one element.
  • Bucket sort: The basic idea of bucket sort is to scatter data into multiple buckets, then sort the elements within each bucket, and finally extract the elements from each bucket in sequence to obtain a sorted array.
  • Trees: For example, binary search trees, AVL trees, red-black trees, B-trees, B+ trees, etc. Their search, insertion, and deletion operations can all be viewed as applications of the divide and conquer strategy.
  • Heaps: A heap is a special complete binary tree, and its various operations, such as insertion, deletion, and heapify, actually imply the divide and conquer idea.
  • Hash tables: Although hash tables do not directly apply divide and conquer, some hash collision resolution solutions indirectly apply the divide and conquer strategy. For example, long linked lists in chaining may be converted to red-black trees to improve query efficiency.

It can be seen that divide and conquer is a \"subtly pervasive\" algorithmic idea, embedded in various algorithms and data structures.

","path":["Chapter 12. Divide and Conquer","12.1   Divide and Conquer Algorithms"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   Hanota Problem","text":"

In merge sort and building binary trees, we decompose the original problem into two subproblems, each half the size of the original problem. However, for the hanota problem, we adopt a different decomposition strategy.

Question

Given three pillars, denoted as A, B, and C. Initially, pillar A has \\(n\\) discs stacked on it, arranged from top to bottom in ascending order of size. Our task is to move these \\(n\\) discs to pillar C while maintaining their original order (as shown in Figure 12-10). The following rules must be followed when moving the discs.

  1. A disc can only be taken from the top of one pillar and placed on top of another pillar.
  2. Only one disc can be moved at a time.
  3. A smaller disc must always be on top of a larger disc.

Figure 12-10   Example of the hanota problem

We denote the hanota problem of size \\(i\\) as \\(f(i)\\). For example, \\(f(3)\\) represents moving \\(3\\) discs from A to C.

","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#1-considering-the-base-cases","level":3,"title":"1.   Considering the Base Cases","text":"

As shown in Figure 12-11, for problem \\(f(1)\\), when there is only one disc, we can move it directly from A to C.

<1><2>

Figure 12-11   Solution for a problem of size 1

As shown in Figure 12-12, for problem \\(f(2)\\), when there are two discs, since we must always keep the smaller disc on top of the larger disc, we need to use B to assist in the move.

  1. First, move the smaller disc from A to B.
  2. Then move the larger disc from A to C.
  3. Finally, move the smaller disc from B to C.
<1><2><3><4>

Figure 12-12   Solution for a problem of size 2

The process of solving problem \\(f(2)\\) can be summarized as: moving two discs from A to C with the help of B. Here, C is called the target pillar, and B is called the buffer pillar.

","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2-subproblem-decomposition","level":3,"title":"2.   Subproblem Decomposition","text":"

For problem \\(f(3)\\), when there are three discs, the situation becomes slightly more complex.

Since we already know the solutions to \\(f(1)\\) and \\(f(2)\\), we can think from a divide and conquer perspective, treating the top two discs on A as a whole, and execute the steps shown in Figure 12-13. This successfully moves the three discs from A to C.

  1. Let B be the target pillar and C be the buffer pillar, and move two discs from A to B.
  2. Move the remaining disc from A directly to C.
  3. Let C be the target pillar and A be the buffer pillar, and move two discs from B to C.
<1><2><3><4>

Figure 12-13   Solution for a problem of size 3

Essentially, we divide problem \\(f(3)\\) into two subproblems \\(f(2)\\) and one subproblem \\(f(1)\\). By solving these three subproblems in order, the original problem is solved. This shows that the subproblems are independent and their solutions can be merged.

From this, we can summarize the divide and conquer strategy for solving the hanota problem shown in Figure 12-14: divide the original problem \\(f(n)\\) into two subproblems \\(f(n-1)\\) and one subproblem \\(f(1)\\), and solve these three subproblems in the following order.

  1. Move \\(n-1\\) discs from A to B with the help of C.
  2. Move the remaining \\(1\\) disc directly from A to C.
  3. Move \\(n-1\\) discs from B to C with the help of A.

For these two subproblems \\(f(n-1)\\), we can recursively divide them in the same way until reaching the smallest subproblem \\(f(1)\\). The solution to \\(f(1)\\) is known and requires only one move operation.

Figure 12-14   Divide and conquer strategy for solving the hanota problem

","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#3-code-implementation","level":3,"title":"3.   Code Implementation","text":"

In the code, we declare a recursive function dfs(i, src, buf, tar), whose purpose is to move the top \\(i\\) discs from pillar src to target pillar tar with the help of buffer pillar buf:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py
def move(src: list[int], tar: list[int]):\n    \"\"\"Move a disk\"\"\"\n    # Take out a disk from the top of src\n    pan = src.pop()\n    # Place the disk on top of tar\n    tar.append(pan)\n\ndef dfs(i: int, src: list[int], buf: list[int], tar: list[int]):\n    \"\"\"Solve the Tower of Hanoi problem f(i)\"\"\"\n    # If there is only one disk left in src, move it directly to tar\n    if i == 1:\n        move(src, tar)\n        return\n    # Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf)\n    # Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    # Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar)\n\ndef solve_hanota(A: list[int], B: list[int], C: list[int]):\n    \"\"\"Solve the Tower of Hanoi problem\"\"\"\n    n = len(A)\n    # Move the top n disks from A to C using B\n    dfs(n, A, B, C)\n
hanota.cpp
/* Move a disk */\nvoid move(vector<int> &src, vector<int> &tar) {\n    // Take out a disk from the top of src\n    int pan = src.back();\n    src.pop_back();\n    // Place the disk on top of tar\n    tar.push_back(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {\n    int n = A.size();\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
hanota.java
/* Move a disk */\nvoid move(List<Integer> src, List<Integer> tar) {\n    // Take out a disk from the top of src\n    Integer pan = src.remove(src.size() - 1);\n    // Place the disk on top of tar\n    tar.add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {\n    int n = A.size();\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
hanota.cs
/* Move a disk */\nvoid Move(List<int> src, List<int> tar) {\n    // Take out a disk from the top of src\n    int pan = src[^1];\n    src.RemoveAt(src.Count - 1);\n    // Place the disk on top of tar\n    tar.Add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid DFS(int i, List<int> src, List<int> buf, List<int> tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        Move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    DFS(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    Move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    DFS(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid SolveHanota(List<int> A, List<int> B, List<int> C) {\n    int n = A.Count;\n    // Move the top n disks from A to C using B\n    DFS(n, A, B, C);\n}\n
hanota.go
/* Move a disk */\nfunc move(src, tar *list.List) {\n    // Take out a disk from the top of src\n    pan := src.Back()\n    // Place the disk on top of tar\n    tar.PushBack(pan.Value)\n    // Remove top disk from src\n    src.Remove(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunc dfsHanota(i int, src, buf, tar *list.List) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move(src, tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfsHanota(i-1, src, tar, buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // Move the top n disks from A to C using B\n    dfsHanota(n, A, B, C)\n}\n
hanota.swift
/* Move a disk */\nfunc move(src: inout [Int], tar: inout [Int]) {\n    // Take out a disk from the top of src\n    let pan = src.popLast()!\n    // Place the disk on top of tar\n    tar.append(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunc dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move(src: &src, tar: &tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src: &src, tar: &tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfunc solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {\n    let n = A.count\n    // The tail of the list is the top of the rod\n    // Move top n disks from src to C using B\n    dfs(i: n, src: &A, buf: &B, tar: &C)\n}\n
hanota.js
/* Move a disk */\nfunction move(src, tar) {\n    // Take out a disk from the top of src\n    const pan = src.pop();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunction dfs(i, src, buf, tar) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
hanota.ts
/* Move a disk */\nfunction move(src: number[], tar: number[]): void {\n    // Take out a disk from the top of src\n    const pan = src.pop();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfunction dfs(i: number, src: number[], buf: number[], tar: number[]): void {\n    // If there is only one disk left in src, move it directly to tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfunction solveHanota(A: number[], B: number[], C: number[]): void {\n    const n = A.length;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
hanota.dart
/* Move a disk */\nvoid move(List<int> src, List<int> tar) {\n  // Take out a disk from the top of src\n  int pan = src.removeLast();\n  // Place the disk on top of tar\n  tar.add(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, List<int> src, List<int> buf, List<int> tar) {\n  // If there is only one disk left in src, move it directly to tar\n  if (i == 1) {\n    move(src, tar);\n    return;\n  }\n  // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n  dfs(i - 1, src, tar, buf);\n  // Subproblem f(1): move the remaining disk from src to tar\n  move(src, tar);\n  // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n  dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(List<int> A, List<int> B, List<int> C) {\n  int n = A.length;\n  // Move the top n disks from A to C using B\n  dfs(n, A, B, C);\n}\n
hanota.rs
/* Move a disk */\nfn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // Take out a disk from the top of src\n    let pan = src.pop().unwrap();\n    // Place the disk on top of tar\n    tar.push(pan);\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // If there is only one disk left in src, move it directly to tar\n    if i == 1 {\n        move_pan(src, tar);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move_pan(src, tar);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar);\n}\n\n/* Solve the Tower of Hanoi problem */\nfn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {\n    let n = A.len() as i32;\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C);\n}\n
hanota.c
/* Move a disk */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n    // Take out a disk from the top of src\n    int pan = src[*srcSize - 1];\n    src[*srcSize - 1] = 0;\n    (*srcSize)--;\n    // Place the disk on top of tar\n    tar[*tarSize] = pan;\n    (*tarSize)++;\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nvoid dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, srcSize, tar, tarSize);\n        return;\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, srcSize, tar, tarSize);\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);\n}\n\n/* Solve the Tower of Hanoi problem */\nvoid solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {\n    // Move the top n disks from A to C using B\n    dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n
hanota.kt
/* Move a disk */\nfun move(src: MutableList<Int>, tar: MutableList<Int>) {\n    // Take out a disk from the top of src\n    val pan = src.removeAt(src.size - 1)\n    // Place the disk on top of tar\n    tar.add(pan)\n}\n\n/* Solve the Tower of Hanoi problem f(i) */\nfun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {\n    // If there is only one disk left in src, move it directly to tar\n    if (i == 1) {\n        move(src, tar)\n        return\n    }\n    // Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n    dfs(i - 1, src, tar, buf)\n    // Subproblem f(1): move the remaining disk from src to tar\n    move(src, tar)\n    // Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n    dfs(i - 1, buf, src, tar)\n}\n\n/* Solve the Tower of Hanoi problem */\nfun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {\n    val n = A.size\n    // Move the top n disks from A to C using B\n    dfs(n, A, B, C)\n}\n
hanota.rb
### Move one disk ###\ndef move(src, tar)\n  # Take out a disk from the top of src\n  pan = src.pop\n  # Place the disk on top of tar\n  tar << pan\nend\n\n### Solve Tower of Hanoi f(i) ###\ndef dfs(i, src, buf, tar)\n  # If there is only one disk left in src, move it directly to tar\n  if i == 1\n    move(src, tar)\n    return\n  end\n\n  # Subproblem f(i-1): move the top i-1 disks from src to buf using tar\n  dfs(i - 1, src, tar, buf)\n  # Subproblem f(1): move the remaining disk from src to tar\n  move(src, tar)\n  # Subproblem f(i-1): move the top i-1 disks from buf to tar using src\n  dfs(i - 1, buf, src, tar)\nend\n\n### Solve Tower of Hanoi ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # Move the top n disks from A to C using B\n  dfs(n, _A, _B, _C)\nend\n

As shown in Figure 12-15, the hanota problem forms a recursion tree of height \\(n\\), where each node represents a subproblem corresponding to an invocation of the dfs() function, therefore the time complexity is \\(O(2^n)\\) and the space complexity is \\(O(n)\\).

Figure 12-15   Recursion tree of the hanota problem

Quote

The hanota problem originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and \\(64\\) golden discs of different sizes. The monks continuously moved the discs, believing that when the last disc was correctly placed, the world would come to an end.

However, even if the monks moved one disc per second, it would take approximately \\(2^{64} \\approx 1.84×10^{19}\\) seconds, which is about \\(5850\\) billion years, far exceeding current estimates of the age of the universe. Therefore, if this legend is true, we should not need to worry about the end of the world.

","path":["Chapter 12. Divide and Conquer","12.4   Hanota Problem"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/","level":1,"title":"12.5   Summary","text":"","path":["Chapter 12. Divide and Conquer","12.5   Summary"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Divide and conquer is a common algorithm design strategy, consisting of two phases: divide (partition) and conquer (merge), typically implemented based on recursion.
  • The criteria for determining whether a problem is a divide and conquer problem include: whether the problem can be decomposed, whether subproblems are independent, and whether subproblems can be merged.
  • Merge sort is a typical application of the divide and conquer strategy. It recursively divides an array into two equal-length subarrays until only one element remains, then merges them layer by layer to complete the sorting.
  • Introducing the divide and conquer strategy can often improve algorithm efficiency. On one hand, the divide and conquer strategy reduces the number of operations; on the other hand, it facilitates parallel optimization of the system after division.
  • Divide and conquer can both solve many algorithmic problems and is widely applied in data structure and algorithm design, appearing everywhere.
  • Compared to brute-force search, adaptive search is more efficient. Search algorithms with time complexity of \\(O(\\log n)\\) are typically implemented based on the divide and conquer strategy.
  • Binary search is another typical application of divide and conquer. It does not include the step of merging solutions of subproblems. We can implement binary search through recursive divide and conquer.
  • In the problem of building a binary tree, building the tree (original problem) can be divided into building the left subtree and right subtree (subproblems), which can be achieved by dividing the index intervals of the preorder and inorder traversals.
  • In the hanota problem, a problem of size \\(n\\) can be divided into two subproblems of size \\(n-1\\) and one subproblem of size \\(1\\). After solving these three subproblems in order, the original problem is solved.
","path":["Chapter 12. Divide and Conquer","12.5   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"Chapter 14.   Dynamic Programming","text":"

Abstract

Streams converge into rivers, rivers converge into the sea.

Dynamic programming gathers solutions to small problems into answers to large problems, step by step guiding us to the shore of problem-solving.

","path":["Chapter 14. Dynamic Programming","Chapter 14.   Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 14.1   Introduction to Dynamic Programming
  • 14.2   Characteristics of Dynamic Programming Problems
  • 14.3   Dynamic Programming Problem-Solving Approach
  • 14.4   0-1 Knapsack Problem
  • 14.5   Unbounded Knapsack Problem
  • 14.6   Edit Distance Problem
  • 14.7   Summary
","path":["Chapter 14. Dynamic Programming","Chapter 14.   Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/","level":1,"title":"14.2   Characteristics of Dynamic Programming Problems","text":"

In the previous section, we learned how dynamic programming solves the original problem by decomposing it into subproblems. In fact, subproblem decomposition is a general algorithmic approach, with different emphases in divide and conquer, dynamic programming, and backtracking.

  • Divide and conquer algorithms recursively divide the original problem into multiple independent subproblems until the smallest subproblems are reached, and merge the solutions to the subproblems during backtracking to ultimately obtain the solution to the original problem.
  • Dynamic programming also recursively decomposes problems, but the main difference from divide and conquer algorithms is that subproblems in dynamic programming are interdependent, and many overlapping subproblems appear during the decomposition process.
  • Backtracking algorithms enumerate all possible solutions through trial and error, and avoid unnecessary search branches through pruning. The solution to the original problem consists of a series of decision steps, and we can regard the subsequence before each decision step as a subproblem.

In fact, dynamic programming is commonly used to solve optimization problems, which not only contain overlapping subproblems but also have two other major characteristics: optimal substructure and no aftereffects.

","path":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1421-optimal-substructure","level":2,"title":"14.2.1   Optimal Substructure","text":"

We make a slight modification to the stair climbing problem to make it more suitable for demonstrating the concept of optimal substructure.

Climbing stairs with minimum cost

Given a staircase, where you can climb \\(1\\) or \\(2\\) steps at a time, and each step has a non-negative integer representing the cost you need to pay at that step. Given a non-negative integer array \\(cost\\), where \\(cost[i]\\) represents the cost at the \\(i\\)-th step, and \\(cost[0]\\) is the ground (starting point). What is the minimum cost required to reach the top?

As shown in Figure 14-6, if the costs of the \\(1\\)st, \\(2\\)nd, and \\(3\\)rd steps are \\(1\\), \\(10\\), and \\(1\\) respectively, then climbing from the ground to the \\(3\\)rd step requires a minimum cost of \\(2\\).

Figure 14-6   Minimum cost to climb to the 3rd step

Let \\(dp[i]\\) be the accumulated cost of climbing to the \\(i\\)-th step. Since the \\(i\\)-th step can only come from the \\(i-1\\)-th or \\(i-2\\)-th step, \\(dp[i]\\) can only equal \\(dp[i-1] + cost[i]\\) or \\(dp[i-2] + cost[i]\\). To minimize the cost, we should choose the smaller of the two:

\\[ dp[i] = \\min(dp[i-1], dp[i-2]) + cost[i] \\]

This leads us to the meaning of optimal substructure: the optimal solution to the original problem is constructed from the optimal solutions to the subproblems.

This problem clearly has optimal substructure: we select the better one from the optimal solutions to the two subproblems \\(dp[i-1]\\) and \\(dp[i-2]\\), and use it to construct the optimal solution to the original problem \\(dp[i]\\).

So, does the stair climbing problem from the previous section have optimal substructure? Its goal is to find the number of ways, which seems to be a counting problem, but if we change the question: \"Find the maximum number of ways\". We surprisingly discover that although the problem before and after modification are equivalent, the optimal substructure has emerged: the maximum number of ways for the \\(n\\)-th step equals the sum of the maximum number of ways for the \\(n-1\\)-th and \\(n-2\\)-th steps. Therefore, the interpretation of optimal substructure is quite flexible and will have different meanings in different problems.

According to the state transition equation and the initial states \\(dp[1] = cost[1]\\) and \\(dp[2] = cost[2]\\), we can obtain the dynamic programming code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
def min_cost_climbing_stairs_dp(cost: list[int]) -> int:\n    \"\"\"Minimum cost climbing stairs: Dynamic programming\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [0] * (n + 1)\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1], dp[2] = cost[1], cost[2]\n    # State transition: gradually solve larger subproblems from smaller ones\n    for i in range(3, n + 1):\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    return dp[n]\n
min_cost_climbing_stairs_dp.cpp
/* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    vector<int> dp(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.java
/* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.cs
/* Minimum cost climbing stairs: Dynamic programming */\nint MinCostClimbingStairsDP(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.go
/* Minimum cost climbing stairs: Dynamic programming */\nfunc minCostClimbingStairsDP(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    dp := make([]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        dp[i] = min(dp[i-1], dp[i-2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.swift
/* Minimum cost climbing stairs: Dynamic programming */\nfunc minCostClimbingStairsDP(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: 0, count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3 ... n {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.js
/* Minimum cost climbing stairs: Dynamic programming */\nfunction minCostClimbingStairsDP(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.ts
/* Minimum cost climbing stairs: Dynamic programming */\nfunction minCostClimbingStairsDP(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.dart
/* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  // Initialize dp table, used to store solutions to subproblems\n  List<int> dp = List.filled(n + 1, 0);\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1] = cost[1];\n  dp[2] = cost[2];\n  // State transition: gradually solve larger subproblems from smaller ones\n  for (int i = 3; i <= n; i++) {\n    dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n  }\n  return dp[n];\n}\n
min_cost_climbing_stairs_dp.rs
/* Minimum cost climbing stairs: Dynamic programming */\nfn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![-1; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3..=n {\n        dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    dp[n]\n}\n
min_cost_climbing_stairs_dp.c
/* Minimum cost climbing stairs: Dynamic programming */\nint minCostClimbingStairsDP(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // Initialize dp table, used to store solutions to subproblems\n    int *dp = calloc(n + 1, sizeof(int));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    int res = dp[n];\n    // Free memory\n    free(dp);\n    return res;\n}\n
min_cost_climbing_stairs_dp.kt
/* Minimum cost climbing stairs: Dynamic programming */\nfun minCostClimbingStairsDP(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = IntArray(n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (i in 3..n) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.rb
### Minimum cost climbing stairs: DP ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = cost[1], cost[2]\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n

Figure 14-7 shows the dynamic programming process for the above code.

Figure 14-7   Dynamic programming process for climbing stairs with minimum cost

This problem can also be space-optimized, compressing from one dimension to zero, reducing the space complexity from \\(O(n)\\) to \\(O(1)\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:\n    \"\"\"Minimum cost climbing stairs: Space-optimized dynamic programming\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    a, b = cost[1], cost[2]\n    for i in range(3, n + 1):\n        a, b = b, min(a, b) + cost[i]\n    return b\n
min_cost_climbing_stairs_dp.cpp
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nint minCostClimbingStairsDPComp(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.java
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nint minCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.cs
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nint MinCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.Min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.go
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfunc minCostClimbingStairsDPComp(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // Initial state: preset the solution to the smallest subproblem\n    a, b := cost[1], cost[2]\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        tmp := b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.swift
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfunc minCostClimbingStairsDPComp(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    var (a, b) = (cost[1], cost[2])\n    for i in 3 ... n {\n        (a, b) = (b, min(a, b) + cost[i])\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.js
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfunction minCostClimbingStairsDPComp(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.ts
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfunction minCostClimbingStairsDPComp(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.dart
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nint minCostClimbingStairsDPComp(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  int a = cost[1], b = cost[2];\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = min(a, tmp) + cost[i];\n    a = tmp;\n  }\n  return b;\n}\n
min_cost_climbing_stairs_dp.rs
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    };\n    let (mut a, mut b) = (cost[1], cost[2]);\n    for i in 3..=n {\n        let tmp = b;\n        b = cmp::min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    b\n}\n
min_cost_climbing_stairs_dp.c
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nint minCostClimbingStairsDPComp(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = myMin(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.kt
/* Minimum cost climbing stairs: Space-optimized dynamic programming */\nfun minCostClimbingStairsDPComp(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    var a = cost[1]\n    var b = cost[2]\n    for (i in 3..n) {\n        val tmp = b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.rb
### Minimum cost climbing stairs: DP ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = cost[1], cost[2]\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n\n# Minimum cost climbing stairs: Space-optimized dynamic programming\ndef min_cost_climbing_stairs_dp_comp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  a, b = cost[1], cost[2]\n  (3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] }\n  b\nend\n
","path":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1422-no-aftereffects","level":2,"title":"14.2.2   No Aftereffects","text":"

No aftereffects is one of the important characteristics that enable dynamic programming to solve problems effectively. Its definition is: given a certain state, its future development is only related to the current state and has nothing to do with all past states.

Taking the stair climbing problem as an example, given state \\(i\\), it will develop into states \\(i+1\\) and \\(i+2\\), corresponding to jumping \\(1\\) step and jumping \\(2\\) steps, respectively. When making these two choices, we do not need to consider the states before state \\(i\\), as they have no effect on the future of state \\(i\\).

However, if we add a constraint to the stair climbing problem, the situation changes.

Climbing stairs with constraint

Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time, but you cannot jump \\(1\\) step in two consecutive rounds. How many ways are there to climb to the top?

As shown in Figure 14-8, there are only \\(2\\) feasible ways to climb to the \\(3\\)rd step. The way of jumping \\(1\\) step three consecutive times does not satisfy the constraint and is therefore discarded.

Figure 14-8   Number of ways to climb to the 3rd step with constraint

In this problem, if the previous round was a jump of \\(1\\) step, then the next round must jump \\(2\\) steps. This means that the next choice cannot be determined solely by the current state (current stair step number), but also depends on the previous state (the stair step number from the previous round).

It is not difficult to see that this problem no longer satisfies no aftereffects, and the state transition equation \\(dp[i] = dp[i-1] + dp[i-2]\\) also fails, because \\(dp[i-1]\\) represents jumping \\(1\\) step in this round, but it includes many solutions where \"the previous round was a jump of \\(1\\) step\", which cannot be directly counted in \\(dp[i]\\) to satisfy the constraint.

For this reason, we need to expand the state definition: state \\([i, j]\\) represents being on the \\(i\\)-th step with the previous round having jumped \\(j\\) steps, where \\(j \\in \\{1, 2\\}\\). This state definition effectively distinguishes whether the previous round was a jump of \\(1\\) step or \\(2\\) steps, allowing us to determine where the current state came from.

  • When the previous round jumped \\(1\\) step, the round before that could only choose to jump \\(2\\) steps, i.e., \\(dp[i, 1]\\) can only be transferred from \\(dp[i-1, 2]\\).
  • When the previous round jumped \\(2\\) steps, the round before that could choose to jump \\(1\\) step or \\(2\\) steps, i.e., \\(dp[i, 2]\\) can be transferred from \\(dp[i-2, 1]\\) or \\(dp[i-2, 2]\\).

As shown in Figure 14-9, under this definition, \\(dp[i, j]\\) represents the number of ways for state \\([i, j]\\). The state transition equation is then:

\\[ \\begin{cases} dp[i, 1] = dp[i-1, 2] \\\\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \\end{cases} \\]

Figure 14-9   Recurrence relation considering constraints

Finally, return \\(dp[n, 1] + dp[n, 2]\\), where the sum of the two represents the total number of ways to climb to the \\(n\\)-th step:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_constraint_dp.py
def climbing_stairs_constraint_dp(n: int) -> int:\n    \"\"\"Climbing stairs with constraint: Dynamic programming\"\"\"\n    if n == 1 or n == 2:\n        return 1\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [[0] * 3 for _ in range(n + 1)]\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1][1], dp[1][2] = 1, 0\n    dp[2][1], dp[2][2] = 0, 1\n    # State transition: gradually solve larger subproblems from smaller ones\n    for i in range(3, n + 1):\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    return dp[n][1] + dp[n][2]\n
climbing_stairs_constraint_dp.cpp
/* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    vector<vector<int>> dp(n + 1, vector<int>(3, 0));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.java
/* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    int[][] dp = new int[n + 1][3];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.cs
/* Climbing stairs with constraint: Dynamic programming */\nint ClimbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    int[,] dp = new int[n + 1, 3];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1, 1] = 1;\n    dp[1, 2] = 0;\n    dp[2, 1] = 0;\n    dp[2, 2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i, 1] = dp[i - 1, 2];\n        dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];\n    }\n    return dp[n, 1] + dp[n, 2];\n}\n
climbing_stairs_constraint_dp.go
/* Climbing stairs with constraint: Dynamic programming */\nfunc climbingStairsConstraintDP(n int) int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    dp := make([][3]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        dp[i][1] = dp[i-1][2]\n        dp[i][2] = dp[i-2][1] + dp[i-2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.swift
/* Climbing stairs with constraint: Dynamic programming */\nfunc climbingStairsConstraintDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3 ... n {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.js
/* Climbing stairs with constraint: Dynamic programming */\nfunction climbingStairsConstraintDP(n) {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = Array.from(new Array(n + 1), () => new Array(3));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.ts
/* Climbing stairs with constraint: Dynamic programming */\nfunction climbingStairsConstraintDP(n: number): number {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = Array.from({ length: n + 1 }, () => new Array(3));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.dart
/* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n  if (n == 1 || n == 2) {\n    return 1;\n  }\n  // Initialize dp table, used to store solutions to subproblems\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1][1] = 1;\n  dp[1][2] = 0;\n  dp[2][1] = 0;\n  dp[2][2] = 1;\n  // State transition: gradually solve larger subproblems from smaller ones\n  for (int i = 3; i <= n; i++) {\n    dp[i][1] = dp[i - 1][2];\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n  }\n  return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.rs
/* Climbing stairs with constraint: Dynamic programming */\nfn climbing_stairs_constraint_dp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return 1;\n    };\n    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![vec![-1; 3]; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3..=n {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.c
/* Climbing stairs with constraint: Dynamic programming */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(3, sizeof(int));\n    }\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    int res = dp[n][1] + dp[n][2];\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
climbing_stairs_constraint_dp.kt
/* Climbing stairs with constraint: Dynamic programming */\nfun climbingStairsConstraintDP(n: Int): Int {\n    if (n == 1 || n == 2) {\n        return 1\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = Array(n + 1) { IntArray(3) }\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (i in 3..n) {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.rb
### Climbing stairs with constraint: DP ###\ndef climbing_stairs_constraint_dp(n)\n  return 1 if n == 1 || n == 2\n\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1) { Array.new(3, 0) }\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1][1], dp[1][2] = 1, 0\n  dp[2][1], dp[2][2] = 0, 1\n  # State transition: gradually solve larger subproblems from smaller ones\n  for i in 3...(n + 1)\n    dp[i][1] = dp[i - 1][2]\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n  end\n\n  dp[n][1] + dp[n][2]\nend\n

In the above case, since we only need to consider one more preceding state, we can still make the problem satisfy no aftereffects by expanding the state definition. However, some problems have very severe \"aftereffects\".

Climbing stairs with obstacle generation

Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time. It is stipulated that when climbing to the \\(i\\)-th step, the system will automatically place an obstacle on the \\(2i\\)-th step, and thereafter no round is allowed to jump to the \\(2i\\)-th step. For example, if the first two rounds jump to the \\(2\\)nd and \\(3\\)rd steps, then afterwards you cannot jump to the \\(4\\)th and \\(6\\)th steps. How many ways are there to climb to the top?

In this problem, the next jump depends on all past states, because each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming is often difficult to solve.

In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy no aftereffects. For such problems, we usually choose to use other methods, such as heuristic search, genetic algorithms, reinforcement learning, etc., to obtain usable local optimal solutions within a limited time.

","path":["Chapter 14. Dynamic Programming","14.2   Characteristics of Dynamic Programming Problems"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   Dynamic Programming Problem-Solving Approach","text":"

The previous two sections introduced the main characteristics of dynamic programming problems. Next, let us explore two more practical issues together.

  1. How to determine whether a problem is a dynamic programming problem?
  2. What is the complete process for solving a dynamic programming problem, and where should we start?
","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431-problem-determination","level":2,"title":"14.3.1   Problem Determination","text":"

Generally speaking, if a problem contains overlapping subproblems, optimal substructure, and satisfies no aftereffects, then it is usually suitable for solving with dynamic programming. However, it is difficult to directly extract these characteristics from the problem description. Therefore, we usually relax the conditions and first observe whether the problem is suitable for solving with backtracking (exhaustive search).

Problems suitable for solving with backtracking usually satisfy the \"decision tree model\", which means the problem can be described using a tree structure, where each node represents a decision and each path represents a sequence of decisions.

In other words, if a problem contains an explicit concept of decisions, and the solution is generated through a series of decisions, then it satisfies the decision tree model and can usually be solved using backtracking.

On this basis, dynamic programming problems also have some \"bonus points\" for determination.

  • The problem contains descriptions such as maximum (minimum) or most (least), indicating optimization.
  • The problem's state can be represented using a list, multi-dimensional matrix, or tree, and a state has a recurrence relation with its surrounding states.

Correspondingly, there are also some \"penalty points\".

  • The goal of the problem is to find all possible solutions, rather than finding the optimal solution.
  • The problem description has obvious permutation and combination characteristics, requiring the return of specific multiple solutions.

If a problem satisfies the decision tree model and has relatively obvious \"bonus points\", we can assume it is a dynamic programming problem and verify it during the solving process.

","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432-problem-solving-steps","level":2,"title":"14.3.2   Problem-Solving Steps","text":"

The problem-solving process for dynamic programming varies depending on the nature and difficulty of the problem, but generally follows these steps: describe decisions, define states, establish the \\(dp\\) table, derive state transition equations, determine boundary conditions, etc.

To illustrate the problem-solving steps more vividly, we use a classic problem \"minimum path sum\" as an example.

Question

Given an \\(n \\times m\\) two-dimensional grid grid, where each cell in the grid contains a non-negative integer representing the cost of that cell. A robot starts from the top-left cell and can only move down or right at each step until reaching the bottom-right cell. Return the minimum path sum from the top-left to the bottom-right.

Figure 14-10 shows an example where the minimum path sum for the given grid is \\(13\\).

Figure 14-10   Minimum path sum example data

Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

The decision in each round of this problem is to move one step down or right from the current cell. Let the row and column indices of the current cell be \\([i, j]\\). After moving down or right, the indices become \\([i+1, j]\\) or \\([i, j+1]\\). Therefore, the state should include two variables, the row index and column index, denoted as \\([i, j]\\).

State \\([i, j]\\) corresponds to the subproblem: the minimum path sum from the starting point \\([0, 0]\\) to \\([i, j]\\), denoted as \\(dp[i, j]\\).

From this, we obtain the two-dimensional \\(dp\\) matrix shown in Figure 14-11, whose size is the same as the input grid \\(grid\\).

Figure 14-11   State definition and dp table

Note

The dynamic programming and backtracking processes can be described as a sequence of decisions, and the state consists of all decision variables. It should contain all variables describing the progress of problem-solving, and should contain sufficient information to derive the next state.

Each state corresponds to a subproblem, and we define a \\(dp\\) table to store the solutions to all subproblems. Each independent variable of the state is a dimension of the \\(dp\\) table. Essentially, the \\(dp\\) table is a mapping between states and solutions to subproblems.

Step 2: Identify the optimal substructure, and then derive the state transition equation

For state \\([i, j]\\), it can only be transferred from the cell above \\([i-1, j]\\) or the cell to the left \\([i, j-1]\\). Therefore, the optimal substructure is: the minimum path sum to reach \\([i, j]\\) is determined by the smaller of the minimum path sums of \\([i, j-1]\\) and \\([i-1, j]\\).

Based on the above analysis, the state transition equation shown in Figure 14-12 can be derived:

\\[ dp[i, j] = \\min(dp[i-1, j], dp[i, j-1]) + grid[i, j] \\]

Figure 14-12   Optimal substructure and state transition equation

Note

Based on the defined \\(dp\\) table, think about the relationship between the original problem and subproblems, and find the method to construct the optimal solution to the original problem from the optimal solutions to the subproblems, which is the optimal substructure.

Once we identify the optimal substructure, we can use it to construct the state transition equation.

Step 3: Determine boundary conditions and state transition order

In this problem, states in the first row can only come from the state to their left, and states in the first column can only come from the state above them. Therefore, the first row \\(i = 0\\) and first column \\(j = 0\\) are boundary conditions.

As shown in Figure 14-13, since each cell is transferred from the cell to its left and the cell above it, we use loops to traverse the matrix, with the outer loop traversing rows and the inner loop traversing columns.

Figure 14-13   Boundary conditions and state transition order

Note

Boundary conditions in dynamic programming are used to initialize the \\(dp\\) table, and in search are used for pruning.

The core of state transition order is to ensure that when computing the solution to the current problem, all the smaller subproblems it depends on have already been computed correctly.

Based on the above analysis, we can directly write the dynamic programming code. However, subproblem decomposition is a top-down approach, so implementing in the order \"brute force search \\(\\rightarrow\\) memoization \\(\\rightarrow\\) dynamic programming\" is more aligned with thinking habits.

","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1-method-1-brute-force-search","level":3,"title":"1.   Method 1: Brute Force Search","text":"

Starting from state \\([i, j]\\), continuously decompose into smaller states \\([i-1, j]\\) and \\([i, j-1]\\). The recursive function includes the following elements.

  • Recursive parameters: state \\([i, j]\\).
  • Return value: minimum path sum from \\([0, 0]\\) to \\([i, j]\\), which is \\(dp[i, j]\\).
  • Termination condition: when \\(i = 0\\) and \\(j = 0\\), return cost \\(grid[0, 0]\\).
  • Pruning: when \\(i < 0\\) or \\(j < 0\\), the index is out of bounds, return cost \\(+\\infty\\), representing infeasibility.

The implementation code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:\n    \"\"\"Minimum path sum: Brute-force search\"\"\"\n    # If it's the top-left cell, terminate the search\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # If row or column index is out of bounds, return +∞ cost\n    if i < 0 or j < 0:\n        return inf\n    # Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    up = min_path_sum_dfs(grid, i - 1, j)\n    left = min_path_sum_dfs(grid, i, j - 1)\n    # Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n
min_path_sum.cpp
/* Minimum path sum: Brute-force search */\nint minPathSumDFS(vector<vector<int>> &grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n}\n
min_path_sum.java
/* Minimum path sum: Brute-force search */\nint minPathSumDFS(int[][] grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.cs
/* Minimum path sum: Brute-force search */\nint MinPathSumDFS(int[][] grid, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = MinPathSumDFS(grid, i - 1, j);\n    int left = MinPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.Min(left, up) + grid[i][j];\n}\n
min_path_sum.go
/* Minimum path sum: Brute-force search */\nfunc minPathSumDFS(grid [][]int, i, j int) int {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    up := minPathSumDFS(grid, i-1, j)\n    left := minPathSumDFS(grid, i, j-1)\n    // Return the minimum path cost from top-left to (i, j)\n    return int(math.Min(float64(left), float64(up))) + grid[i][j]\n}\n
min_path_sum.swift
/* Minimum path sum: Brute-force search */\nfunc minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int {\n    // If it's the top-left cell, terminate the search\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    let up = minPathSumDFS(grid: grid, i: i - 1, j: j)\n    let left = minPathSumDFS(grid: grid, i: i, j: j - 1)\n    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n}\n
min_path_sum.js
/* Minimum path sum: Brute-force search */\nfunction minPathSumDFS(grid, i, j) {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.ts
/* Minimum path sum: Brute-force search */\nfunction minPathSumDFS(\n    grid: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.dart
/* Minimum path sum: Brute-force search */\nint minPathSumDFS(List<List<int>> grid, int i, int j) {\n  // If it's the top-left cell, terminate the search\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // If row or column index is out of bounds, return +∞ cost\n  if (i < 0 || j < 0) {\n    // In Dart, int type is fixed-range integer, no value representing \"infinity\"\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n  int up = minPathSumDFS(grid, i - 1, j);\n  int left = minPathSumDFS(grid, i, j - 1);\n  // Return the minimum path cost from top-left to (i, j)\n  return min(left, up) + grid[i][j];\n}\n
min_path_sum.rs
/* Minimum path sum: Brute-force search */\nfn min_path_sum_dfs(grid: &Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    let up = min_path_sum_dfs(grid, i - 1, j);\n    let left = min_path_sum_dfs(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    std::cmp::min(left, up) + grid[i as usize][j as usize]\n}\n
min_path_sum.c
/* Minimum path sum: Brute-force search */\nint minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // Return the minimum path cost from top-left to (i, j)\n    return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n}\n
min_path_sum.kt
/* Minimum path sum: Brute-force search */\nfun minPathSumDFS(grid: Array<IntArray>, i: Int, j: Int): Int {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n    val up = minPathSumDFS(grid, i - 1, j)\n    val left = minPathSumDFS(grid, i, j - 1)\n    // Return the minimum path cost from top-left to (i, j)\n    return min(left, up) + grid[i][j]\n}\n
min_path_sum.rb
### Minimum path sum: brute force search ###\ndef min_path_sum_dfs(grid, i, j)\n  # If it's the top-left cell, terminate the search\n  return grid[i][j] if i == 0 && j == 0\n  # If row or column index is out of bounds, return +∞ cost\n  return Float::INFINITY if i < 0 || j < 0\n  # Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1)\n  up = min_path_sum_dfs(grid, i - 1, j)\n  left = min_path_sum_dfs(grid, i, j - 1)\n  # Return the minimum path cost from top-left to (i, j)\n  [left, up].min + grid[i][j]\nend\n

Figure 14-14 shows the recursion tree rooted at \\(dp[2, 1]\\), which includes some overlapping subproblems whose number will increase sharply as the size of grid grid grows.

Essentially, the reason for overlapping subproblems is: there are multiple paths from the top-left corner to reach a certain cell.

Figure 14-14   Brute force search recursion tree

Each state has two choices, down and right, so the total number of steps from the top-left corner to the bottom-right corner is \\(m + n - 2\\), giving a worst-case time complexity of \\(O(2^{m + n})\\), where \\(n\\) and \\(m\\) are the number of rows and columns of the grid, respectively. Note that this calculation does not account for situations near the grid boundaries, where only one choice remains when reaching the grid boundary, so the actual number of paths will be somewhat less.

","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-method-2-memoization","level":3,"title":"2.   Method 2: Memoization","text":"

We introduce a memo list mem of the same size as grid grid to record the solutions to subproblems and prune overlapping subproblems:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dfs_mem(\n    grid: list[list[int]], mem: list[list[int]], i: int, j: int\n) -> int:\n    \"\"\"Minimum path sum: Memoization search\"\"\"\n    # If it's the top-left cell, terminate the search\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # If row or column index is out of bounds, return +∞ cost\n    if i < 0 or j < 0:\n        return inf\n    # If there's a record, return it directly\n    if mem[i][j] != -1:\n        return mem[i][j]\n    # Minimum path cost for left and upper cells\n    up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n    left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n    # Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n
min_path_sum.cpp
/* Minimum path sum: Memoization search */\nint minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
min_path_sum.java
/* Minimum path sum: Memoization search */\nint minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.cs
/* Minimum path sum: Memoization search */\nint MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = MinPathSumDFSMem(grid, mem, i - 1, j);\n    int left = MinPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.Min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.go
/* Minimum path sum: Memoization search */\nfunc minPathSumDFSMem(grid, mem [][]int, i, j int) int {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // If there's a record, return it directly\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\n    up := minPathSumDFSMem(grid, mem, i-1, j)\n    left := minPathSumDFSMem(grid, mem, i, j-1)\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = int(math.Min(float64(left), float64(up))) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.swift
/* Minimum path sum: Memoization search */\nfunc minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int {\n    // If it's the top-left cell, terminate the search\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // If there's a record, return it directly\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\n    let up = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j)\n    let left = minPathSumDFSMem(grid: grid, mem: &mem, i: i, j: j - 1)\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.js
/* Minimum path sum: Memoization search */\nfunction minPathSumDFSMem(grid, mem, i, j) {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] !== -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.ts
/* Minimum path sum: Memoization search */\nfunction minPathSumDFSMem(\n    grid: Array<Array<number>>,\n    mem: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // If it's the top-left cell, terminate the search\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.dart
/* Minimum path sum: Memoization search */\nint minPathSumDFSMem(List<List<int>> grid, List<List<int>> mem, int i, int j) {\n  // If it's the top-left cell, terminate the search\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // If row or column index is out of bounds, return +∞ cost\n  if (i < 0 || j < 0) {\n    // In Dart, int type is fixed-range integer, no value representing \"infinity\"\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // If there's a record, return it directly\n  if (mem[i][j] != -1) {\n    return mem[i][j];\n  }\n  // Minimum path cost for left and upper cells\n  int up = minPathSumDFSMem(grid, mem, i - 1, j);\n  int left = minPathSumDFSMem(grid, mem, i, j - 1);\n  // Record and return the minimum path cost from top-left to (i, j)\n  mem[i][j] = min(left, up) + grid[i][j];\n  return mem[i][j];\n}\n
min_path_sum.rs
/* Minimum path sum: Memoization search */\nfn min_path_sum_dfs_mem(grid: &Vec<Vec<i32>>, mem: &mut Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // If it's the top-left cell, terminate the search\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // If there's a record, return it directly\n    if mem[i as usize][j as usize] != -1 {\n        return mem[i as usize][j as usize];\n    }\n    // Minimum path cost for left and upper cells\n    let up = min_path_sum_dfs_mem(grid, mem, i - 1, j);\n    let left = min_path_sum_dfs_mem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize];\n    mem[i as usize][j as usize]\n}\n
min_path_sum.c
/* Minimum path sum: Memoization search */\nint minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // Minimum path cost for left and upper cells\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
min_path_sum.kt
/* Minimum path sum: Memoization search */\nfun minPathSumDFSMem(\n    grid: Array<IntArray>,\n    mem: Array<IntArray>,\n    i: Int,\n    j: Int\n): Int {\n    // If it's the top-left cell, terminate the search\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // If row or column index is out of bounds, return +∞ cost\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // If there's a record, return it directly\n    if (mem[i][j] != -1) {\n        return mem[i][j]\n    }\n    // Minimum path cost for left and upper cells\n    val up = minPathSumDFSMem(grid, mem, i - 1, j)\n    val left = minPathSumDFSMem(grid, mem, i, j - 1)\n    // Record and return the minimum path cost from top-left to (i, j)\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.rb
### Minimum path sum: memoization search ###\ndef min_path_sum_dfs_mem(grid, mem, i, j)\n  # If it's the top-left cell, terminate the search\n  return grid[0][0] if i == 0 && j == 0\n  # If row or column index is out of bounds, return +∞ cost\n  return Float::INFINITY if i < 0 || j < 0\n  # If there's a record, return it directly\n  return mem[i][j] if mem[i][j] != -1\n  # Minimum path cost for left and upper cells\n  up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n  left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n  # Record and return the minimum path cost from top-left to (i, j)\n  mem[i][j] = [left, up].min + grid[i][j]\nend\n

As shown in Figure 14-15, after introducing memoization, all subproblem solutions only need to be computed once, so the time complexity depends on the total number of states, which is the grid size \\(O(nm)\\).

Figure 14-15   Memoization recursion tree

","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#3-method-3-dynamic-programming","level":3,"title":"3.   Method 3: Dynamic Programming","text":"

Implement the dynamic programming solution based on iteration, as shown in the code below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dp(grid: list[list[int]]) -> int:\n    \"\"\"Minimum path sum: Dynamic programming\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Initialize dp table\n    dp = [[0] * m for _ in range(n)]\n    dp[0][0] = grid[0][0]\n    # State transition: first row\n    for j in range(1, m):\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    # State transition: first column\n    for i in range(1, n):\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    # State transition: rest of the rows and columns\n    for i in range(1, n):\n        for j in range(1, m):\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n    return dp[n - 1][m - 1]\n
min_path_sum.cpp
/* Minimum path sum: Dynamic programming */\nint minPathSumDP(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Initialize dp table\n    vector<vector<int>> dp(n, vector<int>(m));\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.java
/* Minimum path sum: Dynamic programming */\nint minPathSumDP(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Initialize dp table\n    int[][] dp = new int[n][m];\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.cs
/* Minimum path sum: Dynamic programming */\nint MinPathSumDP(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Initialize dp table\n    int[,] dp = new int[n, m];\n    dp[0, 0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0, j] = dp[0, j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i, 0] = dp[i - 1, 0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i, j] = Math.Min(dp[i, j - 1], dp[i - 1, j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1, m - 1];\n}\n
min_path_sum.go
/* Minimum path sum: Dynamic programming */\nfunc minPathSumDP(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Initialize dp table\n    dp := make([][]int, n)\n    for i := 0; i < n; i++ {\n        dp[i] = make([]int, m)\n    }\n    dp[0][0] = grid[0][0]\n    // State transition: first row\n    for j := 1; j < m; j++ {\n        dp[0][j] = dp[0][j-1] + grid[0][j]\n    }\n    // State transition: first column\n    for i := 1; i < n; i++ {\n        dp[i][0] = dp[i-1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i < n; i++ {\n        for j := 1; j < m; j++ {\n            dp[i][j] = int(math.Min(float64(dp[i][j-1]), float64(dp[i-1][j]))) + grid[i][j]\n        }\n    }\n    return dp[n-1][m-1]\n}\n
min_path_sum.swift
/* Minimum path sum: Dynamic programming */\nfunc minPathSumDP(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: m), count: n)\n    dp[0][0] = grid[0][0]\n    // State transition: first row\n    for j in 1 ..< m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // State transition: first column\n    for i in 1 ..< n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\n    for i in 1 ..< n {\n        for j in 1 ..< m {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
min_path_sum.js
/* Minimum path sum: Dynamic programming */\nfunction minPathSumDP(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i < n; i++) {\n        for (let j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.ts
/* Minimum path sum: Dynamic programming */\nfunction minPathSumDP(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i < n; i++) {\n        for (let j: number = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.dart
/* Minimum path sum: Dynamic programming */\nint minPathSumDP(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n, (i) => List.filled(m, 0));\n  dp[0][0] = grid[0][0];\n  // State transition: first row\n  for (int j = 1; j < m; j++) {\n    dp[0][j] = dp[0][j - 1] + grid[0][j];\n  }\n  // State transition: first column\n  for (int i = 1; i < n; i++) {\n    dp[i][0] = dp[i - 1][0] + grid[i][0];\n  }\n  // State transition: rest of the rows and columns\n  for (int i = 1; i < n; i++) {\n    for (int j = 1; j < m; j++) {\n      dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n    }\n  }\n  return dp[n - 1][m - 1];\n}\n
min_path_sum.rs
/* Minimum path sum: Dynamic programming */\nfn min_path_sum_dp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Initialize dp table\n    let mut dp = vec![vec![0; m]; n];\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for j in 1..m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for i in 1..n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for i in 1..n {\n        for j in 1..m {\n            dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    dp[n - 1][m - 1]\n}\n
min_path_sum.c
/* Minimum path sum: Dynamic programming */\nint minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Initialize dp table\n    int **dp = malloc(n * sizeof(int *));\n    for (int i = 0; i < n; i++) {\n        dp[i] = calloc(m, sizeof(int));\n    }\n    dp[0][0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // State transition: first column\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    int res = dp[n - 1][m - 1];\n    // Free memory\n    for (int i = 0; i < n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
min_path_sum.kt
/* Minimum path sum: Dynamic programming */\nfun minPathSumDP(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Initialize dp table\n    val dp = Array(n) { IntArray(m) }\n    dp[0][0] = grid[0][0]\n    // State transition: first row\n    for (j in 1..<m) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // State transition: first column\n    for (i in 1..<n) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // State transition: rest of the rows and columns\n    for (i in 1..<n) {\n        for (j in 1..<m) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
min_path_sum.rb
### Minimum path sum: dynamic programming ###\ndef min_path_sum_dp(grid)\n  n, m = grid.length, grid.first.length\n  # Initialize dp table\n  dp = Array.new(n) { Array.new(m, 0) }\n  dp[0][0] = grid[0][0]\n  # State transition: first row\n  (1...m).each { |j| dp[0][j] = dp[0][j - 1] + grid[0][j] }\n  # State transition: first column\n  (1...n).each { |i| dp[i][0] = dp[i - 1][0] + grid[i][0] }\n  # State transition: rest of the rows and columns\n  for i in 1...n\n    for j in 1...m\n      dp[i][j] = [dp[i][j - 1], dp[i - 1][j]].min + grid[i][j]\n    end\n  end\n  dp[n -1][m -1]\nend\n

Figure 14-16 shows the state transition process for minimum path sum, which traverses the entire grid, thus the time complexity is \\(O(nm)\\).

The array dp has size \\(n \\times m\\), thus the space complexity is \\(O(nm)\\).

<1><2><3><4><5><6><7><8><9><10><11><12>

Figure 14-16   Dynamic programming process for minimum path sum

","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#4-space-optimization","level":3,"title":"4.   Space Optimization","text":"

Since each cell is only related to the cell to its left and the cell above it, we can use a single-row array to implement the \\(dp\\) table.

Note that since the array dp can only represent the state of one row, we cannot initialize the first column state in advance, but rather update it when traversing each row:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dp_comp(grid: list[list[int]]) -> int:\n    \"\"\"Minimum path sum: Space-optimized dynamic programming\"\"\"\n    n, m = len(grid), len(grid[0])\n    # Initialize dp table\n    dp = [0] * m\n    # State transition: first row\n    dp[0] = grid[0][0]\n    for j in range(1, m):\n        dp[j] = dp[j - 1] + grid[0][j]\n    # State transition: rest of the rows\n    for i in range(1, n):\n        # State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        # State transition: rest of the columns\n        for j in range(1, m):\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n    return dp[m - 1]\n
min_path_sum.cpp
/* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // Initialize dp table\n    vector<int> dp(m);\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (int j = 1; j < m; j++) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.java
/* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // Initialize dp table\n    int[] dp = new int[m];\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.cs
/* Minimum path sum: Space-optimized dynamic programming */\nint MinPathSumDPComp(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // Initialize dp table\n    int[] dp = new int[m];\n    dp[0] = grid[0][0];\n    // State transition: first row\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.Min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.go
/* Minimum path sum: Space-optimized dynamic programming */\nfunc minPathSumDPComp(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // Initialize dp table\n    dp := make([]int, m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for j := 1; j < m; j++ {\n        dp[j] = dp[j-1] + grid[0][j]\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i < n; i++ {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\n        for j := 1; j < m; j++ {\n            dp[j] = int(math.Min(float64(dp[j-1]), float64(dp[j]))) + grid[i][j]\n        }\n    }\n    return dp[m-1]\n}\n
min_path_sum.swift
/* Minimum path sum: Space-optimized dynamic programming */\nfunc minPathSumDPComp(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for j in 1 ..< m {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // State transition: rest of the rows\n    for i in 1 ..< n {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\n        for j in 1 ..< m {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
min_path_sum.js
/* Minimum path sum: Space-optimized dynamic programming */\nfunction minPathSumDPComp(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = new Array(m);\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.ts
/* Minimum path sum: Space-optimized dynamic programming */\nfunction minPathSumDPComp(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // Initialize dp table\n    const dp = new Array(m);\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.dart
/* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // Initialize dp table\n  List<int> dp = List.filled(m, 0);\n  dp[0] = grid[0][0];\n  for (int j = 1; j < m; j++) {\n    dp[j] = dp[j - 1] + grid[0][j];\n  }\n  // State transition: rest of the rows\n  for (int i = 1; i < n; i++) {\n    // State transition: first column\n    dp[0] = dp[0] + grid[i][0];\n    // State transition: rest of the columns\n    for (int j = 1; j < m; j++) {\n      dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n    }\n  }\n  return dp[m - 1];\n}\n
min_path_sum.rs
/* Minimum path sum: Space-optimized dynamic programming */\nfn min_path_sum_dp_comp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // Initialize dp table\n    let mut dp = vec![0; m];\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for j in 1..m {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for i in 1..n {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for j in 1..m {\n            dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    dp[m - 1]\n}\n
min_path_sum.c
/* Minimum path sum: Space-optimized dynamic programming */\nint minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // Initialize dp table\n    int *dp = calloc(m, sizeof(int));\n    // State transition: first row\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i < n; i++) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0];\n        // State transition: rest of the columns\n        for (int j = 1; j < m; j++) {\n            dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    int res = dp[m - 1];\n    // Free memory\n    free(dp);\n    return res;\n}\n
min_path_sum.kt
/* Minimum path sum: Space-optimized dynamic programming */\nfun minPathSumDPComp(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // Initialize dp table\n    val dp = IntArray(m)\n    // State transition: first row\n    dp[0] = grid[0][0]\n    for (j in 1..<m) {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // State transition: rest of the rows\n    for (i in 1..<n) {\n        // State transition: first column\n        dp[0] = dp[0] + grid[i][0]\n        // State transition: rest of the columns\n        for (j in 1..<m) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
min_path_sum.rb
### Minimum path sum: space-optimized DP ###\ndef min_path_sum_dp_comp(grid)\n  n, m = grid.length, grid.first.length\n  # Initialize dp table\n  dp = Array.new(m, 0)\n  # State transition: first row\n  dp[0] = grid[0][0]\n  (1...m).each { |j| dp[j] = dp[j - 1] + grid[0][j] }\n  # State transition: rest of the rows\n  for i in 1...n\n    # State transition: first column\n    dp[0] = dp[0] + grid[i][0]\n    # State transition: rest of the columns\n    (1...m).each { |j| dp[j] = [dp[j - 1], dp[j]].min + grid[i][j] }\n  end\n  dp[m - 1]\nend\n
","path":["Chapter 14. Dynamic Programming","14.3   Dynamic Programming Problem-Solving Approach"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/","level":1,"title":"14.6   Edit Distance Problem","text":"

Edit distance, also known as Levenshtein distance, refers to the minimum number of edits required to transform one string into another, commonly used in information retrieval and natural language processing to measure the similarity between two sequences.

Question

Given two strings \\(s\\) and \\(t\\), return the minimum number of edits required to transform \\(s\\) into \\(t\\).

You can perform three types of edit operations on a string: insert a character, delete a character, or replace a character with any other character.

As shown in Figure 14-27, transforming kitten into sitting requires 3 edits, including 2 replacements and 1 insertion; transforming hello into algo requires 3 steps, including 2 replacements and 1 deletion.

Figure 14-27   Example data for edit distance

The edit distance problem can be naturally explained using the decision tree model. Strings correspond to tree nodes, and a round of decision (one edit operation) corresponds to an edge of the tree.

As shown in Figure 14-28, without restricting operations, each node can branch into many edges, with each edge corresponding to one operation, meaning there are many possible paths to transform hello into algo.

From the perspective of the decision tree, the goal of this problem is to find the shortest path between node hello and node algo.

Figure 14-28   Representing edit distance problem based on decision tree model

","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#1-dynamic-programming-approach","level":3,"title":"1.   Dynamic Programming Approach","text":"

Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

Each round of decision involves performing one edit operation on string \\(s\\).

We want the problem scale to gradually decrease during the editing process, which allows us to construct subproblems. Let the lengths of strings \\(s\\) and \\(t\\) be \\(n\\) and \\(m\\) respectively. We first consider the tail characters of the two strings, \\(s[n-1]\\) and \\(t[m-1]\\).

  • If \\(s[n-1]\\) and \\(t[m-1]\\) are the same, we can skip them and directly consider \\(s[n-2]\\) and \\(t[m-2]\\).
  • If \\(s[n-1]\\) and \\(t[m-1]\\) are different, we need to perform one edit on \\(s\\) (insert, delete, or replace) to make the tail characters of the two strings the same, allowing us to skip them and consider a smaller-scale problem.

In other words, each round of decision (edit operation) we make on string \\(s\\) will change the remaining characters to be matched in \\(s\\) and \\(t\\). Therefore, the state is the \\(i\\)-th and \\(j\\)-th characters currently being considered in \\(s\\) and \\(t\\), denoted as \\([i, j]\\).

State \\([i, j]\\) corresponds to the subproblem: the minimum number of edits required to change the first \\(i\\) characters of \\(s\\) into the first \\(j\\) characters of \\(t\\).

From this, we obtain a two-dimensional \\(dp\\) table of size \\((i+1) \\times (j+1)\\).

Step 2: Identify the optimal substructure, and then derive the state transition equation

Consider subproblem \\(dp[i, j]\\), where the tail characters of the corresponding two strings are \\(s[i-1]\\) and \\(t[j-1]\\), which can be divided into the three cases shown in Figure 14-29 based on different edit operations.

  1. Insert \\(t[j-1]\\) after \\(s[i-1]\\), then the remaining subproblem is \\(dp[i, j-1]\\).
  2. Delete \\(s[i-1]\\), then the remaining subproblem is \\(dp[i-1, j]\\).
  3. Replace \\(s[i-1]\\) with \\(t[j-1]\\), then the remaining subproblem is \\(dp[i-1, j-1]\\).

Figure 14-29   State transition for edit distance

Based on the above analysis, the optimal substructure can be obtained: the minimum number of edits for \\(dp[i, j]\\) equals the minimum among the minimum edit steps of \\(dp[i, j-1]\\), \\(dp[i-1, j]\\), and \\(dp[i-1, j-1]\\), plus the edit step \\(1\\) for this time. The corresponding state transition equation is:

\\[ dp[i, j] = \\min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \\]

Please note that when \\(s[i-1]\\) and \\(t[j-1]\\) are the same, no edit is required for the current character, in which case the state transition equation is:

\\[ dp[i, j] = dp[i-1, j-1] \\]

Step 3: Determine boundary conditions and state transition order

When both strings are empty, the number of edit steps is \\(0\\), i.e., \\(dp[0, 0] = 0\\). When \\(s\\) is empty but \\(t\\) is not, the minimum number of edit steps equals the length of \\(t\\), i.e., the first row \\(dp[0, j] = j\\). When \\(s\\) is not empty but \\(t\\) is empty, the minimum number of edit steps equals the length of \\(s\\), i.e., the first column \\(dp[i, 0] = i\\).

Observing the state transition equation, the solution \\(dp[i, j]\\) depends on solutions to the left, above, and upper-left, so the entire \\(dp\\) table can be traversed in order through two nested loops.

","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
def edit_distance_dp(s: str, t: str) -> int:\n    \"\"\"Edit distance: Dynamic programming\"\"\"\n    n, m = len(s), len(t)\n    dp = [[0] * (m + 1) for _ in range(n + 1)]\n    # State transition: first row and first column\n    for i in range(1, n + 1):\n        dp[i][0] = i\n    for j in range(1, m + 1):\n        dp[0][j] = j\n    # State transition: rest of the rows and columns\n    for i in range(1, n + 1):\n        for j in range(1, m + 1):\n            if s[i - 1] == t[j - 1]:\n                # If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            else:\n                # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1\n    return dp[n][m]\n
edit_distance.cpp
/* Edit distance: Dynamic programming */\nint editDistanceDP(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));\n    // State transition: first row and first column\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.java
/* Edit distance: Dynamic programming */\nint editDistanceDP(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[][] dp = new int[n + 1][m + 1];\n    // State transition: first row and first column\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.cs
/* Edit distance: Dynamic programming */\nint EditDistanceDP(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[,] dp = new int[n + 1, m + 1];\n    // State transition: first row and first column\n    for (int i = 1; i <= n; i++) {\n        dp[i, 0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0, j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[i, j] = dp[i - 1, j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n, m];\n}\n
edit_distance.go
/* Edit distance: Dynamic programming */\nfunc editDistanceDP(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, m+1)\n    }\n    // State transition: first row and first column\n    for i := 1; i <= n; i++ {\n        dp[i][0] = i\n    }\n    for j := 1; j <= m; j++ {\n        dp[0][j] = j\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= m; j++ {\n            if s[i-1] == t[j-1] {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i-1][j-1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.swift
/* Edit distance: Dynamic programming */\nfunc editDistanceDP(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)\n    // State transition: first row and first column\n    for i in 1 ... n {\n        dp[i][0] = i\n    }\n    for j in 1 ... m {\n        dp[0][j] = j\n    }\n    // State transition: rest of the rows and columns\n    for i in 1 ... n {\n        for j in 1 ... m {\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.js
/* Edit distance: Dynamic programming */\nfunction editDistanceDP(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));\n    // State transition: first row and first column\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.ts
/* Edit distance: Dynamic programming */\nfunction editDistanceDP(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: m + 1 }, () => 0)\n    );\n    // State transition: first row and first column\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.dart
/* Edit distance: Dynamic programming */\nint editDistanceDP(String s, String t) {\n  int n = s.length, m = t.length;\n  List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));\n  // State transition: first row and first column\n  for (int i = 1; i <= n; i++) {\n    dp[i][0] = i;\n  }\n  for (int j = 1; j <= m; j++) {\n    dp[0][j] = j;\n  }\n  // State transition: rest of the rows and columns\n  for (int i = 1; i <= n; i++) {\n    for (int j = 1; j <= m; j++) {\n      if (s[i - 1] == t[j - 1]) {\n        // If two characters are equal, skip both characters\n        dp[i][j] = dp[i - 1][j - 1];\n      } else {\n        // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n      }\n    }\n  }\n  return dp[n][m];\n}\n
edit_distance.rs
/* Edit distance: Dynamic programming */\nfn edit_distance_dp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![vec![0; m + 1]; n + 1];\n    // State transition: first row and first column\n    for i in 1..=n {\n        dp[i][0] = i as i32;\n    }\n    for j in 1..m {\n        dp[0][j] = j as i32;\n    }\n    // State transition: rest of the rows and columns\n    for i in 1..=n {\n        for j in 1..=m {\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] =\n                    std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    dp[n][m]\n}\n
edit_distance.c
/* Edit distance: Dynamic programming */\nint editDistanceDP(char *s, char *t, int n, int m) {\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(m + 1, sizeof(int));\n    }\n    // State transition: first row and first column\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    int res = dp[n][m];\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
edit_distance.kt
/* Edit distance: Dynamic programming */\nfun editDistanceDP(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = Array(n + 1) { IntArray(m + 1) }\n    // State transition: first row and first column\n    for (i in 1..n) {\n        dp[i][0] = i\n    }\n    for (j in 1..m) {\n        dp[0][j] = j\n    }\n    // State transition: rest of the rows and columns\n    for (i in 1..n) {\n        for (j in 1..m) {\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.rb
### Edit distance: dynamic programming ###\ndef edit_distance_dp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(n + 1) { Array.new(m + 1, 0) }\n  # State transition: first row and first column\n  (1...(n + 1)).each { |i| dp[i][0] = i }\n  (1...(m + 1)).each { |j| dp[0][j] = j }\n  # State transition: rest of the rows and columns\n  for i in 1...(n + 1)\n    for j in 1...(m +1)\n      if s[i - 1] == t[j - 1]\n        # If two characters are equal, skip both characters\n        dp[i][j] = dp[i - 1][j - 1]\n      else\n        # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1\n      end\n    end\n  end\n  dp[n][m]\nend\n

As shown in Figure 14-30, the state transition process for the edit distance problem is very similar to the knapsack problem and can both be viewed as the process of filling a two-dimensional grid.

<1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

Figure 14-30   Dynamic programming process for edit distance

","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#3-space-optimization","level":3,"title":"3.   Space Optimization","text":"

Since \\(dp[i, j]\\) is transferred from the solutions above \\(dp[i-1, j]\\), to the left \\(dp[i, j-1]\\), and to the upper-left \\(dp[i-1, j-1]\\), forward traversal will lose the upper-left solution \\(dp[i-1, j-1]\\), and reverse traversal cannot build \\(dp[i, j-1]\\) in advance, so neither traversal order is feasible.

For this reason, we can use a variable leftup to temporarily store the upper-left solution \\(dp[i-1, j-1]\\), so we only need to consider the solutions to the left and above. This situation is the same as the unbounded knapsack problem, allowing for forward traversal. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
def edit_distance_dp_comp(s: str, t: str) -> int:\n    \"\"\"Edit distance: Space-optimized dynamic programming\"\"\"\n    n, m = len(s), len(t)\n    dp = [0] * (m + 1)\n    # State transition: first row\n    for j in range(1, m + 1):\n        dp[j] = j\n    # State transition: rest of the rows\n    for i in range(1, n + 1):\n        # State transition: first column\n        leftup = dp[0]  # Temporarily store dp[i-1, j-1]\n        dp[0] += 1\n        # State transition: rest of the columns\n        for j in range(1, m + 1):\n            temp = dp[j]\n            if s[i - 1] == t[j - 1]:\n                # If two characters are equal, skip both characters\n                dp[j] = leftup\n            else:\n                # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(dp[j - 1], dp[j], leftup) + 1\n            leftup = temp  # Update for next round's dp[i-1, j-1]\n    return dp[m]\n
edit_distance.cpp
/* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<int> dp(m + 1, 0);\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.java
/* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[] dp = new int[m + 1];\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.cs
/* Edit distance: Space-optimized dynamic programming */\nint EditDistanceDPComp(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[] dp = new int[m + 1];\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.go
/* Edit distance: Space-optimized dynamic programming */\nfunc editDistanceDPComp(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([]int, m+1)\n    // State transition: first row\n    for j := 1; j <= m; j++ {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for i := 1; i <= n; i++ {\n        // State transition: first column\n        leftUp := dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for j := 1; j <= m; j++ {\n            temp := dp[j]\n            if s[i-1] == t[j-1] {\n                // If two characters are equal, skip both characters\n                dp[j] = leftUp\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1\n            }\n            leftUp = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
edit_distance.swift
/* Edit distance: Space-optimized dynamic programming */\nfunc editDistanceDPComp(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: 0, count: m + 1)\n    // State transition: first row\n    for j in 1 ... m {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for i in 1 ... n {\n        // State transition: first column\n        var leftup = dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for j in 1 ... m {\n            let temp = dp[j]\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
edit_distance.js
/* Edit distance: Space-optimized dynamic programming */\nfunction editDistanceDPComp(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // State transition: first row\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i <= n; i++) {\n        // State transition: first column\n        let leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.ts
/* Edit distance: Space-optimized dynamic programming */\nfunction editDistanceDPComp(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // State transition: first row\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (let i = 1; i <= n; i++) {\n        // State transition: first column\n        let leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.dart
/* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(String s, String t) {\n  int n = s.length, m = t.length;\n  List<int> dp = List.filled(m + 1, 0);\n  // State transition: first row\n  for (int j = 1; j <= m; j++) {\n    dp[j] = j;\n  }\n  // State transition: rest of the rows\n  for (int i = 1; i <= n; i++) {\n    // State transition: first column\n    int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n    dp[0] = i;\n    // State transition: rest of the columns\n    for (int j = 1; j <= m; j++) {\n      int temp = dp[j];\n      if (s[i - 1] == t[j - 1]) {\n        // If two characters are equal, skip both characters\n        dp[j] = leftup;\n      } else {\n        // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n      }\n      leftup = temp; // Update for next round's dp[i-1, j-1]\n    }\n  }\n  return dp[m];\n}\n
edit_distance.rs
/* Edit distance: Space-optimized dynamic programming */\nfn edit_distance_dp_comp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![0; m + 1];\n    // State transition: first row\n    for j in 1..m {\n        dp[j] = j as i32;\n    }\n    // State transition: rest of the rows\n    for i in 1..=n {\n        // State transition: first column\n        let mut leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i as i32;\n        // State transition: rest of the columns\n        for j in 1..=m {\n            let temp = dp[j];\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    dp[m]\n}\n
edit_distance.c
/* Edit distance: Space-optimized dynamic programming */\nint editDistanceDPComp(char *s, char *t, int n, int m) {\n    int *dp = calloc(m + 1, sizeof(int));\n    // State transition: first row\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // State transition: rest of the rows\n    for (int i = 1; i <= n; i++) {\n        // State transition: first column\n        int leftup = dp[0]; // Temporarily store dp[i-1, j-1]\n        dp[0] = i;\n        // State transition: rest of the columns\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup;\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // Update for next round's dp[i-1, j-1]\n        }\n    }\n    int res = dp[m];\n    // Free memory\n    free(dp);\n    return res;\n}\n
edit_distance.kt
/* Edit distance: Space-optimized dynamic programming */\nfun editDistanceDPComp(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = IntArray(m + 1)\n    // State transition: first row\n    for (j in 1..m) {\n        dp[j] = j\n    }\n    // State transition: rest of the rows\n    for (i in 1..n) {\n        // State transition: first column\n        var leftup = dp[0] // Temporarily store dp[i-1, j-1]\n        dp[0] = i\n        // State transition: rest of the columns\n        for (j in 1..m) {\n            val temp = dp[j]\n            if (s[i - 1] == t[j - 1]) {\n                // If two characters are equal, skip both characters\n                dp[j] = leftup\n            } else {\n                // Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // Update for next round's dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
edit_distance.rb
### Edit distance: space-optimized DP ###\ndef edit_distance_dp_comp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(m + 1, 0)\n  # State transition: first row\n  (1...(m + 1)).each { |j| dp[j] = j }\n  # State transition: rest of the rows\n  for i in 1...(n + 1)\n    # State transition: first column\n    leftup = dp.first # Temporarily store dp[i-1, j-1]\n    dp[0] += 1\n    # State transition: rest of the columns\n    for j in 1...(m + 1)\n      temp = dp[j]\n      if s[i - 1] == t[j - 1]\n        # If two characters are equal, skip both characters\n        dp[j] = leftup\n      else\n        # Minimum edit steps = minimum edit steps of insert, delete, replace + 1\n        dp[j] = [dp[j - 1], dp[j], leftup].min + 1\n      end\n      leftup = temp # Update for next round's dp[i-1, j-1]\n    end\n  end\n  dp[m]\nend\n
","path":["Chapter 14. Dynamic Programming","14.6   Edit Distance Problem"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/","level":1,"title":"14.1   Introduction to Dynamic Programming","text":"

Dynamic programming is an important algorithmic paradigm that decomposes a problem into a series of smaller subproblems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving time efficiency.

In this section, we start with a classic example, first presenting its brute force backtracking solution, observing the overlapping subproblems within it, and then gradually deriving a more efficient dynamic programming solution.

Climbing stairs

Given a staircase with \\(n\\) steps, where you can climb \\(1\\) or \\(2\\) steps at a time, how many different ways are there to reach the top?

As shown in Figure 14-1, for a \\(3\\)-step staircase, there are \\(3\\) different ways to reach the top.

Figure 14-1   Number of ways to reach the 3rd step

The goal of this problem is to find the number of ways, we can consider using backtracking to enumerate all possibilities. Specifically, imagine climbing stairs as a multi-round selection process: starting from the ground, choosing to go up \\(1\\) or \\(2\\) steps in each round, incrementing the count by \\(1\\) whenever the top of the stairs is reached, and pruning when exceeding the top. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_backtrack.py
def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:\n    \"\"\"Backtracking\"\"\"\n    # When climbing to the n-th stair, add 1 to the solution count\n    if state == n:\n        res[0] += 1\n    # Traverse all choices\n    for choice in choices:\n        # Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n:\n            continue\n        # Attempt: make a choice, update state\n        backtrack(choices, state + choice, n, res)\n        # Backtrack\n\ndef climbing_stairs_backtrack(n: int) -> int:\n    \"\"\"Climbing stairs: Backtracking\"\"\"\n    choices = [1, 2]  # Can choose to climb up 1 or 2 stairs\n    state = 0  # Start climbing from the 0-th stair\n    res = [0]  # Use res[0] to record the solution count\n    backtrack(choices, state, n, res)\n    return res[0]\n
climbing_stairs_backtrack.cpp
/* Backtracking */\nvoid backtrack(vector<int> &choices, int state, int n, vector<int> &res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    for (auto &choice : choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    vector<int> choices = {1, 2}; // Can choose to climb up 1 or 2 stairs\n    int state = 0;                // Start climbing from the 0-th stair\n    vector<int> res = {0};        // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res[0];\n}\n
climbing_stairs_backtrack.java
/* Backtracking */\nvoid backtrack(List<Integer> choices, int state, int n, List<Integer> res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (Integer choice : choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    List<Integer> choices = Arrays.asList(1, 2); // Can choose to climb up 1 or 2 stairs\n    int state = 0; // Start climbing from the 0-th stair\n    List<Integer> res = new ArrayList<>();\n    res.add(0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.cs
/* Backtracking */\nvoid Backtrack(List<int> choices, int state, int n, List<int> res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    foreach (int choice in choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        Backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint ClimbingStairsBacktrack(int n) {\n    List<int> choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    int state = 0; // Start climbing from the 0-th stair\n    List<int> res = [0]; // Use res[0] to record the solution count\n    Backtrack(choices, state, n, res);\n    return res[0];\n}\n
climbing_stairs_backtrack.go
/* Backtracking */\nfunc backtrack(choices []int, state, n int, res []int) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] = res[0] + 1\n    }\n    // Traverse all choices\n    for _, choice := range choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state+choice > n {\n            continue\n        }\n        // Attempt: make choice, update state\n        backtrack(choices, state+choice, n, res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunc climbingStairsBacktrack(n int) int {\n    // Can choose to climb up 1 or 2 stairs\n    choices := []int{1, 2}\n    // Start climbing from the 0-th stair\n    state := 0\n    res := make([]int, 1)\n    // Use res[0] to record the solution count\n    res[0] = 0\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
climbing_stairs_backtrack.swift
/* Backtracking */\nfunc backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] += 1\n    }\n    // Traverse all choices\n    for choice in choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n {\n            continue\n        }\n        // Attempt: make choice, update state\n        backtrack(choices: choices, state: state + choice, n: n, res: &res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunc climbingStairsBacktrack(n: Int) -> Int {\n    let choices = [1, 2] // Can choose to climb up 1 or 2 stairs\n    let state = 0 // Start climbing from the 0-th stair\n    var res: [Int] = []\n    res.append(0) // Use res[0] to record the solution count\n    backtrack(choices: choices, state: state, n: n, res: &res)\n    return res[0]\n}\n
climbing_stairs_backtrack.js
/* Backtracking */\nfunction backtrack(choices, state, n, res) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunction climbingStairsBacktrack(n) {\n    const choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    const state = 0; // Start climbing from the 0-th stair\n    const res = new Map();\n    res.set(0, 0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.ts
/* Backtracking */\nfunction backtrack(\n    choices: number[],\n    state: number,\n    n: number,\n    res: Map<0, any>\n): void {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state === n) res.set(0, res.get(0) + 1);\n    // Traverse all choices\n    for (const choice of choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfunction climbingStairsBacktrack(n: number): number {\n    const choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n    const state = 0; // Start climbing from the 0-th stair\n    const res = new Map();\n    res.set(0, 0); // Use res[0] to record the solution count\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.dart
/* Backtracking */\nvoid backtrack(List<int> choices, int state, int n, List<int> res) {\n  // When climbing to the n-th stair, add 1 to the solution count\n  if (state == n) {\n    res[0]++;\n  }\n  // Traverse all choices\n  for (int choice in choices) {\n    // Pruning: not allowed to go beyond the n-th stair\n    if (state + choice > n) continue;\n    // Attempt: make choice, update state\n    backtrack(choices, state + choice, n, res);\n    // Backtrack\n  }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n  List<int> choices = [1, 2]; // Can choose to climb up 1 or 2 stairs\n  int state = 0; // Start climbing from the 0-th stair\n  List<int> res = [];\n  res.add(0); // Use res[0] to record the solution count\n  backtrack(choices, state, n, res);\n  return res[0];\n}\n
climbing_stairs_backtrack.rs
/* Backtracking */\nfn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if state == n {\n        res[0] = res[0] + 1;\n    }\n    // Traverse all choices\n    for &choice in choices {\n        // Pruning: not allowed to go beyond the n-th stair\n        if state + choice > n {\n            continue;\n        }\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfn climbing_stairs_backtrack(n: usize) -> i32 {\n    let choices = vec![1, 2]; // Can choose to climb up 1 or 2 stairs\n    let state = 0; // Start climbing from the 0-th stair\n    let mut res = Vec::new();\n    res.push(0); // Use res[0] to record the solution count\n    backtrack(&choices, state, n as i32, &mut res);\n    res[0]\n}\n
climbing_stairs_backtrack.c
/* Backtracking */\nvoid backtrack(int *choices, int state, int n, int *res, int len) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0]++;\n    // Traverse all choices\n    for (int i = 0; i < len; i++) {\n        int choice = choices[i];\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n)\n            continue;\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res, len);\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nint climbingStairsBacktrack(int n) {\n    int choices[2] = {1, 2}; // Can choose to climb up 1 or 2 stairs\n    int state = 0;           // Start climbing from the 0-th stair\n    int *res = (int *)malloc(sizeof(int));\n    *res = 0; // Use res[0] to record the solution count\n    int len = sizeof(choices) / sizeof(int);\n    backtrack(choices, state, n, res, len);\n    int result = *res;\n    free(res);\n    return result;\n}\n
climbing_stairs_backtrack.kt
/* Backtracking */\nfun backtrack(\n    choices: MutableList<Int>,\n    state: Int,\n    n: Int,\n    res: MutableList<Int>\n) {\n    // When climbing to the n-th stair, add 1 to the solution count\n    if (state == n)\n        res[0] = res[0] + 1\n    // Traverse all choices\n    for (choice in choices) {\n        // Pruning: not allowed to go beyond the n-th stair\n        if (state + choice > n) continue\n        // Attempt: make choice, update state\n        backtrack(choices, state + choice, n, res)\n        // Backtrack\n    }\n}\n\n/* Climbing stairs: Backtracking */\nfun climbingStairsBacktrack(n: Int): Int {\n    val choices = mutableListOf(1, 2) // Can choose to climb up 1 or 2 stairs\n    val state = 0 // Start climbing from the 0-th stair\n    val res = mutableListOf<Int>()\n    res.add(0) // Use res[0] to record the solution count\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
climbing_stairs_backtrack.rb
### Backtracking ###\ndef backtrack(choices, state, n, res)\n  # When climbing to the n-th stair, add 1 to the solution count\n  res[0] += 1 if state == n\n  # Traverse all choices\n  for choice in choices\n    # Pruning: not allowed to go beyond the n-th stair\n    next if state + choice > n\n\n    # Attempt: make choice, update state\n    backtrack(choices, state + choice, n, res)\n  end\n  # Backtrack\nend\n\n### Climbing stairs: backtracking ###\ndef climbing_stairs_backtrack(n)\n  choices = [1, 2] # Can choose to climb up 1 or 2 stairs\n  state = 0 # Start climbing from the 0-th stair\n  res = [0] # Use res[0] to record the solution count\n  backtrack(choices, state, n, res)\n  res.first\nend\n
","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1411-method-1-brute-force-search","level":2,"title":"14.1.1   Method 1: Brute Force Search","text":"

Backtracking algorithms typically do not explicitly decompose problems, but rather treat solving the problem as a series of decision steps, searching for all possible solutions through trial and pruning.

We can try to analyze this problem from the perspective of problem decomposition. Let the number of ways to climb to the \\(i\\)-th step be \\(dp[i]\\), then \\(dp[i]\\) is the original problem, and its subproblems include:

\\[ dp[i-1], dp[i-2], \\dots, dp[2], dp[1] \\]

Since we can only go up \\(1\\) or \\(2\\) steps in each round, when we stand on the \\(i\\)-th step, we could only have been on the \\(i-1\\)-th or \\(i-2\\)-th step in the previous round. In other words, we can only reach the \\(i\\)-th step from the \\(i-1\\)-th or \\(i-2\\)-th step.

This leads to an important conclusion: the number of ways to climb to the \\(i-1\\)-th step plus the number of ways to climb to the \\(i-2\\)-th step equals the number of ways to climb to the \\(i\\)-th step. The formula is as follows:

\\[ dp[i] = dp[i-1] + dp[i-2] \\]

This means that in the stair climbing problem, there exists a recurrence relation among the subproblems, the solution to the original problem can be constructed from the solutions to the subproblems. Figure 14-2 illustrates this recurrence relation.

Figure 14-2   Recurrence relation for the number of ways

We can obtain a brute force search solution based on the recurrence formula. Starting from \\(dp[n]\\), recursively decompose a larger problem into the sum of two smaller problems, until reaching the smallest subproblems \\(dp[1]\\) and \\(dp[2]\\) and returning. Among them, the solutions to the smallest subproblems are known, namely \\(dp[1] = 1\\) and \\(dp[2] = 2\\), representing \\(1\\) and \\(2\\) ways to climb to the \\(1\\)st and \\(2\\)nd steps, respectively.

Observe the following code, which, like standard backtracking code, belongs to depth-first search but is more concise:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs.py
def dfs(i: int) -> int:\n    \"\"\"Search\"\"\"\n    # Known dp[1] and dp[2], return them\n    if i == 1 or i == 2:\n        return i\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1) + dfs(i - 2)\n    return count\n\ndef climbing_stairs_dfs(n: int) -> int:\n    \"\"\"Climbing stairs: Search\"\"\"\n    return dfs(n)\n
climbing_stairs_dfs.cpp
/* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.java
/* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.cs
/* Search */\nint DFS(int i) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1) + DFS(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nint ClimbingStairsDFS(int n) {\n    return DFS(n);\n}\n
climbing_stairs_dfs.go
/* Search */\nfunc dfs(i int) int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfs(i-1) + dfs(i-2)\n    return count\n}\n\n/* Climbing stairs: Search */\nfunc climbingStairsDFS(n int) int {\n    return dfs(n)\n}\n
climbing_stairs_dfs.swift
/* Search */\nfunc dfs(i: Int) -> Int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1) + dfs(i: i - 2)\n    return count\n}\n\n/* Climbing stairs: Search */\nfunc climbingStairsDFS(n: Int) -> Int {\n    dfs(i: n)\n}\n
climbing_stairs_dfs.js
/* Search */\nfunction dfs(i) {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nfunction climbingStairsDFS(n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.ts
/* Search */\nfunction dfs(i: number): number {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nfunction climbingStairsDFS(n: number): number {\n    return dfs(n);\n}\n
climbing_stairs_dfs.dart
/* Search */\nint dfs(int i) {\n  // Known dp[1] and dp[2], return them\n  if (i == 1 || i == 2) return i;\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1) + dfs(i - 2);\n  return count;\n}\n\n/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n  return dfs(n);\n}\n
climbing_stairs_dfs.rs
/* Search */\nfn dfs(i: usize) -> i32 {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1) + dfs(i - 2);\n    count\n}\n\n/* Climbing stairs: Search */\nfn climbing_stairs_dfs(n: usize) -> i32 {\n    dfs(n)\n}\n
climbing_stairs_dfs.c
/* Search */\nint dfs(int i) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* Climbing stairs: Search */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.kt
/* Search */\nfun dfs(i: Int): Int {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2) return i\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1) + dfs(i - 2)\n    return count\n}\n\n/* Climbing stairs: Search */\nfun climbingStairsDFS(n: Int): Int {\n    return dfs(n)\n}\n
climbing_stairs_dfs.rb
### Search ###\ndef dfs(i)\n  # Known dp[1] and dp[2], return them\n  return i if i == 1 || i == 2\n  # dp[i] = dp[i-1] + dp[i-2]\n  dfs(i - 1) + dfs(i - 2)\nend\n\n### Climbing stairs: search ###\ndef climbing_stairs_dfs(n)\n  dfs(n)\nend\n

Figure 14-3 shows the recursion tree formed by brute force search. For the problem \\(dp[n]\\), the depth of its recursion tree is \\(n\\), with a time complexity of \\(O(2^n)\\). Exponential order represents explosive growth; if we input a relatively large \\(n\\), we will fall into a long wait.

Figure 14-3   Recursion tree for climbing stairs

Observing the above figure, the exponential time complexity is caused by \"overlapping subproblems\". For example, \\(dp[9]\\) is decomposed into \\(dp[8]\\) and \\(dp[7]\\), and \\(dp[8]\\) is decomposed into \\(dp[7]\\) and \\(dp[6]\\), both of which contain the subproblem \\(dp[7]\\).

And so on, subproblems contain smaller overlapping subproblems, ad infinitum. The vast majority of computational resources are wasted on these overlapping subproblems.

","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1412-method-2-memoization","level":2,"title":"14.1.2   Method 2: Memoization","text":"

To improve algorithm efficiency, we want all overlapping subproblems to be computed only once. For this purpose, we declare an array mem to record the solution to each subproblem and prune overlapping subproblems during the search process.

  1. When computing \\(dp[i]\\) for the first time, we record it in mem[i] for later use.
  2. When we need to compute \\(dp[i]\\) again, we can directly retrieve the result from mem[i], thereby avoiding redundant computation of that subproblem.

The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs_mem.py
def dfs(i: int, mem: list[int]) -> int:\n    \"\"\"Memoization search\"\"\"\n    # Known dp[1] and dp[2], return them\n    if i == 1 or i == 2:\n        return i\n    # If record dp[i] exists, return it directly\n    if mem[i] != -1:\n        return mem[i]\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    # Record dp[i]\n    mem[i] = count\n    return count\n\ndef climbing_stairs_dfs_mem(n: int) -> int:\n    \"\"\"Climbing stairs: Memoization search\"\"\"\n    # mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    mem = [-1] * (n + 1)\n    return dfs(n, mem)\n
climbing_stairs_dfs_mem.cpp
/* Memoization search */\nint dfs(int i, vector<int> &mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    vector<int> mem(n + 1, -1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.java
/* Memoization search */\nint dfs(int i, int[] mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    int[] mem = new int[n + 1];\n    Arrays.fill(mem, -1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.cs
/* Memoization search */\nint DFS(int i, int[] mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1, mem) + DFS(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint ClimbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    int[] mem = new int[n + 1];\n    Array.Fill(mem, -1);\n    return DFS(n, mem);\n}\n
climbing_stairs_dfs_mem.go
/* Memoization search */\nfunc dfsMem(i int, mem []int) int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // If record dp[i] exists, return it directly\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfsMem(i-1, mem) + dfsMem(i-2, mem)\n    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfunc climbingStairsDFSMem(n int) int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    mem := make([]int, n+1)\n    for i := range mem {\n        mem[i] = -1\n    }\n    return dfsMem(n, mem)\n}\n
climbing_stairs_dfs_mem.swift
/* Memoization search */\nfunc dfs(i: Int, mem: inout [Int]) -> Int {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i\n    }\n    // If record dp[i] exists, return it directly\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1, mem: &mem) + dfs(i: i - 2, mem: &mem)\n    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfunc climbingStairsDFSMem(n: Int) -> Int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    var mem = Array(repeating: -1, count: n + 1)\n    return dfs(i: n, mem: &mem)\n}\n
climbing_stairs_dfs_mem.js
/* Memoization search */\nfunction dfs(i, mem) {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nfunction climbingStairsDFSMem(n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.ts
/* Memoization search */\nfunction dfs(i: number, mem: number[]): number {\n    // Known dp[1] and dp[2], return them\n    if (i === 1 || i === 2) return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nfunction climbingStairsDFSMem(n: number): number {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.dart
/* Memoization search */\nint dfs(int i, List<int> mem) {\n  // Known dp[1] and dp[2], return them\n  if (i == 1 || i == 2) return i;\n  // If record dp[i] exists, return it directly\n  if (mem[i] != -1) return mem[i];\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n  // Record dp[i]\n  mem[i] = count;\n  return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n  // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n  List<int> mem = List.filled(n + 1, -1);\n  return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.rs
/* Memoization search */\nfn dfs(i: usize, mem: &mut [i32]) -> i32 {\n    // Known dp[1] and dp[2], return them\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // If record dp[i] exists, return it directly\n    if mem[i] != -1 {\n        return mem[i];\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    count\n}\n\n/* Climbing stairs: Memoization search */\nfn climbing_stairs_dfs_mem(n: usize) -> i32 {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    let mut mem = vec![-1; n + 1];\n    dfs(n, &mut mem)\n}\n
climbing_stairs_dfs_mem.c
/* Memoization search */\nint dfs(int i, int *mem) {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2)\n        return i;\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // Record dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* Climbing stairs: Memoization search */\nint climbingStairsDFSMem(int n) {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    int *mem = (int *)malloc((n + 1) * sizeof(int));\n    for (int i = 0; i <= n; i++) {\n        mem[i] = -1;\n    }\n    int result = dfs(n, mem);\n    free(mem);\n    return result;\n}\n
climbing_stairs_dfs_mem.kt
/* Memoization search */\nfun dfs(i: Int, mem: IntArray): Int {\n    // Known dp[1] and dp[2], return them\n    if (i == 1 || i == 2) return i\n    // If record dp[i] exists, return it directly\n    if (mem[i] != -1) return mem[i]\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    // Record dp[i]\n    mem[i] = count\n    return count\n}\n\n/* Climbing stairs: Memoization search */\nfun climbingStairsDFSMem(n: Int): Int {\n    // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n    val mem = IntArray(n + 1)\n    mem.fill(-1)\n    return dfs(n, mem)\n}\n
climbing_stairs_dfs_mem.rb
### Memoization search ###\ndef dfs(i, mem)\n  # Known dp[1] and dp[2], return them\n  return i if i == 1 || i == 2\n  # If record dp[i] exists, return it directly\n  return mem[i] if mem[i] != -1\n\n  # dp[i] = dp[i-1] + dp[i-2]\n  count = dfs(i - 1, mem) + dfs(i - 2, mem)\n  # Record dp[i]\n  mem[i] = count\nend\n\n### Climbing stairs: memoization search ###\ndef climbing_stairs_dfs_mem(n)\n  # mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record\n  mem = Array.new(n + 1, -1)\n  dfs(n, mem)\nend\n

Observe Figure 14-4, after memoization, all overlapping subproblems only need to be computed once, optimizing the time complexity to \\(O(n)\\), which is a tremendous leap.

Figure 14-4   Recursion tree with memoization

","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1413-method-3-dynamic-programming","level":2,"title":"14.1.3   Method 3: Dynamic Programming","text":"

Memoization is a \"top-down\" method: we start from the original problem (root node), recursively decompose larger subproblems into smaller ones, until reaching the smallest known subproblems (leaf nodes). Afterward, by backtracking, we collect the solutions to the subproblems layer by layer to construct the solution to the original problem.

In contrast, dynamic programming is a \"bottom-up\" method: starting from the solutions to the smallest subproblems, iteratively constructing solutions to larger subproblems until obtaining the solution to the original problem.

Since dynamic programming does not include a backtracking process, it only requires loop iteration for implementation and does not need recursion. In the following code, we initialize an array dp to store the solutions to subproblems, which serves the same recording function as the array mem in memoization:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
def climbing_stairs_dp(n: int) -> int:\n    \"\"\"Climbing stairs: Dynamic programming\"\"\"\n    if n == 1 or n == 2:\n        return n\n    # Initialize dp table, used to store solutions to subproblems\n    dp = [0] * (n + 1)\n    # Initial state: preset the solution to the smallest subproblem\n    dp[1], dp[2] = 1, 2\n    # State transition: gradually solve larger subproblems from smaller ones\n    for i in range(3, n + 1):\n        dp[i] = dp[i - 1] + dp[i - 2]\n    return dp[n]\n
climbing_stairs_dp.cpp
/* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    vector<int> dp(n + 1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.java
/* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.cs
/* Climbing stairs: Dynamic programming */\nint ClimbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int[] dp = new int[n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.go
/* Climbing stairs: Dynamic programming */\nfunc climbingStairsDP(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    dp := make([]int, n+1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        dp[i] = dp[i-1] + dp[i-2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.swift
/* Climbing stairs: Dynamic programming */\nfunc climbingStairsDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    var dp = Array(repeating: 0, count: n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3 ... n {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.js
/* Climbing stairs: Dynamic programming */\nfunction climbingStairsDP(n) {\n    if (n === 1 || n === 2) return n;\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1).fill(-1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.ts
/* Climbing stairs: Dynamic programming */\nfunction climbingStairsDP(n: number): number {\n    if (n === 1 || n === 2) return n;\n    // Initialize dp table, used to store solutions to subproblems\n    const dp = new Array(n + 1).fill(-1);\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.dart
/* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n  if (n == 1 || n == 2) return n;\n  // Initialize dp table, used to store solutions to subproblems\n  List<int> dp = List.filled(n + 1, 0);\n  // Initial state: preset the solution to the smallest subproblem\n  dp[1] = 1;\n  dp[2] = 2;\n  // State transition: gradually solve larger subproblems from smaller ones\n  for (int i = 3; i <= n; i++) {\n    dp[i] = dp[i - 1] + dp[i - 2];\n  }\n  return dp[n];\n}\n
climbing_stairs_dp.rs
/* Climbing stairs: Dynamic programming */\nfn climbing_stairs_dp(n: usize) -> i32 {\n    // Known dp[1] and dp[2], return them\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    // Initialize dp table, used to store solutions to subproblems\n    let mut dp = vec![-1; n + 1];\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i in 3..=n {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    dp[n]\n}\n
climbing_stairs_dp.c
/* Climbing stairs: Dynamic programming */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // Initialize dp table, used to store solutions to subproblems\n    int *dp = (int *)malloc((n + 1) * sizeof(int));\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1;\n    dp[2] = 2;\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    int result = dp[n];\n    free(dp);\n    return result;\n}\n
climbing_stairs_dp.kt
/* Climbing stairs: Dynamic programming */\nfun climbingStairsDP(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    // Initialize dp table, used to store solutions to subproblems\n    val dp = IntArray(n + 1)\n    // Initial state: preset the solution to the smallest subproblem\n    dp[1] = 1\n    dp[2] = 2\n    // State transition: gradually solve larger subproblems from smaller ones\n    for (i in 3..n) {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.rb
### Climbing stairs: dynamic programming ###\ndef climbing_stairs_dp(n)\n  return n  if n == 1 || n == 2\n\n  # Initialize dp table, used to store solutions to subproblems\n  dp = Array.new(n + 1, 0)\n  # Initial state: preset the solution to the smallest subproblem\n  dp[1], dp[2] = 1, 2\n  # State transition: gradually solve larger subproblems from smaller ones\n  (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] }\n\n  dp[n]\nend\n

Figure 14-5 simulates the execution process of the above code.

Figure 14-5   Dynamic programming process for climbing stairs

Like backtracking algorithms, dynamic programming also uses the \"state\" concept to represent specific stages of problem solving, with each state corresponding to a subproblem and its corresponding local optimal solution. For example, the state in the stair climbing problem is defined as the current stair step number \\(i\\).

Based on the above content, we can summarize the commonly used terminology in dynamic programming.

  • The array dp is called the dp table, where \\(dp[i]\\) represents the solution to the subproblem corresponding to state \\(i\\).
  • The states corresponding to the smallest subproblems (the \\(1\\)st and \\(2\\)nd steps) are called initial states.
  • The recurrence formula \\(dp[i] = dp[i-1] + dp[i-2]\\) is called the state transition equation.
","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1414-space-optimization","level":2,"title":"14.1.4   Space Optimization","text":"

Observant readers may have noticed that since \\(dp[i]\\) is only related to \\(dp[i-1]\\) and \\(dp[i-2]\\), we do not need to use an array dp to store the solutions to all subproblems, but can simply use two variables to roll forward. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
def climbing_stairs_dp_comp(n: int) -> int:\n    \"\"\"Climbing stairs: Space-optimized dynamic programming\"\"\"\n    if n == 1 or n == 2:\n        return n\n    a, b = 1, 2\n    for _ in range(3, n + 1):\n        a, b = b, a + b\n    return b\n
climbing_stairs_dp.cpp
/* Climbing stairs: Space-optimized dynamic programming */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.java
/* Climbing stairs: Space-optimized dynamic programming */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.cs
/* Climbing stairs: Space-optimized dynamic programming */\nint ClimbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.go
/* Climbing stairs: Space-optimized dynamic programming */\nfunc climbingStairsDPComp(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    a, b := 1, 2\n    // State transition: gradually solve larger subproblems from smaller ones\n    for i := 3; i <= n; i++ {\n        a, b = b, a+b\n    }\n    return b\n}\n
climbing_stairs_dp.swift
/* Climbing stairs: Space-optimized dynamic programming */\nfunc climbingStairsDPComp(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    var a = 1\n    var b = 2\n    for _ in 3 ... n {\n        (a, b) = (b, a + b)\n    }\n    return b\n}\n
climbing_stairs_dp.js
/* Climbing stairs: Space-optimized dynamic programming */\nfunction climbingStairsDPComp(n) {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.ts
/* Climbing stairs: Space-optimized dynamic programming */\nfunction climbingStairsDPComp(n: number): number {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.dart
/* Climbing stairs: Space-optimized dynamic programming */\nint climbingStairsDPComp(int n) {\n  if (n == 1 || n == 2) return n;\n  int a = 1, b = 2;\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = a + b;\n    a = tmp;\n  }\n  return b;\n}\n
climbing_stairs_dp.rs
/* Climbing stairs: Space-optimized dynamic programming */\nfn climbing_stairs_dp_comp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    let (mut a, mut b) = (1, 2);\n    for _ in 3..=n {\n        let tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    b\n}\n
climbing_stairs_dp.c
/* Climbing stairs: Space-optimized dynamic programming */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.kt
/* Climbing stairs: Space-optimized dynamic programming */\nfun climbingStairsDPComp(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    var a = 1\n    var b = 2\n    for (i in 3..n) {\n        val temp = b\n        b += a\n        a = temp\n    }\n    return b\n}\n
climbing_stairs_dp.rb
### Climbing stairs: space-optimized DP ###\ndef climbing_stairs_dp_comp(n)\n  return n if n == 1 || n == 2\n\n  a, b = 1, 2\n  (3...(n + 1)).each { a, b = b, a + b }\n\n  b\nend\n

Observing the above code, since the space occupied by the array dp is saved, the space complexity is reduced from \\(O(n)\\) to \\(O(1)\\).

In dynamic programming problems, the current state often depends only on a limited number of preceding states, allowing us to retain only the necessary states and save memory space through \"dimension reduction\". This space optimization technique is called \"rolling variable\" or \"rolling array\".

","path":["Chapter 14. Dynamic Programming","14.1   Introduction to Dynamic Programming"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/","level":1,"title":"14.4   0-1 Knapsack Problem","text":"

The knapsack problem is an excellent introductory problem for dynamic programming and is one of the most common problem forms in dynamic programming. It has many variants, such as the 0-1 knapsack problem, the unbounded knapsack problem, and the multiple knapsack problem.

In this section, we will first solve the most common 0-1 knapsack problem.

Question

Given \\(n\\) items, where the weight of the \\(i\\)-th item is \\(wgt[i-1]\\) and its value is \\(val[i-1]\\), and a knapsack with capacity \\(cap\\). Each item can only be selected once. What is the maximum value that can be placed in the knapsack within the capacity limit?

Observe Figure 14-17. Since item number \\(i\\) starts counting from \\(1\\) and array indices start from \\(0\\), item \\(i\\) corresponds to weight \\(wgt[i-1]\\) and value \\(val[i-1]\\).

Figure 14-17   Example data for 0-1 knapsack

We can view the 0-1 knapsack problem as a process consisting of \\(n\\) rounds of decisions, where for each item there are two decisions: not putting it in and putting it in, thus the problem satisfies the decision tree model.

The goal of this problem is to find \"the maximum value that can be placed in the knapsack within the capacity limit\", so it is more likely to be a dynamic programming problem.

Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

For each item, if not placed in the knapsack, the knapsack capacity remains unchanged; if placed in, the knapsack capacity decreases. From this, we can derive the state definition: current item number \\(i\\) and knapsack capacity \\(c\\), denoted as \\([i, c]\\).

State \\([i, c]\\) corresponds to the subproblem: the maximum value among the first \\(i\\) items in a knapsack of capacity \\(c\\), denoted as \\(dp[i, c]\\).

What we need to find is \\(dp[n, cap]\\), so we need a two-dimensional \\(dp\\) table of size \\((n+1) \\times (cap+1)\\).

Step 2: Identify the optimal substructure, and then derive the state transition equation

After making the decision for item \\(i\\), what remains is the subproblem of the first \\(i-1\\) items, which can be divided into the following two cases.

  • Not putting item \\(i\\): The knapsack capacity remains unchanged, and the state changes to \\([i-1, c]\\).
  • Putting item \\(i\\): The knapsack capacity decreases by \\(wgt[i-1]\\), the value increases by \\(val[i-1]\\), and the state changes to \\([i-1, c-wgt[i-1]]\\).

The above analysis reveals the optimal substructure of this problem: the maximum value \\(dp[i, c]\\) equals the larger value between not putting item \\(i\\) and putting item \\(i\\). From this, the state transition equation can be derived:

\\[ dp[i, c] = \\max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \\]

Note that if the weight of the current item \\(wgt[i - 1]\\) exceeds the remaining knapsack capacity \\(c\\), then the only option is not to put it in the knapsack.

Step 3: Determine boundary conditions and state transition order

When there are no items or the knapsack capacity is \\(0\\), the maximum value is \\(0\\), i.e., the first column \\(dp[i, 0]\\) and the first row \\(dp[0, c]\\) are both equal to \\(0\\).

The current state \\([i, c]\\) is transferred from the state above \\([i-1, c]\\) and the state in the upper-left \\([i-1, c-wgt[i-1]]\\), so the entire \\(dp\\) table is traversed in order through two nested loops.

Based on the above analysis, we will next implement the brute force search, memoization, and dynamic programming solutions in order.

","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#1-method-1-brute-force-search","level":3,"title":"1.   Method 1: Brute Force Search","text":"

The search code includes the following elements.

  • Recursive parameters: state \\([i, c]\\).
  • Return value: solution to the subproblem \\(dp[i, c]\\).
  • Termination condition: when the item number is out of bounds \\(i = 0\\) or the remaining knapsack capacity is \\(0\\), terminate recursion and return value \\(0\\).
  • Pruning: if the weight of the current item exceeds the remaining knapsack capacity, only the option of not putting it in is available.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:\n    \"\"\"0-1 knapsack: Brute-force search\"\"\"\n    # If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 or c == 0:\n        return 0\n    # If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c:\n        return knapsack_dfs(wgt, val, i - 1, c)\n    # Calculate the maximum value of not putting in and putting in item i\n    no = knapsack_dfs(wgt, val, i - 1, c)\n    yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # Return the larger value of the two options\n    return max(no, yes)\n
knapsack.cpp
/* 0-1 knapsack: Brute-force search */\nint knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return max(no, yes);\n}\n
knapsack.java
/* 0-1 knapsack: Brute-force search */\nint knapsackDFS(int[] wgt, int[] val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
knapsack.cs
/* 0-1 knapsack: Brute-force search */\nint KnapsackDFS(int[] weight, int[] val, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (weight[i - 1] > c) {\n        return KnapsackDFS(weight, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = KnapsackDFS(weight, val, i - 1, c);\n    int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return Math.Max(no, yes);\n}\n
knapsack.go
/* 0-1 knapsack: Brute-force search */\nfunc knapsackDFS(wgt, val []int, i, c int) int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i-1] > c {\n        return knapsackDFS(wgt, val, i-1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    no := knapsackDFS(wgt, val, i-1, c)\n    yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1]\n    // Return the larger value of the two options\n    return int(math.Max(float64(no), float64(yes)))\n}\n
knapsack.swift
/* 0-1 knapsack: Brute-force search */\nfunc knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c {\n        return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    let no = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    let yes = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // Return the larger value of the two options\n    return max(no, yes)\n}\n
knapsack.js
/* 0-1 knapsack: Brute-force search */\nfunction knapsackDFS(wgt, val, i, c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
knapsack.ts
/* 0-1 knapsack: Brute-force search */\nfunction knapsackDFS(\n    wgt: Array<number>,\n    val: Array<number>,\n    i: number,\n    c: number\n): number {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return Math.max(no, yes);\n}\n
knapsack.dart
/* 0-1 knapsack: Brute-force search */\nint knapsackDFS(List<int> wgt, List<int> val, int i, int c) {\n  // If all items have been selected or knapsack has no remaining capacity, return value 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // If exceeds knapsack capacity, can only choose not to put it in\n  if (wgt[i - 1] > c) {\n    return knapsackDFS(wgt, val, i - 1, c);\n  }\n  // Calculate the maximum value of not putting in and putting in item i\n  int no = knapsackDFS(wgt, val, i - 1, c);\n  int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // Return the larger value of the two options\n  return max(no, yes);\n}\n
knapsack.rs
/* 0-1 knapsack: Brute-force search */\nfn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    let no = knapsack_dfs(wgt, val, i - 1, c);\n    let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // Return the larger value of the two options\n    std::cmp::max(no, yes)\n}\n
knapsack.c
/* 0-1 knapsack: Brute-force search */\nint knapsackDFS(int wgt[], int val[], int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Return the larger value of the two options\n    return myMax(no, yes);\n}\n
knapsack.kt
/* 0-1 knapsack: Brute-force search */\nfun knapsackDFS(\n    wgt: IntArray,\n    _val: IntArray,\n    i: Int,\n    c: Int\n): Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, _val, i - 1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    val no = knapsackDFS(wgt, _val, i - 1, c)\n    val yes = knapsackDFS(wgt, _val, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // Return the larger value of the two options\n    return max(no, yes)\n}\n
knapsack.rb
### 0-1 knapsack: brute force search ###\ndef knapsack_dfs(wgt, val, i, c)\n  # If all items have been selected or knapsack has no remaining capacity, return value 0\n  return 0 if i == 0 || c == 0\n  # If exceeds knapsack capacity, can only choose not to put it in\n  return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c\n  # Calculate the maximum value of not putting in and putting in item i\n  no = knapsack_dfs(wgt, val, i - 1, c)\n  yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # Return the larger value of the two options\n  [no, yes].max\nend\n

As shown in Figure 14-18, since each item generates two search branches of not selecting and selecting, the time complexity is \\(O(2^n)\\).

Observing the recursion tree, it is easy to see overlapping subproblems, such as \\(dp[1, 10]\\). When there are many items, large knapsack capacity, and especially many items with the same weight, the number of overlapping subproblems will increase significantly.

Figure 14-18   Brute force search recursion tree for 0-1 knapsack problem

","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#2-method-2-memoization","level":3,"title":"2.   Method 2: Memoization","text":"

To ensure that overlapping subproblems are only computed once, we use a memo list mem to record the solutions to subproblems, where mem[i][c] corresponds to \\(dp[i, c]\\).

After introducing memoization, the time complexity depends on the number of subproblems, which is \\(O(n \\times cap)\\). The implementation code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dfs_mem(\n    wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int\n) -> int:\n    \"\"\"0-1 knapsack: Memoization search\"\"\"\n    # If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 or c == 0:\n        return 0\n    # If there's a record, return it directly\n    if mem[i][c] != -1:\n        return mem[i][c]\n    # If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c:\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    # Calculate the maximum value of not putting in and putting in item i\n    no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n
knapsack.cpp
/* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(vector<int> &wgt, vector<int> &val, vector<vector<int>> &mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes);\n    return mem[i][c];\n}\n
knapsack.java
/* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.cs
/* 0-1 knapsack: Memoization search */\nint KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (weight[i - 1] > c) {\n        return KnapsackDFSMem(weight, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = KnapsackDFSMem(weight, val, mem, i - 1, c);\n    int yes = KnapsackDFSMem(weight, val, mem, i - 1, c - weight[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = Math.Max(no, yes);\n    return mem[i][c];\n}\n
knapsack.go
/* 0-1 knapsack: Memoization search */\nfunc knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i-1] > c {\n        return knapsackDFSMem(wgt, val, mem, i-1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    no := knapsackDFSMem(wgt, val, mem, i-1, c)\n    yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1]\n    // Return the larger value of the two options\n    mem[i][c] = int(math.Max(float64(no), float64(yes)))\n    return mem[i][c]\n}\n
knapsack.swift
/* 0-1 knapsack: Memoization search */\nfunc knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c {\n        return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    let no = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    let yes = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
knapsack.js
/* 0-1 knapsack: Memoization search */\nfunction knapsackDFSMem(wgt, val, mem, i, c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.ts
/* 0-1 knapsack: Memoization search */\nfunction knapsackDFSMem(\n    wgt: Array<number>,\n    val: Array<number>,\n    mem: Array<Array<number>>,\n    i: number,\n    c: number\n): number {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.dart
/* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(\n  List<int> wgt,\n  List<int> val,\n  List<List<int>> mem,\n  int i,\n  int c,\n) {\n  // If all items have been selected or knapsack has no remaining capacity, return value 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // If there's a record, return it directly\n  if (mem[i][c] != -1) {\n    return mem[i][c];\n  }\n  // If exceeds knapsack capacity, can only choose not to put it in\n  if (wgt[i - 1] > c) {\n    return knapsackDFSMem(wgt, val, mem, i - 1, c);\n  }\n  // Calculate the maximum value of not putting in and putting in item i\n  int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n  int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // Record and return the larger value of the two options\n  mem[i][c] = max(no, yes);\n  return mem[i][c];\n}\n
knapsack.rs
/* 0-1 knapsack: Memoization search */\nfn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec<Vec<i32>>, i: usize, c: usize) -> i32 {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if mem[i][c] != -1 {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = std::cmp::max(no, yes);\n    mem[i][c]\n}\n
knapsack.c
/* 0-1 knapsack: Memoization search */\nint knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // Record and return the larger value of the two options\n    mem[i][c] = myMax(no, yes);\n    return mem[i][c];\n}\n
knapsack.kt
/* 0-1 knapsack: Memoization search */\nfun knapsackDFSMem(\n    wgt: IntArray,\n    _val: IntArray,\n    mem: Array<IntArray>,\n    i: Int,\n    c: Int\n): Int {\n    // If all items have been selected or knapsack has no remaining capacity, return value 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // If there's a record, return it directly\n    if (mem[i][c] != -1) {\n        return mem[i][c]\n    }\n    // If exceeds knapsack capacity, can only choose not to put it in\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    }\n    // Calculate the maximum value of not putting in and putting in item i\n    val no = knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    val yes = knapsackDFSMem(wgt, _val, mem, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // Record and return the larger value of the two options\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
knapsack.rb
### 0-1 knapsack: memoization search ###\ndef knapsack_dfs_mem(wgt, val, mem, i, c)\n  # If all items have been selected or knapsack has no remaining capacity, return value 0\n  return 0 if i == 0 || c == 0\n  # If there's a record, return it directly\n  return mem[i][c] if mem[i][c] != -1\n  # If exceeds knapsack capacity, can only choose not to put it in\n  return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c\n  # Calculate the maximum value of not putting in and putting in item i\n  no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n  yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # Record and return the larger value of the two options\n  mem[i][c] = [no, yes].max\nend\n

Figure 14-19 shows the search branches pruned in memoization.

Figure 14-19   Memoization recursion tree for 0-1 knapsack problem

","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#3-method-3-dynamic-programming","level":3,"title":"3.   Method 3: Dynamic Programming","text":"

Dynamic programming is essentially the process of filling the \\(dp\\) table during state transitions. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 knapsack: Dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # State transition\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
knapsack.cpp
/* 0-1 knapsack: Dynamic programming */\nint knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.java
/* 0-1 knapsack: Dynamic programming */\nint knapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.cs
/* 0-1 knapsack: Dynamic programming */\nint KnapsackDP(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (weight[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i, c] = Math.Max(dp[i - 1, c - weight[i - 1]] + val[i - 1], dp[i - 1, c]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
knapsack.go
/* 0-1 knapsack: Dynamic programming */\nfunc knapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i-1][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.swift
/* 0-1 knapsack: Dynamic programming */\nfunc knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.js
/* 0-1 knapsack: Dynamic programming */\nfunction knapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(n + 1)\n        .fill(0)\n        .map(() => Array(cap + 1).fill(0));\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.ts
/* 0-1 knapsack: Dynamic programming */\nfunction knapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.dart
/* 0-1 knapsack: Dynamic programming */\nint knapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // The larger value between not selecting and selecting item i\n        dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
knapsack.rs
/* 0-1 knapsack: Dynamic programming */\nfn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = std::cmp::max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1],\n                );\n            }\n        }\n    }\n    dp[n][cap]\n}\n
knapsack.c
/* 0-1 knapsack: Dynamic programming */\nint knapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
knapsack.kt
/* 0-1 knapsack: Dynamic programming */\nfun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.rb
### 0-1 knapsack: dynamic programming ###\ndef knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # State transition\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c]\n      else\n        # The larger value between not selecting and selecting item i\n        dp[i][c] = [dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n

As shown in Figure 14-20, both time complexity and space complexity are determined by the size of the array dp, which is \\(O(n \\times cap)\\).

<1><2><3><4><5><6><7><8><9><10><11><12><13><14>

Figure 14-20   Dynamic programming process for 0-1 knapsack problem

","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#4-space-optimization","level":3,"title":"4.   Space Optimization","text":"

Since each state is only related to the state in the row above it, we can use two arrays rolling forward to reduce the space complexity from \\(O(n^2)\\) to \\(O(n)\\).

Further thinking, can we achieve space optimization using just one array? Observing, we can see that each state is transferred from the cell directly above or the cell in the upper-left. If there is only one array, when we start traversing row \\(i\\), that array still stores the state of row \\(i-1\\).

  • If using forward traversal, then when traversing to \\(dp[i, j]\\), the values in the upper-left \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\) may have already been overwritten, thus preventing correct state transition.
  • If using reverse traversal, there will be no overwriting issue, and state transition can proceed correctly.

Figure 14-21 shows the transition process from row \\(i = 1\\) to row \\(i = 2\\) using a single array. Please consider the difference between forward and reverse traversal.

<1><2><3><4><5><6>

Figure 14-21   Space-optimized dynamic programming process for 0-1 knapsack

In the code implementation, we simply need to delete the first dimension \\(i\\) of the array dp and change the inner loop to reverse traversal:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 knapsack: Space-optimized dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [0] * (cap + 1)\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in reverse order\n        for c in range(cap, 0, -1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
knapsack.cpp
/* 0-1 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<int> dp(cap + 1, 0);\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.java
/* 0-1 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.cs
/* 0-1 knapsack: Space-optimized dynamic programming */\nint KnapsackDPComp(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c > 0; c--) {\n            if (weight[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.go
/* 0-1 knapsack: Space-optimized dynamic programming */\nfunc knapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([]int, cap+1)\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in reverse order\n        for c := cap; c >= 1; c-- {\n            if wgt[i-1] <= c {\n                // The larger value between not selecting and selecting item i\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.swift
/* 0-1 knapsack: Space-optimized dynamic programming */\nfunc knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: cap + 1)\n    // State transition\n    for i in 1 ... n {\n        // Traverse in reverse order\n        for c in (1 ... cap).reversed() {\n            if wgt[i - 1] <= c {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.js
/* 0-1 knapsack: Space-optimized dynamic programming */\nfunction knapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(cap + 1).fill(0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.ts
/* 0-1 knapsack: Space-optimized dynamic programming */\nfunction knapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array(cap + 1).fill(0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.dart
/* 0-1 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<int> dp = List.filled(cap + 1, 0);\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    // Traverse in reverse order\n    for (int c = cap; c >= 1; c--) {\n      if (wgt[i - 1] <= c) {\n        // The larger value between not selecting and selecting item i\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
knapsack.rs
/* 0-1 knapsack: Space-optimized dynamic programming */\nfn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![0; cap + 1];\n    // State transition\n    for i in 1..=n {\n        // Traverse in reverse order\n        for c in (1..=cap).rev() {\n            if wgt[i - 1] <= c as i32 {\n                // The larger value between not selecting and selecting item i\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
knapsack.c
/* 0-1 knapsack: Space-optimized dynamic programming */\nint knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int *dp = calloc(cap + 1, sizeof(int));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        // Traverse in reverse order\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Free memory\n    free(dp);\n    return res;\n}\n
knapsack.kt
/* 0-1 knapsack: Space-optimized dynamic programming */\nfun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = IntArray(cap + 1)\n    // State transition\n    for (i in 1..n) {\n        // Traverse in reverse order\n        for (c in cap downTo 1) {\n            if (wgt[i - 1] <= c) {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.rb
### 0-1 knapsack: space-optimized DP ###\ndef knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(cap + 1, 0)\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in reverse order\n    for c in cap.downto(1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c]\n      else\n        # The larger value between not selecting and selecting item i\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
","path":["Chapter 14. Dynamic Programming","14.4   0-1 Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/summary/","level":1,"title":"14.7   Summary","text":"","path":["Chapter 14. Dynamic Programming","14.7   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Dynamic programming decomposes problems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving computational efficiency.
  • Without considering time constraints, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree contains a large number of overlapping subproblems, resulting in extremely low efficiency. By introducing a memo list, we can store the solutions to all computed subproblems, ensuring that overlapping subproblems are only computed once.
  • Memoization is a top-down recursive solution, while the corresponding dynamic programming is a bottom-up iterative solution, similar to \"filling in a table\". Since the current state only depends on certain local states, we can eliminate one dimension of the \\(dp\\) table to reduce space complexity.
  • Subproblem decomposition is a general algorithmic approach, with different properties in divide and conquer, dynamic programming, and backtracking.
  • Dynamic programming problems have three major characteristics: overlapping subproblems, optimal substructure, and no aftereffects.
  • If the optimal solution to the original problem can be constructed from the optimal solutions to the subproblems, then it has optimal substructure.
  • No aftereffects means that for a given state, its future development is only related to that state and has nothing to do with all past states. Many combinatorial optimization problems do not have no aftereffects and cannot be quickly solved using dynamic programming.

Knapsack problem

  • The knapsack problem is one of the most typical dynamic programming problems, with variants such as the 0-1 knapsack, unbounded knapsack, and multiple knapsack.
  • The state definition for the 0-1 knapsack is the maximum value among the first \\(i\\) items in a knapsack of capacity \\(c\\). Based on the two decisions of not putting an item in the knapsack and putting it in, the optimal substructure can be identified and the state transition equation constructed. In space optimization, since each state depends on the state directly above and to the upper-left, the list needs to be traversed in reverse order to avoid overwriting the upper-left state.
  • The unbounded knapsack problem has no limit on the selection quantity of each type of item, so the state transition for choosing to put in an item differs from the 0-1 knapsack problem. Since the state depends on the state directly above and directly to the left, space optimization should use forward traversal.
  • The coin change problem is a variant of the unbounded knapsack problem. It changes from seeking the \"maximum\" value to seeking the \"minimum\" number of coins, so \\(\\max()\\) in the state transition equation should be changed to \\(\\min()\\). It changes from seeking \"not exceeding\" the knapsack capacity to seeking \"exactly\" making up the target amount, so \\(amt + 1\\) is used to represent the invalid solution of \"unable to make up the target amount\".
  • Coin change problem II changes from seeking the \"minimum number of coins\" to seeking the \"number of coin combinations\", so the state transition equation correspondingly changes from \\(\\min()\\) to a summation operator.

Edit distance problem

  • Edit distance (Levenshtein distance) is used to measure the similarity between two strings, defined as the minimum number of edit steps from one string to another, with edit operations including insert, delete, and replace.
  • The state definition for the edit distance problem is the minimum number of edit steps required to change the first \\(i\\) characters of \\(s\\) into the first \\(j\\) characters of \\(t\\). When \\(s[i] \\ne t[j]\\), there are three decisions: insert, delete, replace, each with corresponding remaining subproblems. From this, the optimal substructure can be identified and the state transition equation constructed. When \\(s[i] = t[j]\\), no edit is required for the current character.
  • In edit distance, the state depends on the state directly above, directly to the left, and to the upper-left, so after space optimization, neither forward nor reverse traversal can correctly perform state transitions. For this reason, we use a variable to temporarily store the upper-left state, thus transforming to a situation equivalent to the unbounded knapsack problem, allowing for forward traversal after space optimization.
","path":["Chapter 14. Dynamic Programming","14.7   Summary"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   Unbounded Knapsack Problem","text":"

In this section, we first solve another common knapsack problem: the unbounded knapsack, and then explore a special case of it: the coin change problem.

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1451-unbounded-knapsack-problem","level":2,"title":"14.5.1   Unbounded Knapsack Problem","text":"

Question

Given \\(n\\) items, where the weight of the \\(i\\)-th item is \\(wgt[i-1]\\) and its value is \\(val[i-1]\\), and a knapsack with capacity \\(cap\\). Each item can be selected multiple times. What is the maximum value that can be placed in the knapsack within the capacity limit? An example is shown in Figure 14-22.

Figure 14-22   Example data for unbounded knapsack problem

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach","level":3,"title":"1.   Dynamic Programming Approach","text":"

The unbounded knapsack problem is very similar to the 0-1 knapsack problem, differing only in that there is no limit on the number of times an item can be selected.

  • In the 0-1 knapsack problem, there is only one of each type of item, so after placing item \\(i\\) in the knapsack, we can only choose from the first \\(i-1\\) items.
  • In the unbounded knapsack problem, the quantity of each type of item is unlimited, so after placing item \\(i\\) in the knapsack, we can still choose from the first \\(i\\) items.

Under the rules of the unbounded knapsack problem, the changes in state \\([i, c]\\) are divided into two cases.

  • Not putting item \\(i\\): Same as the 0-1 knapsack problem, transfer to \\([i-1, c]\\).
  • Putting item \\(i\\): Different from the 0-1 knapsack problem, transfer to \\([i, c-wgt[i-1]]\\).

Thus, the state transition equation becomes:

\\[ dp[i, c] = \\max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \\]","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

Comparing the code for the two problems, there is one change in state transition from \\(i-1\\) to \\(i\\), with everything else identical:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Unbounded knapsack: Dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # State transition\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
unbounded_knapsack.cpp
/* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.java
/* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.cs
/* Unbounded knapsack: Dynamic programming */\nint UnboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i, c] = Math.Max(dp[i - 1, c], dp[i, c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
unbounded_knapsack.go
/* Unbounded knapsack: Dynamic programming */\nfunc unboundedKnapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.swift
/* Unbounded knapsack: Dynamic programming */\nfunc unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.js
/* Unbounded knapsack: Dynamic programming */\nfunction unboundedKnapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.ts
/* Unbounded knapsack: Dynamic programming */\nfunction unboundedKnapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.dart
/* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // The larger value between not selecting and selecting item i\n        dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
unbounded_knapsack.rs
/* Unbounded knapsack: Dynamic programming */\nfn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.c
/* Unbounded knapsack: Dynamic programming */\nint unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = myMax(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
unbounded_knapsack.kt
/* Unbounded knapsack: Dynamic programming */\nfun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.rb
### Unbounded knapsack: dynamic programming ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # State transition\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[i][c] = dp[i - 1][c]\n      else\n        # The larger value between not selecting and selecting item i\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization","level":3,"title":"3.   Space Optimization","text":"

Since the current state is transferred from states on the left and above, after space optimization, each row in the \\(dp\\) table should be traversed in forward order.

This traversal order is exactly opposite to the 0-1 knapsack. Please refer to Figure 14-23 to understand the difference between the two.

<1><2><3><4><5><6>

Figure 14-23   Space-optimized dynamic programming process for unbounded knapsack problem

The code implementation is relatively simple, just delete the first dimension of the array dp:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Unbounded knapsack: Space-optimized dynamic programming\"\"\"\n    n = len(wgt)\n    # Initialize dp table\n    dp = [0] * (cap + 1)\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            else:\n                # The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
unbounded_knapsack.cpp
/* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // Initialize dp table\n    vector<int> dp(cap + 1, 0);\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.java
/* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.cs
/* Unbounded knapsack: Space-optimized dynamic programming */\nint UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // Initialize dp table\n    int[] dp = new int[cap + 1];\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.go
/* Unbounded knapsack: Space-optimized dynamic programming */\nfunc unboundedKnapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // Initialize dp table\n    dp := make([]int, cap+1)\n    // State transition\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.swift
/* Unbounded knapsack: Space-optimized dynamic programming */\nfunc unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: cap + 1)\n    // State transition\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.js
/* Unbounded knapsack: Space-optimized dynamic programming */\nfunction unboundedKnapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.ts
/* Unbounded knapsack: Space-optimized dynamic programming */\nfunction unboundedKnapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // Initialize dp table\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.dart
/* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // Initialize dp table\n  List<int> dp = List.filled(cap + 1, 0);\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c];\n      } else {\n        // The larger value between not selecting and selecting item i\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
unbounded_knapsack.rs
/* Unbounded knapsack: Space-optimized dynamic programming */\nfn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // Initialize dp table\n    let mut dp = vec![0; cap + 1];\n    // State transition\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
unbounded_knapsack.c
/* Unbounded knapsack: Space-optimized dynamic programming */\nint unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // Initialize dp table\n    int *dp = calloc(cap + 1, sizeof(int));\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c];\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // Free memory\n    free(dp);\n    return res;\n}\n
unbounded_knapsack.kt
/* Unbounded knapsack: Space-optimized dynamic programming */\nfun unboundedKnapsackDPComp(\n    wgt: IntArray,\n    _val: IntArray,\n    cap: Int\n): Int {\n    val n = wgt.size\n    // Initialize dp table\n    val dp = IntArray(cap + 1)\n    // State transition\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // If exceeds knapsack capacity, don't select item i\n                dp[c] = dp[c]\n            } else {\n                // The larger value between not selecting and selecting item i\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.rb
### Unbounded knapsack: space-optimized DP ###\ndef unbounded_knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # Initialize dp table\n  dp = Array.new(cap + 1, 0)\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for c in 1...(cap + 1)\n      if wgt[i -1] > c\n        # If exceeds knapsack capacity, don't select item i\n        dp[c] = dp[c]\n      else\n        # The larger value between not selecting and selecting item i\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1452-coin-change-problem","level":2,"title":"14.5.2   Coin Change Problem","text":"

The knapsack problem represents a large class of dynamic programming problems and has many variants, such as the coin change problem.

Question

Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type of coin is \\(coins[i - 1]\\), and the target amount is \\(amt\\). Each type of coin can be selected multiple times. What is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return \\(-1\\). An example is shown in Figure 14-24.

Figure 14-24   Example data for coin change problem

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach_1","level":3,"title":"1.   Dynamic Programming Approach","text":"

The coin change problem can be viewed as a special case of the unbounded knapsack problem, with the following connections and differences.

  • The two problems can be converted to each other: \"item\" corresponds to \"coin\", \"item weight\" corresponds to \"coin denomination\", and \"knapsack capacity\" corresponds to \"target amount\".
  • The optimization goals are opposite: the unbounded knapsack problem aims to maximize item value, while the coin change problem aims to minimize the number of coins.
  • The unbounded knapsack problem seeks solutions \"not exceeding\" the knapsack capacity, while the coin change problem seeks solutions that \"exactly\" make up the target amount.

Step 1: Think about the decisions in each round, define the state, and thus obtain the \\(dp\\) table

State \\([i, a]\\) corresponds to the subproblem: the minimum number of coins among the first \\(i\\) types of coins that can make up amount \\(a\\), denoted as \\(dp[i, a]\\).

The two-dimensional \\(dp\\) table has size \\((n+1) \\times (amt+1)\\).

Step 2: Identify the optimal substructure, and then derive the state transition equation

This problem differs from the unbounded knapsack problem in the following two aspects regarding the state transition equation.

  • This problem seeks the minimum value, so the operator \\(\\max()\\) needs to be changed to \\(\\min()\\).
  • The optimization target is the number of coins rather than item value, so when a coin is selected, simply execute \\(+1\\).
\\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

Step 3: Determine boundary conditions and state transition order

When the target amount is \\(0\\), the minimum number of coins needed to make it up is \\(0\\), so all \\(dp[i, 0]\\) in the first column equal \\(0\\).

When there are no coins, it is impossible to make up any amount \\(> 0\\), which is an invalid solution. To enable the \\(\\min()\\) function in the state transition equation to identify and filter out invalid solutions, we consider using \\(+ \\infty\\) to represent them, i.e., set all \\(dp[0, a]\\) in the first row to \\(+ \\infty\\).

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation_1","level":3,"title":"2.   Code Implementation","text":"

Most programming languages do not provide a \\(+ \\infty\\) variable, and can only use the maximum value of integer type int as a substitute. However, this can lead to large number overflow: the \\(+ 1\\) operation in the state transition equation may cause overflow.

For this reason, we use the number \\(amt + 1\\) to represent invalid solutions, because the maximum number of coins needed to make up \\(amt\\) is at most \\(amt\\). Before returning, check whether \\(dp[n, amt]\\) equals \\(amt + 1\\); if so, return \\(-1\\), indicating that the target amount cannot be made up. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
def coin_change_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Dynamic programming\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Initialize dp table\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # State transition: first row and first column\n    for a in range(1, amt + 1):\n        dp[0][a] = MAX\n    # State transition: rest of the rows and columns\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # The smaller value between not selecting and selecting coin i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n    return dp[n][amt] if dp[n][amt] != MAX else -1\n
coin_change.cpp
/* Coin change: Dynamic programming */\nint coinChangeDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.java
/* Coin change: Dynamic programming */\nint coinChangeDP(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][amt + 1];\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.cs
/* Coin change: Dynamic programming */\nint CoinChangeDP(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, amt + 1];\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0, a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i, a] = Math.Min(dp[i - 1, a], dp[i, a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n, amt] != MAX ? dp[n, amt] : -1;\n}\n
coin_change.go
/* Coin change: Dynamic programming */\nfunc coinChangeDP(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // State transition: first row and first column\n    for a := 1; a <= amt; a++ {\n        dp[0][a] = max\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = int(math.Min(float64(dp[i-1][a]), float64(dp[i][a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt]\n    }\n    return -1\n}\n
coin_change.swift
/* Coin change: Dynamic programming */\nfunc coinChangeDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // State transition: first row and first column\n    for a in 1 ... amt {\n        dp[0][a] = MAX\n    }\n    // State transition: rest of the rows and columns\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1\n}\n
coin_change.js
/* Coin change: Dynamic programming */\nfunction coinChangeDP(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // State transition: first row and first column\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
coin_change.ts
/* Coin change: Dynamic programming */\nfunction coinChangeDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // State transition: first row and first column\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
coin_change.dart
/* Coin change: Dynamic programming */\nint coinChangeDP(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // State transition: first row and first column\n  for (int a = 1; a <= amt; a++) {\n    dp[0][a] = MAX;\n  }\n  // State transition: rest of the rows and columns\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // The smaller value between not selecting and selecting coin i\n        dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.rs
/* Coin change: Dynamic programming */\nfn coin_change_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Initialize dp table\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // State transition: first row and first column\n    for a in 1..=amt {\n        dp[0][a] = max;\n    }\n    // State transition: rest of the rows and columns\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt] as i32;\n    } else {\n        -1\n    }\n}\n
coin_change.c
/* Coin change: Dynamic programming */\nint coinChangeDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // State transition: first row and first column\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // State transition: rest of the rows and columns\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[n][amt] != MAX ? dp[n][amt] : -1;\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
coin_change.kt
/* Coin change: Dynamic programming */\nfun coinChangeDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // State transition: first row and first column\n    for (a in 1..amt) {\n        dp[0][a] = MAX\n    }\n    // State transition: rest of the rows and columns\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[n][amt] != MAX) dp[n][amt] else -1\n}\n
coin_change.rb
### Coin change: dynamic programming ###\ndef coin_change_dp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # State transition: first row and first column\n  (1...(amt + 1)).each { |a| dp[0][a] = _MAX }\n  # State transition: rest of the rows and columns\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a]\n      else\n        # The smaller value between not selecting and selecting coin i\n        dp[i][a] = [dp[i - 1][a], dp[i][a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[n][amt] != _MAX ? dp[n][amt] : -1\nend\n

Figure 14-25 shows the dynamic programming process for coin change, which is very similar to the unbounded knapsack problem.

<1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

Figure 14-25   Dynamic programming process for coin change problem

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization_1","level":3,"title":"3.   Space Optimization","text":"

The space optimization for the coin change problem is handled in the same way as the unbounded knapsack problem:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
def coin_change_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Space-optimized dynamic programming\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # Initialize dp table\n    dp = [MAX] * (amt + 1)\n    dp[0] = 0\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            else:\n                # The smaller value between not selecting and selecting coin i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n    return dp[amt] if dp[amt] != MAX else -1\n
coin_change.cpp
/* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // Initialize dp table\n    vector<int> dp(amt + 1, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.java
/* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    Arrays.fill(dp, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.cs
/* Coin change: Space-optimized dynamic programming */\nint CoinChangeDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    Array.Fill(dp, MAX);\n    dp[0] = 0;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = Math.Min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.go
/* Coin change: Dynamic programming */\nfunc coinChangeDPComp(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // Initialize dp table\n    dp := make([]int, amt+1)\n    for i := 1; i <= amt; i++ {\n        dp[i] = max\n    }\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in forward order\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = int(math.Min(float64(dp[a]), float64(dp[a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt]\n    }\n    return -1\n}\n
coin_change.swift
/* Coin change: Space-optimized dynamic programming */\nfunc coinChangeDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // Initialize dp table\n    var dp = Array(repeating: MAX, count: amt + 1)\n    dp[0] = 0\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1\n}\n
coin_change.js
/* Coin change: Space-optimized dynamic programming */\nfunction coinChangeDPComp(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
coin_change.ts
/* Coin change: Space-optimized dynamic programming */\nfunction coinChangeDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
coin_change.dart
/* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // Initialize dp table\n  List<int> dp = List.filled(amt + 1, MAX);\n  dp[0] = 0;\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[a] = dp[a];\n      } else {\n        // The smaller value between not selecting and selecting coin i\n        dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.rs
/* Coin change: Space-optimized dynamic programming */\nfn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // Initialize dp table\n    let mut dp = vec![0; amt + 1];\n    dp.fill(max);\n    dp[0] = 0;\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt] as i32;\n    } else {\n        -1\n    }\n}\n
coin_change.c
/* Coin change: Space-optimized dynamic programming */\nint coinChangeDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // Initialize dp table\n    int *dp = malloc((amt + 1) * sizeof(int));\n    for (int j = 1; j <= amt; j++) {\n        dp[j] = MAX;\n    } \n    dp[0] = 0;\n\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[amt] != MAX ? dp[amt] : -1;\n    // Free memory\n    free(dp);\n    return res;\n}\n
coin_change.kt
/* Coin change: Space-optimized dynamic programming */\nfun coinChangeDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // Initialize dp table\n    val dp = IntArray(amt + 1)\n    dp.fill(MAX)\n    dp[0] = 0\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // The smaller value between not selecting and selecting coin i\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[amt] != MAX) dp[amt] else -1\n}\n
coin_change.rb
### Coin change: space-optimized DP ###\ndef coin_change_dp_comp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # Initialize dp table\n  dp = Array.new(amt + 1, _MAX)\n  dp[0] = 0\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[a] = dp[a]\n      else\n        # The smaller value between not selecting and selecting coin i\n        dp[a] = [dp[a], dp[a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[amt] != _MAX ? dp[amt] : -1\nend\n
","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1453-coin-change-problem-ii","level":2,"title":"14.5.3   Coin Change Problem Ii","text":"

Question

Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type of coin is \\(coins[i - 1]\\), and the target amount is \\(amt\\). Each type of coin can be selected multiple times. What is the number of coin combinations that can make up the target amount? An example is shown in Figure 14-26.

Figure 14-26   Example data for coin change problem II

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1-dynamic-programming-approach_2","level":3,"title":"1.   Dynamic Programming Approach","text":"

Compared to the previous problem, this problem's goal is to find the number of combinations, so the subproblem becomes: the number of combinations among the first \\(i\\) types of coins that can make up amount \\(a\\). The \\(dp\\) table remains a two-dimensional matrix of size \\((n+1) \\times (amt + 1)\\).

The number of combinations for the current state equals the sum of the combinations from not selecting the current coin and selecting the current coin. The state transition equation is:

\\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\]

When the target amount is \\(0\\), no coins need to be selected to make up the target amount, so all \\(dp[i, 0]\\) in the first column should be initialized to \\(1\\). When there are no coins, it is impossible to make up any amount \\(>0\\), so all \\(dp[0, a]\\) in the first row equal \\(0\\).

","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2-code-implementation_2","level":3,"title":"2.   Code Implementation","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
def coin_change_ii_dp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change II: Dynamic programming\"\"\"\n    n = len(coins)\n    # Initialize dp table\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # Initialize first column\n    for i in range(n + 1):\n        dp[i][0] = 1\n    # State transition\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n    return dp[n][amt]\n
coin_change_ii.cpp
/* Coin change II: Dynamic programming */\nint coinChangeIIDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Initialize dp table\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.java
/* Coin change II: Dynamic programming */\nint coinChangeIIDP(int[] coins, int amt) {\n    int n = coins.length;\n    // Initialize dp table\n    int[][] dp = new int[n + 1][amt + 1];\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.cs
/* Coin change II: Dynamic programming */\nint CoinChangeIIDP(int[] coins, int amt) {\n    int n = coins.Length;\n    // Initialize dp table\n    int[,] dp = new int[n + 1, amt + 1];\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i, 0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i, a] = dp[i - 1, a] + dp[i, a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n, amt];\n}\n
coin_change_ii.go
/* Coin change II: Dynamic programming */\nfunc coinChangeIIDP(coins []int, amt int) int {\n    n := len(coins)\n    // Initialize dp table\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // Initialize first column\n    for i := 0; i <= n; i++ {\n        dp[i][0] = 1\n    }\n    // State transition: rest of the rows and columns\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.swift
/* Coin change II: Dynamic programming */\nfunc coinChangeIIDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Initialize dp table\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // Initialize first column\n    for i in 0 ... n {\n        dp[i][0] = 1\n    }\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.js
/* Coin change II: Dynamic programming */\nfunction coinChangeIIDP(coins, amt) {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Initialize first column\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.ts
/* Coin change II: Dynamic programming */\nfunction coinChangeIIDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // Initialize first column\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.dart
/* Coin change II: Dynamic programming */\nint coinChangeIIDP(List<int> coins, int amt) {\n  int n = coins.length;\n  // Initialize dp table\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // Initialize first column\n  for (int i = 0; i <= n; i++) {\n    dp[i][0] = 1;\n  }\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // Sum of the two options: not selecting and selecting coin i\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[n][amt];\n}\n
coin_change_ii.rs
/* Coin change II: Dynamic programming */\nfn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Initialize dp table\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // Initialize first column\n    for i in 0..=n {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[n][amt]\n}\n
coin_change_ii.c
/* Coin change II: Dynamic programming */\nint coinChangeIIDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Initialize dp table\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // Initialize first column\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[n][amt];\n    // Free memory\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
coin_change_ii.kt
/* Coin change II: Dynamic programming */\nfun coinChangeIIDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Initialize dp table\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // Initialize first column\n    for (i in 0..n) {\n        dp[i][0] = 1\n    }\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.rb
### Coin change II: dynamic programming ###\ndef coin_change_ii_dp(coins, amt)\n  n = coins.length\n  # Initialize dp table\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # Initialize first column\n  (0...(n + 1)).each { |i| dp[i][0] = 1 }\n  # State transition\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[i][a] = dp[i - 1][a]\n      else\n        # Sum of the two options: not selecting and selecting coin i\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n      end\n    end\n  end\n  dp[n][amt]\nend\n
","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3-space-optimization_2","level":3,"title":"3.   Space Optimization","text":"

The space optimization is handled in the same way, just delete the coin dimension:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change II: Space-optimized dynamic programming\"\"\"\n    n = len(coins)\n    # Initialize dp table\n    dp = [0] * (amt + 1)\n    dp[0] = 1\n    # State transition\n    for i in range(1, n + 1):\n        # Traverse in forward order\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            else:\n                # Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n    return dp[amt]\n
coin_change_ii.cpp
/* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // Initialize dp table\n    vector<int> dp(amt + 1, 0);\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.java
/* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.cs
/* Coin change II: Space-optimized dynamic programming */\nint CoinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    // Initialize dp table\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.go
/* Coin change II: Space-optimized dynamic programming */\nfunc coinChangeIIDPComp(coins []int, amt int) int {\n    n := len(coins)\n    // Initialize dp table\n    dp := make([]int, amt+1)\n    dp[0] = 1\n    // State transition\n    for i := 1; i <= n; i++ {\n        // Traverse in forward order\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a-coins[i-1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.swift
/* Coin change II: Space-optimized dynamic programming */\nfunc coinChangeIIDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // Initialize dp table\n    var dp = Array(repeating: 0, count: amt + 1)\n    dp[0] = 1\n    // State transition\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.js
/* Coin change II: Space-optimized dynamic programming */\nfunction coinChangeIIDPComp(coins, amt) {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.ts
/* Coin change II: Space-optimized dynamic programming */\nfunction coinChangeIIDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // Initialize dp table\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // State transition\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.dart
/* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  // Initialize dp table\n  List<int> dp = List.filled(amt + 1, 0);\n  dp[0] = 1;\n  // State transition\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // If exceeds target amount, don't select coin i\n        dp[a] = dp[a];\n      } else {\n        // Sum of the two options: not selecting and selecting coin i\n        dp[a] = dp[a] + dp[a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[amt];\n}\n
coin_change_ii.rs
/* Coin change II: Space-optimized dynamic programming */\nfn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // Initialize dp table\n    let mut dp = vec![0; amt + 1];\n    dp[0] = 1;\n    // State transition\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[amt]\n}\n
coin_change_ii.c
/* Coin change II: Space-optimized dynamic programming */\nint coinChangeIIDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // Initialize dp table\n    int *dp = calloc(amt + 1, sizeof(int));\n    dp[0] = 1;\n    // State transition\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a];\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[amt];\n    // Free memory\n    free(dp);\n    return res;\n}\n
coin_change_ii.kt
/* Coin change II: Space-optimized dynamic programming */\nfun coinChangeIIDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // Initialize dp table\n    val dp = IntArray(amt + 1)\n    dp[0] = 1\n    // State transition\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // If exceeds target amount, don't select coin i\n                dp[a] = dp[a]\n            } else {\n                // Sum of the two options: not selecting and selecting coin i\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.rb
### Coin change II: space-optimized DP ###\ndef coin_change_ii_dp_comp(coins, amt)\n  n = coins.length\n  # Initialize dp table\n  dp = Array.new(amt + 1, 0)\n  dp[0] = 1\n  # State transition\n  for i in 1...(n + 1)\n    # Traverse in forward order\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # If exceeds target amount, don't select coin i\n        dp[a] = dp[a]\n      else\n        # Sum of the two options: not selecting and selecting coin i\n        dp[a] = dp[a] + dp[a - coins[i - 1]]\n      end\n    end\n  end\n  dp[amt]\nend\n
","path":["Chapter 14. Dynamic Programming","14.5   Unbounded Knapsack Problem"],"tags":[]},{"location":"chapter_graph/","level":1,"title":"Chapter 9.   Graph","text":"

Abstract

In the journey of life, we are like nodes, connected by countless invisible edges.

Each encounter and parting leaves a unique mark on this vast network graph.

","path":["Chapter 9. Graph","Chapter 9.   Graph"],"tags":[]},{"location":"chapter_graph/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 9.1   Graph
  • 9.2   Basic Operations on Graphs
  • 9.3   Graph Traversal
  • 9.4   Summary
","path":["Chapter 9. Graph","Chapter 9.   Graph"],"tags":[]},{"location":"chapter_graph/graph/","level":1,"title":"9.1   Graph","text":"

A graph is a nonlinear data structure consisting of vertices and edges. We can abstractly represent a graph \\(G\\) as a set of vertices \\(V\\) and a set of edges \\(E\\). The following example shows a graph containing 5 vertices and 7 edges.

\\[ \\begin{aligned} V & = \\{ 1, 2, 3, 4, 5 \\} \\newline E & = \\{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \\} \\newline G & = \\{ V, E \\} \\newline \\end{aligned} \\]

If we view vertices as nodes and edges as references (pointers) connecting the nodes, we can see graphs as a data structure extended from linked lists. As shown in Figure 9-1, compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex.

Figure 9-1   Relationships among linked lists, trees, and graphs

","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#911-common-types-and-terminology-of-graphs","level":2,"title":"9.1.1   Common Types and Terminology of Graphs","text":"

Graphs can be divided into undirected graphs and directed graphs based on whether edges have direction, as shown in Figure 9-2.

  • In undirected graphs, edges represent a \"bidirectional\" connection between two vertices, such as the \"friend relationship\" on WeChat or QQ.
  • In directed graphs, edges have directionality, meaning edges \\(A \\rightarrow B\\) and \\(A \\leftarrow B\\) are independent of each other, such as the \"follow\" and \"be followed\" relationships on Weibo or TikTok.

Figure 9-2   Directed and undirected graphs

Graphs can be divided into connected graphs and disconnected graphs based on whether all vertices are connected, as shown in Figure 9-3.

  • For connected graphs, starting from any vertex, all other vertices can be reached.
  • For disconnected graphs, starting from a certain vertex, at least one vertex cannot be reached.

Figure 9-3   Connected and disconnected graphs

We can also add a \"weight\" variable to edges, resulting in weighted graphs as shown in Figure 9-4. For example, in mobile games like \"Honor of Kings\", the system calculates the \"intimacy\" between players based on their shared game time, and such intimacy networks can be represented using weighted graphs.

Figure 9-4   Weighted and unweighted graphs

Graph data structures include the following commonly used terms.

  • Adjacency: When two vertices are connected by an edge, these two vertices are said to be \"adjacent\". In Figure 9-4, the adjacent vertices of vertex 1 are vertices 2, 3, and 5.
  • Path: The sequence of edges from vertex A to vertex B is called a \"path\" from A to B. In Figure 9-4, the edge sequence 1-5-2-4 is a path from vertex 1 to vertex 4.
  • Degree: The number of edges a vertex has. For directed graphs, in-degree indicates how many edges point to the vertex, and out-degree indicates how many edges point out from the vertex.
","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#912-representation-of-graphs","level":2,"title":"9.1.2   Representation of Graphs","text":"

Common representations of graphs include \"adjacency matrices\" and \"adjacency lists\". The following uses undirected graphs as examples.

","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#1-adjacency-matrix","level":3,"title":"1.   Adjacency Matrix","text":"

Given a graph with \\(n\\) vertices, an adjacency matrix uses an \\(n \\times n\\) matrix to represent the graph, where each row (column) represents a vertex, and matrix elements represent edges, using \\(1\\) or \\(0\\) to indicate whether an edge exists between two vertices.

As shown in Figure 9-5, let the adjacency matrix be \\(M\\) and the vertex list be \\(V\\). Then matrix element \\(M[i, j] = 1\\) indicates that an edge exists between vertex \\(V[i]\\) and vertex \\(V[j]\\), whereas \\(M[i, j] = 0\\) indicates no edge between the two vertices.

Figure 9-5   Adjacency matrix representation of a graph

Adjacency matrices have the following properties.

  • In simple graphs, vertices cannot connect to themselves, so the elements on the main diagonal of the adjacency matrix are meaningless.
  • For undirected graphs, edges in both directions are equivalent, so the adjacency matrix is symmetric about the main diagonal.
  • Replacing the elements of the adjacency matrix from \\(1\\) and \\(0\\) to weights allows representation of weighted graphs.

When using adjacency matrices to represent graphs, we can directly access matrix elements to obtain edges, resulting in highly efficient addition, deletion, lookup, and modification operations, all with a time complexity of \\(O(1)\\). However, the space complexity of the matrix is \\(O(n^2)\\), which consumes significant memory.

","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#2-adjacency-list","level":3,"title":"2.   Adjacency List","text":"

An adjacency list uses \\(n\\) linked lists to represent a graph, with linked list nodes representing vertices. The \\(i\\)-th linked list corresponds to vertex \\(i\\) and stores all adjacent vertices of that vertex (vertices connected to that vertex). Figure 9-6 shows an example of a graph stored using an adjacency list.

Figure 9-6   Adjacency list representation of a graph

Adjacency lists only store edges that actually exist, and the total number of edges is typically much less than \\(n^2\\), making them more space-efficient. However, finding edges in an adjacency list requires traversing the linked list, so its time efficiency is inferior to that of adjacency matrices.

Observing Figure 9-6, the structure of adjacency lists is very similar to \"chaining\" in hash tables, so we can adopt similar methods to optimize efficiency. For example, when linked lists are long, they can be converted to AVL trees or red-black trees, thereby optimizing time efficiency from \\(O(n)\\) to \\(O(\\log n)\\); linked lists can also be converted to hash tables, thereby reducing time complexity to \\(O(1)\\).

","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph/#913-common-applications-of-graphs","level":2,"title":"9.1.3   Common Applications of Graphs","text":"

As shown in Table 9-1, many real-world systems can be modeled using graphs, and corresponding problems can be reduced to graph computation problems.

Table 9-1   Common graphs in real life

Vertices Edges Graph Computation Problem Social network Users Friend relationships Potential friend recommendation Subway lines Stations Connectivity between stations Shortest route recommendation Solar system Celestial bodies Gravitational forces between celestial bodies Planetary orbit calculation","path":["Chapter 9. Graph","9.1   Graph"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   Basic Operations on Graphs","text":"

Basic operations on graphs can be divided into operations on \"edges\" and operations on \"vertices\". Under the two representation methods of \"adjacency matrix\" and \"adjacency list\", the implementation methods differ.

","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#921-implementation-based-on-adjacency-matrix","level":2,"title":"9.2.1   Implementation Based on Adjacency Matrix","text":"

Given an undirected graph with \\(n\\) vertices, the various operations are implemented as shown in Figure 9-7.

  • Adding or removing an edge: Directly modify the specified edge in the adjacency matrix, using \\(O(1)\\) time. Since it is an undirected graph, both directions of the edge need to be updated simultaneously.
  • Adding a vertex: Add a row and a column at the end of the adjacency matrix and fill them all with \\(0\\)s, using \\(O(n)\\) time.
  • Removing a vertex: Delete a row and a column in the adjacency matrix. The worst case occurs when removing the first row and column, requiring \\((n-1)^2\\) elements to be \"moved up and to the left\", thus using \\(O(n^2)\\) time.
  • Initialization: Pass in \\(n\\) vertices, initialize a vertex list vertices of length \\(n\\), using \\(O(n)\\) time; initialize an adjacency matrix adjMat of size \\(n \\times n\\), using \\(O(n^2)\\) time.
Initialize adjacency matrixAdd an edgeRemove an edgeAdd a vertexRemove a vertex

Figure 9-7   Initialization, adding and removing edges, adding and removing vertices in adjacency matrix

The following is the implementation code for graphs represented using an adjacency matrix:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_matrix.py
class GraphAdjMat:\n    \"\"\"Undirected graph class based on adjacency matrix\"\"\"\n\n    def __init__(self, vertices: list[int], edges: list[list[int]]):\n        \"\"\"Constructor\"\"\"\n        # Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n        self.vertices: list[int] = []\n        # Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n        self.adj_mat: list[list[int]] = []\n        # Add vertices\n        for val in vertices:\n            self.add_vertex(val)\n        # Add edges\n        # Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for e in edges:\n            self.add_edge(e[0], e[1])\n\n    def size(self) -> int:\n        \"\"\"Get the number of vertices\"\"\"\n        return len(self.vertices)\n\n    def add_vertex(self, val: int):\n        \"\"\"Add vertex\"\"\"\n        n = self.size()\n        # Add the value of the new vertex to the vertex list\n        self.vertices.append(val)\n        # Add a row to the adjacency matrix\n        new_row = [0] * n\n        self.adj_mat.append(new_row)\n        # Add a column to the adjacency matrix\n        for row in self.adj_mat:\n            row.append(0)\n\n    def remove_vertex(self, index: int):\n        \"\"\"Remove vertex\"\"\"\n        if index >= self.size():\n            raise IndexError()\n        # Remove the vertex at index from the vertex list\n        self.vertices.pop(index)\n        # Remove the row at index from the adjacency matrix\n        self.adj_mat.pop(index)\n        # Remove the column at index from the adjacency matrix\n        for row in self.adj_mat:\n            row.pop(index)\n\n    def add_edge(self, i: int, j: int):\n        \"\"\"Add edge\"\"\"\n        # Parameters i, j correspond to the vertices element indices\n        # Handle index out of bounds and equality\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        # In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        self.adj_mat[i][j] = 1\n        self.adj_mat[j][i] = 1\n\n    def remove_edge(self, i: int, j: int):\n        \"\"\"Remove edge\"\"\"\n        # Parameters i, j correspond to the vertices element indices\n        # Handle index out of bounds and equality\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        self.adj_mat[i][j] = 0\n        self.adj_mat[j][i] = 0\n\n    def print(self):\n        \"\"\"Print adjacency matrix\"\"\"\n        print(\"Vertex list =\", self.vertices)\n        print(\"Adjacency matrix =\")\n        print_matrix(self.adj_mat)\n
graph_adjacency_matrix.cpp
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vector<int> vertices;       // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    vector<vector<int>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n  public:\n    /* Constructor */\n    GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {\n        // Add vertex\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const vector<int> &edge : edges) {\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int size() const {\n        return vertices.size();\n    }\n\n    /* Add vertex */\n    void addVertex(int val) {\n        int n = size();\n        // Add the value of the new vertex to the vertex list\n        vertices.push_back(val);\n        // Add a row to the adjacency matrix\n        adjMat.emplace_back(vector<int>(n, 0));\n        // Add a column to the adjacency matrix\n        for (vector<int> &row : adjMat) {\n            row.push_back(0);\n        }\n    }\n\n    /* Remove vertex */\n    void removeVertex(int index) {\n        if (index >= size()) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        // Remove the vertex at index from the vertex list\n        vertices.erase(vertices.begin() + index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.erase(adjMat.begin() + index);\n        // Remove the column at index from the adjacency matrix\n        for (vector<int> &row : adjMat) {\n            row.erase(row.begin() + index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    void addEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    void removeEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"Vertex does not exist\");\n        }\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    void print() {\n        cout << \"Vertex list = \";\n        printVector(vertices);\n        cout << \"Adjacency matrix =\" << endl;\n        printVectorMatrix(adjMat);\n    }\n};\n
graph_adjacency_matrix.java
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    List<Integer> vertices; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    List<List<Integer>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = new ArrayList<>();\n        this.adjMat = new ArrayList<>();\n        // Add vertex\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (int[] e : edges) {\n            addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    public int size() {\n        return vertices.size();\n    }\n\n    /* Add vertex */\n    public void addVertex(int val) {\n        int n = size();\n        // Add the value of the new vertex to the vertex list\n        vertices.add(val);\n        // Add a row to the adjacency matrix\n        List<Integer> newRow = new ArrayList<>(n);\n        for (int j = 0; j < n; j++) {\n            newRow.add(0);\n        }\n        adjMat.add(newRow);\n        // Add a column to the adjacency matrix\n        for (List<Integer> row : adjMat) {\n            row.add(0);\n        }\n    }\n\n    /* Remove vertex */\n    public void removeVertex(int index) {\n        if (index >= size())\n            throw new IndexOutOfBoundsException();\n        // Remove the vertex at index from the vertex list\n        vertices.remove(index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.remove(index);\n        // Remove the column at index from the adjacency matrix\n        for (List<Integer> row : adjMat) {\n            row.remove(index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void addEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat.get(i).set(j, 1);\n        adjMat.get(j).set(i, 1);\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void removeEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        adjMat.get(i).set(j, 0);\n        adjMat.get(j).set(i, 0);\n    }\n\n    /* Print adjacency matrix */\n    public void print() {\n        System.out.print(\"Vertex list = \");\n        System.out.println(vertices);\n        System.out.println(\"Adjacency matrix =\");\n        PrintUtil.printMatrix(adjMat);\n    }\n}\n
graph_adjacency_matrix.cs
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    List<int> vertices;     // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    List<List<int>> adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        foreach (int val in vertices) {\n            AddVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        foreach (int[] e in edges) {\n            AddEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int Size() {\n        return vertices.Count;\n    }\n\n    /* Add vertex */\n    public void AddVertex(int val) {\n        int n = Size();\n        // Add the value of the new vertex to the vertex list\n        vertices.Add(val);\n        // Add a row to the adjacency matrix\n        List<int> newRow = new(n);\n        for (int j = 0; j < n; j++) {\n            newRow.Add(0);\n        }\n        adjMat.Add(newRow);\n        // Add a column to the adjacency matrix\n        foreach (List<int> row in adjMat) {\n            row.Add(0);\n        }\n    }\n\n    /* Remove vertex */\n    public void RemoveVertex(int index) {\n        if (index >= Size())\n            throw new IndexOutOfRangeException();\n        // Remove the vertex at index from the vertex list\n        vertices.RemoveAt(index);\n        // Remove the row at index from the adjacency matrix\n        adjMat.RemoveAt(index);\n        // Remove the column at index from the adjacency matrix\n        foreach (List<int> row in adjMat) {\n            row.RemoveAt(index);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void AddEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    public void RemoveEdge(int i, int j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    public void Print() {\n        Console.Write(\"Vertex list = \");\n        PrintUtil.PrintList(vertices);\n        Console.WriteLine(\"Adjacency matrix =\");\n        PrintUtil.PrintMatrix(adjMat);\n    }\n}\n
graph_adjacency_matrix.go
/* Undirected graph class based on adjacency matrix */\ntype graphAdjMat struct {\n    // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    vertices []int\n    // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    adjMat [][]int\n}\n\n/* Constructor */\nfunc newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat {\n    // Add vertex\n    n := len(vertices)\n    adjMat := make([][]int, n)\n    for i := range adjMat {\n        adjMat[i] = make([]int, n)\n    }\n    // Initialize graph\n    g := &graphAdjMat{\n        vertices: vertices,\n        adjMat:   adjMat,\n    }\n    // Add edge\n    // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    for i := range edges {\n        g.addEdge(edges[i][0], edges[i][1])\n    }\n    return g\n}\n\n/* Get the number of vertices */\nfunc (g *graphAdjMat) size() int {\n    return len(g.vertices)\n}\n\n/* Add vertex */\nfunc (g *graphAdjMat) addVertex(val int) {\n    n := g.size()\n    // Add the value of the new vertex to the vertex list\n    g.vertices = append(g.vertices, val)\n    // Add a row to the adjacency matrix\n    newRow := make([]int, n)\n    g.adjMat = append(g.adjMat, newRow)\n    // Add a column to the adjacency matrix\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i], 0)\n    }\n}\n\n/* Remove vertex */\nfunc (g *graphAdjMat) removeVertex(index int) {\n    if index >= g.size() {\n        return\n    }\n    // Remove the vertex at index from the vertex list\n    g.vertices = append(g.vertices[:index], g.vertices[index+1:]...)\n    // Remove the row at index from the adjacency matrix\n    g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...)\n    // Remove the column at index from the adjacency matrix\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...)\n    }\n}\n\n/* Add edge */\n// Parameters i, j correspond to the vertices element indices\nfunc (g *graphAdjMat) addEdge(i, j int) {\n    // Handle index out of bounds and equality\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    g.adjMat[i][j] = 1\n    g.adjMat[j][i] = 1\n}\n\n/* Remove edge */\n// Parameters i, j correspond to the vertices element indices\nfunc (g *graphAdjMat) removeEdge(i, j int) {\n    // Handle index out of bounds and equality\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    g.adjMat[i][j] = 0\n    g.adjMat[j][i] = 0\n}\n\n/* Print adjacency matrix */\nfunc (g *graphAdjMat) print() {\n    fmt.Printf(\"\\tVertex list = %v\\n\", g.vertices)\n    fmt.Printf(\"\\tAdjacency matrix = \\n\")\n    for i := range g.adjMat {\n        fmt.Printf(\"\\t\\t\\t%v\\n\", g.adjMat[i])\n    }\n}\n
graph_adjacency_matrix.swift
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    private var vertices: [Int] // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    private var adjMat: [[Int]] // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    init(vertices: [Int], edges: [[Int]]) {\n        self.vertices = []\n        adjMat = []\n        // Add vertex\n        for val in vertices {\n            addVertex(val: val)\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for e in edges {\n            addEdge(i: e[0], j: e[1])\n        }\n    }\n\n    /* Get the number of vertices */\n    func size() -> Int {\n        vertices.count\n    }\n\n    /* Add vertex */\n    func addVertex(val: Int) {\n        let n = size()\n        // Add the value of the new vertex to the vertex list\n        vertices.append(val)\n        // Add a row to the adjacency matrix\n        let newRow = Array(repeating: 0, count: n)\n        adjMat.append(newRow)\n        // Add a column to the adjacency matrix\n        for i in adjMat.indices {\n            adjMat[i].append(0)\n        }\n    }\n\n    /* Remove vertex */\n    func removeVertex(index: Int) {\n        if index >= size() {\n            fatalError(\"Out of bounds\")\n        }\n        // Remove the vertex at index from the vertex list\n        vertices.remove(at: index)\n        // Remove the row at index from the adjacency matrix\n        adjMat.remove(at: index)\n        // Remove the column at index from the adjacency matrix\n        for i in adjMat.indices {\n            adjMat[i].remove(at: index)\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    func addEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Out of bounds\")\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    func removeEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"Out of bounds\")\n        }\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* Print adjacency matrix */\n    func print() {\n        Swift.print(\"Vertex list = \", terminator: \"\")\n        Swift.print(vertices)\n        Swift.print(\"Adjacency matrix =\")\n        PrintUtil.printMatrix(matrix: adjMat)\n    }\n}\n
graph_adjacency_matrix.js
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vertices; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    adjMat; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    constructor(vertices, edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size() {\n        return this.vertices.length;\n    }\n\n    /* Add vertex */\n    addVertex(val) {\n        const n = this.size();\n        // Add the value of the new vertex to the vertex list\n        this.vertices.push(val);\n        // Add a row to the adjacency matrix\n        const newRow = [];\n        for (let j = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Add a column to the adjacency matrix\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    removeVertex(index) {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Remove the vertex at index from the vertex list\n        this.vertices.splice(index, 1);\n\n        // Remove the row at index from the adjacency matrix\n        this.adjMat.splice(index, 1);\n        // Remove the column at index from the adjacency matrix\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    addEdge(i, j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // In undirected graph, adjacency matrix is symmetric about main diagonal, i.e., satisfies (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    removeEdge(i, j) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    print() {\n        console.log('Vertex list = ', this.vertices);\n        console.log('Adjacency matrix =', this.adjMat);\n    }\n}\n
graph_adjacency_matrix.ts
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n    vertices: number[]; // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    adjMat: number[][]; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    constructor(vertices: number[], edges: number[][]) {\n        this.vertices = [];\n        this.adjMat = [];\n        // Add vertex\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size(): number {\n        return this.vertices.length;\n    }\n\n    /* Add vertex */\n    addVertex(val: number): void {\n        const n: number = this.size();\n        // Add the value of the new vertex to the vertex list\n        this.vertices.push(val);\n        // Add a row to the adjacency matrix\n        const newRow: number[] = [];\n        for (let j: number = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // Add a column to the adjacency matrix\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    removeVertex(index: number): void {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // Remove the vertex at index from the vertex list\n        this.vertices.splice(index, 1);\n\n        // Remove the row at index from the adjacency matrix\n        this.adjMat.splice(index, 1);\n        // Remove the column at index from the adjacency matrix\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    addEdge(i: number, j: number): void {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // In undirected graph, adjacency matrix is symmetric about main diagonal, i.e., satisfies (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    removeEdge(i: number, j: number): void {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    print(): void {\n        console.log('Vertex list = ', this.vertices);\n        console.log('Adjacency matrix =', this.adjMat);\n    }\n}\n
graph_adjacency_matrix.dart
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat {\n  List<int> vertices = []; // Vertex elements, elements represent \"vertex values\", indices represent \"vertex indices\"\n  List<List<int>> adjMat = []; // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n  /* Constructor */\n  GraphAdjMat(List<int> vertices, List<List<int>> edges) {\n    this.vertices = [];\n    this.adjMat = [];\n    // Add vertex\n    for (int val in vertices) {\n      addVertex(val);\n    }\n    // Add edge\n    // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    for (List<int> e in edges) {\n      addEdge(e[0], e[1]);\n    }\n  }\n\n  /* Get the number of vertices */\n  int size() {\n    return vertices.length;\n  }\n\n  /* Add vertex */\n  void addVertex(int val) {\n    int n = size();\n    // Add the value of the new vertex to the vertex list\n    vertices.add(val);\n    // Add a row to the adjacency matrix\n    List<int> newRow = List.filled(n, 0, growable: true);\n    adjMat.add(newRow);\n    // Add a column to the adjacency matrix\n    for (List<int> row in adjMat) {\n      row.add(0);\n    }\n  }\n\n  /* Remove vertex */\n  void removeVertex(int index) {\n    if (index >= size()) {\n      throw IndexError;\n    }\n    // Remove the vertex at index from the vertex list\n    vertices.removeAt(index);\n    // Remove the row at index from the adjacency matrix\n    adjMat.removeAt(index);\n    // Remove the column at index from the adjacency matrix\n    for (List<int> row in adjMat) {\n      row.removeAt(index);\n    }\n  }\n\n  /* Add edge */\n  // Parameters i, j correspond to the vertices element indices\n  void addEdge(int i, int j) {\n    // Handle index out of bounds and equality\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    adjMat[i][j] = 1;\n    adjMat[j][i] = 1;\n  }\n\n  /* Remove edge */\n  // Parameters i, j correspond to the vertices element indices\n  void removeEdge(int i, int j) {\n    // Handle index out of bounds and equality\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    adjMat[i][j] = 0;\n    adjMat[j][i] = 0;\n  }\n\n  /* Print adjacency matrix */\n  void printAdjMat() {\n    print(\"Vertex list = $vertices\");\n    print(\"Adjacency matrix = \");\n    printMatrix(adjMat);\n  }\n}\n
graph_adjacency_matrix.rs
/* Undirected graph type based on adjacency matrix */\npub struct GraphAdjMat {\n    // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    pub vertices: Vec<i32>,\n    // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    pub adj_mat: Vec<Vec<i32>>,\n}\n\nimpl GraphAdjMat {\n    /* Constructor */\n    pub fn new(vertices: Vec<i32>, edges: Vec<[usize; 2]>) -> Self {\n        let mut graph = GraphAdjMat {\n            vertices: vec![],\n            adj_mat: vec![],\n        };\n        // Add vertex\n        for val in vertices {\n            graph.add_vertex(val);\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for edge in edges {\n            graph.add_edge(edge[0], edge[1])\n        }\n\n        graph\n    }\n\n    /* Get the number of vertices */\n    pub fn size(&self) -> usize {\n        self.vertices.len()\n    }\n\n    /* Add vertex */\n    pub fn add_vertex(&mut self, val: i32) {\n        let n = self.size();\n        // Add the value of the new vertex to the vertex list\n        self.vertices.push(val);\n        // Add a row to the adjacency matrix\n        self.adj_mat.push(vec![0; n]);\n        // Add a column to the adjacency matrix\n        for row in self.adj_mat.iter_mut() {\n            row.push(0);\n        }\n    }\n\n    /* Remove vertex */\n    pub fn remove_vertex(&mut self, index: usize) {\n        if index >= self.size() {\n            panic!(\"index error\")\n        }\n        // Remove the vertex at index from the vertex list\n        self.vertices.remove(index);\n        // Remove the row at index from the adjacency matrix\n        self.adj_mat.remove(index);\n        // Remove the column at index from the adjacency matrix\n        for row in self.adj_mat.iter_mut() {\n            row.remove(index);\n        }\n    }\n\n    /* Add edge */\n    pub fn add_edge(&mut self, i: usize, j: usize) {\n        // Parameters i, j correspond to the vertices element indices\n        // Handle index out of bounds and equality\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        self.adj_mat[i][j] = 1;\n        self.adj_mat[j][i] = 1;\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    pub fn remove_edge(&mut self, i: usize, j: usize) {\n        // Parameters i, j correspond to the vertices element indices\n        // Handle index out of bounds and equality\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        self.adj_mat[i][j] = 0;\n        self.adj_mat[j][i] = 0;\n    }\n\n    /* Print adjacency matrix */\n    pub fn print(&self) {\n        println!(\"Vertex list = {:?}\", self.vertices);\n        println!(\"Adjacency matrix =\");\n        println!(\"[\");\n        for row in &self.adj_mat {\n            println!(\"  {:?},\", row);\n        }\n        println!(\"]\")\n    }\n}\n
graph_adjacency_matrix.c
/* Undirected graph structure based on adjacency matrix */\ntypedef struct {\n    int vertices[MAX_SIZE];\n    int adjMat[MAX_SIZE][MAX_SIZE];\n    int size;\n} GraphAdjMat;\n\n/* Constructor */\nGraphAdjMat *newGraphAdjMat() {\n    GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat));\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        for (int j = 0; j < MAX_SIZE; j++) {\n            graph->adjMat[i][j] = 0;\n        }\n    }\n    return graph;\n}\n\n/* Destructor */\nvoid delGraphAdjMat(GraphAdjMat *graph) {\n    free(graph);\n}\n\n/* Add vertex */\nvoid addVertex(GraphAdjMat *graph, int val) {\n    if (graph->size == MAX_SIZE) {\n        fprintf(stderr, \"Graph vertex count has reached maximum\\n\");\n        return;\n    }\n    // Add nth vertex and zero nth row and column\n    int n = graph->size;\n    graph->vertices[n] = val;\n    for (int i = 0; i <= n; i++) {\n        graph->adjMat[n][i] = graph->adjMat[i][n] = 0;\n    }\n    graph->size++;\n}\n\n/* Remove vertex */\nvoid removeVertex(GraphAdjMat *graph, int index) {\n    if (index < 0 || index >= graph->size) {\n        fprintf(stderr, \"Vertex index out of bounds\\n\");\n        return;\n    }\n    // Remove the vertex at index from the vertex list\n    for (int i = index; i < graph->size - 1; i++) {\n        graph->vertices[i] = graph->vertices[i + 1];\n    }\n    // Remove the row at index from the adjacency matrix\n    for (int i = index; i < graph->size - 1; i++) {\n        for (int j = 0; j < graph->size; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i + 1][j];\n        }\n    }\n    // Remove the column at index from the adjacency matrix\n    for (int i = 0; i < graph->size; i++) {\n        for (int j = index; j < graph->size - 1; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i][j + 1];\n        }\n    }\n    graph->size--;\n}\n\n/* Add edge */\n// Parameters i, j correspond to the vertices element indices\nvoid addEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"Edge index out of bounds or equal\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 1;\n    graph->adjMat[j][i] = 1;\n}\n\n/* Remove edge */\n// Parameters i, j correspond to the vertices element indices\nvoid removeEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"Edge index out of bounds or equal\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 0;\n    graph->adjMat[j][i] = 0;\n}\n\n/* Print adjacency matrix */\nvoid printGraphAdjMat(GraphAdjMat *graph) {\n    printf(\"Vertex list = \");\n    printArray(graph->vertices, graph->size);\n    printf(\"Adjacency matrix =\\n\");\n    for (int i = 0; i < graph->size; i++) {\n        printArray(graph->adjMat[i], graph->size);\n    }\n}\n
graph_adjacency_matrix.kt
/* Undirected graph class based on adjacency matrix */\nclass GraphAdjMat(vertices: IntArray, edges: Array<IntArray>) {\n    val vertices = mutableListOf<Int>() // Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    val adjMat = mutableListOf<MutableList<Int>>() // Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n\n    /* Constructor */\n    init {\n        // Add vertex\n        for (vertex in vertices) {\n            addVertex(vertex)\n        }\n        // Add edge\n        // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n        for (edge in edges) {\n            addEdge(edge[0], edge[1])\n        }\n    }\n\n    /* Get the number of vertices */\n    fun size(): Int {\n        return vertices.size\n    }\n\n    /* Add vertex */\n    fun addVertex(_val: Int) {\n        val n = size()\n        // Add the value of the new vertex to the vertex list\n        vertices.add(_val)\n        // Add a row to the adjacency matrix\n        val newRow = mutableListOf<Int>()\n        for (j in 0..<n) {\n            newRow.add(0)\n        }\n        adjMat.add(newRow)\n        // Add a column to the adjacency matrix\n        for (row in adjMat) {\n            row.add(0)\n        }\n    }\n\n    /* Remove vertex */\n    fun removeVertex(index: Int) {\n        if (index >= size())\n            throw IndexOutOfBoundsException()\n        // Remove the vertex at index from the vertex list\n        vertices.removeAt(index)\n        // Remove the row at index from the adjacency matrix\n        adjMat.removeAt(index)\n        // Remove the column at index from the adjacency matrix\n        for (row in adjMat) {\n            row.removeAt(index)\n        }\n    }\n\n    /* Add edge */\n    // Parameters i, j correspond to the vertices element indices\n    fun addEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* Remove edge */\n    // Parameters i, j correspond to the vertices element indices\n    fun removeEdge(i: Int, j: Int) {\n        // Handle index out of bounds and equality\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* Print adjacency matrix */\n    fun print() {\n        print(\"Vertex list = \")\n        println(vertices)\n        println(\"Adjacency matrix =\")\n        printMatrix(adjMat)\n    }\n}\n
graph_adjacency_matrix.rb
### Undirected graph class based on adjacency matrix ###\nclass GraphAdjMat\n  def initialize(vertices, edges)\n    ### Constructor ###\n    # Vertex list, where the element represents the \"vertex value\" and the index represents the \"vertex index\"\n    @vertices = []\n    # Adjacency matrix, where the row and column indices correspond to the \"vertex index\"\n    @adj_mat = []\n    # Add vertex\n    vertices.each { |val| add_vertex(val) }\n    # Add edge\n    # Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices\n    edges.each { |e| add_edge(e[0], e[1]) }\n  end\n\n  ### Get number of vertices ###\n  def size\n    @vertices.length\n  end\n\n  ### Add vertex ###\n  def add_vertex(val)\n    n = size\n    # Add the value of the new vertex to the vertex list\n    @vertices << val\n    # Add a row to the adjacency matrix\n    new_row = Array.new(n, 0)\n    @adj_mat << new_row\n    # Add a column to the adjacency matrix\n    @adj_mat.each { |row| row << 0 }\n  end\n\n  ### Delete vertex ###\n  def remove_vertex(index)\n    raise IndexError if index >= size\n\n    # Remove the vertex at index from the vertex list\n    @vertices.delete_at(index)\n    # Remove the row at index from the adjacency matrix\n    @adj_mat.delete_at(index)\n    # Remove the column at index from the adjacency matrix\n    @adj_mat.each { |row| row.delete_at(index) }\n  end\n\n  ### Add edge ###\n  def add_edge(i, j)\n    # Parameters i, j correspond to the vertices element indices\n    # Handle index out of bounds and equality\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    # In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i)\n    @adj_mat[i][j] = 1\n    @adj_mat[j][i] = 1\n  end\n\n  ### Delete edge ###\n  def remove_edge(i, j)\n    # Parameters i, j correspond to the vertices element indices\n    # Handle index out of bounds and equality\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    @adj_mat[i][j] = 0\n    @adj_mat[j][i] = 0\n  end\n\n  ### Print adjacency matrix ###\n  def __print__\n    puts \"Vertex list = #{@vertices}\"\n    puts 'Adjacency matrix ='\n    print_matrix(@adj_mat)\n  end\nend\n
","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#922-implementation-based-on-adjacency-list","level":2,"title":"9.2.2   Implementation Based on Adjacency List","text":"

Given an undirected graph with a total of \\(n\\) vertices and \\(m\\) edges, the various operations can be implemented as shown in Figure 9-8.

  • Adding an edge: Add the edge at the end of the corresponding vertex's linked list, using \\(O(1)\\) time. Since it is an undirected graph, edges in both directions need to be added simultaneously.
  • Removing an edge: Find and remove the specified edge in the corresponding vertex's linked list, using \\(O(m)\\) time. In an undirected graph, edges in both directions need to be removed simultaneously.
  • Adding a vertex: Add a linked list in the adjacency list and set the new vertex as the head node of the list, using \\(O(1)\\) time.
  • Removing a vertex: Traverse the entire adjacency list and remove all edges containing the specified vertex, using \\(O(n + m)\\) time.
  • Initialization: Create \\(n\\) vertices and \\(2m\\) edges in the adjacency list, using \\(O(n + m)\\) time.
Initialize adjacency listAdd an edgeRemove an edgeAdd a vertexRemove a vertex

Figure 9-8   Initialization, adding and removing edges, adding and removing vertices in adjacency list

The following is the adjacency list code implementation. Compared to Figure 9-8, the actual code has the following differences.

  • For convenience in adding and removing vertices, and to simplify the code, we use lists (dynamic arrays) instead of linked lists.
  • A hash table is used to store the adjacency list, where key is the vertex instance and value is the list (linked list) of adjacent vertices for that vertex.

Additionally, we use the Vertex class to represent vertices in the adjacency list. The reason for this is: if we used list indices to distinguish different vertices as with adjacency matrices, then to delete the vertex at index \\(i\\), we would need to traverse the entire adjacency list and decrement all indices greater than \\(i\\) by \\(1\\), which is very inefficient. However, if each vertex is a unique Vertex instance, deleting a vertex does not require modifying other vertices.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_list.py
class GraphAdjList:\n    \"\"\"Undirected graph class based on adjacency list\"\"\"\n\n    def __init__(self, edges: list[list[Vertex]]):\n        \"\"\"Constructor\"\"\"\n        # Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n        self.adj_list = dict[Vertex, list[Vertex]]()\n        # Add all vertices and edges\n        for edge in edges:\n            self.add_vertex(edge[0])\n            self.add_vertex(edge[1])\n            self.add_edge(edge[0], edge[1])\n\n    def size(self) -> int:\n        \"\"\"Get the number of vertices\"\"\"\n        return len(self.adj_list)\n\n    def add_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"Add edge\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Add edge vet1 - vet2\n        self.adj_list[vet1].append(vet2)\n        self.adj_list[vet2].append(vet1)\n\n    def remove_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"Remove edge\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # Remove edge vet1 - vet2\n        self.adj_list[vet1].remove(vet2)\n        self.adj_list[vet2].remove(vet1)\n\n    def add_vertex(self, vet: Vertex):\n        \"\"\"Add vertex\"\"\"\n        if vet in self.adj_list:\n            return\n        # Add a new linked list in the adjacency list\n        self.adj_list[vet] = []\n\n    def remove_vertex(self, vet: Vertex):\n        \"\"\"Remove vertex\"\"\"\n        if vet not in self.adj_list:\n            raise ValueError()\n        # Remove the linked list corresponding to vertex vet in the adjacency list\n        self.adj_list.pop(vet)\n        # Traverse the linked lists of other vertices and remove all edges containing vet\n        for vertex in self.adj_list:\n            if vet in self.adj_list[vertex]:\n                self.adj_list[vertex].remove(vet)\n\n    def print(self):\n        \"\"\"Print adjacency list\"\"\"\n        print(\"Adjacency list =\")\n        for vertex in self.adj_list:\n            tmp = [v.val for v in self.adj_list[vertex]]\n            print(f\"{vertex.val}: {tmp},\")\n
graph_adjacency_list.cpp
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n  public:\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    unordered_map<Vertex *, vector<Vertex *>> adjList;\n\n    /* Remove specified node from vector */\n    void remove(vector<Vertex *> &vec, Vertex *vet) {\n        for (int i = 0; i < vec.size(); i++) {\n            if (vec[i] == vet) {\n                vec.erase(vec.begin() + i);\n                break;\n            }\n        }\n    }\n\n    /* Constructor */\n    GraphAdjList(const vector<vector<Vertex *>> &edges) {\n        // Add all vertices and edges\n        for (const vector<Vertex *> &edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int size() {\n        return adjList.size();\n    }\n\n    /* Add edge */\n    void addEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"Vertex does not exist\");\n        // Add edge vet1 - vet2\n        adjList[vet1].push_back(vet2);\n        adjList[vet2].push_back(vet1);\n    }\n\n    /* Remove edge */\n    void removeEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"Vertex does not exist\");\n        // Remove edge vet1 - vet2\n        remove(adjList[vet1], vet2);\n        remove(adjList[vet2], vet1);\n    }\n\n    /* Add vertex */\n    void addVertex(Vertex *vet) {\n        if (adjList.count(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList[vet] = vector<Vertex *>();\n    }\n\n    /* Remove vertex */\n    void removeVertex(Vertex *vet) {\n        if (!adjList.count(vet))\n            throw invalid_argument(\"Vertex does not exist\");\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.erase(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (auto &adj : adjList) {\n            remove(adj.second, vet);\n        }\n    }\n\n    /* Print adjacency list */\n    void print() {\n        cout << \"Adjacency list =\" << endl;\n        for (auto &adj : adjList) {\n            const auto &key = adj.first;\n            const auto &vec = adj.second;\n            cout << key->val << \": \";\n            printVector(vetsToVals(vec));\n        }\n    }\n};\n
graph_adjacency_list.java
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    Map<Vertex, List<Vertex>> adjList;\n\n    /* Constructor */\n    public GraphAdjList(Vertex[][] edges) {\n        this.adjList = new HashMap<>();\n        // Add all vertices and edges\n        for (Vertex[] edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    public int size() {\n        return adjList.size();\n    }\n\n    /* Add edge */\n    public void addEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Add edge vet1 - vet2\n        adjList.get(vet1).add(vet2);\n        adjList.get(vet2).add(vet1);\n    }\n\n    /* Remove edge */\n    public void removeEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // Remove edge vet1 - vet2\n        adjList.get(vet1).remove(vet2);\n        adjList.get(vet2).remove(vet1);\n    }\n\n    /* Add vertex */\n    public void addVertex(Vertex vet) {\n        if (adjList.containsKey(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList.put(vet, new ArrayList<>());\n    }\n\n    /* Remove vertex */\n    public void removeVertex(Vertex vet) {\n        if (!adjList.containsKey(vet))\n            throw new IllegalArgumentException();\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.remove(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (List<Vertex> list : adjList.values()) {\n            list.remove(vet);\n        }\n    }\n\n    /* Print adjacency list */\n    public void print() {\n        System.out.println(\"Adjacency list =\");\n        for (Map.Entry<Vertex, List<Vertex>> pair : adjList.entrySet()) {\n            List<Integer> tmp = new ArrayList<>();\n            for (Vertex vertex : pair.getValue())\n                tmp.add(vertex.val);\n            System.out.println(pair.getKey().val + \": \" + tmp + \",\");\n        }\n    }\n}\n
graph_adjacency_list.cs
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    public Dictionary<Vertex, List<Vertex>> adjList;\n\n    /* Constructor */\n    public GraphAdjList(Vertex[][] edges) {\n        adjList = [];\n        // Add all vertices and edges\n        foreach (Vertex[] edge in edges) {\n            AddVertex(edge[0]);\n            AddVertex(edge[1]);\n            AddEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    int Size() {\n        return adjList.Count;\n    }\n\n    /* Add edge */\n    public void AddEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Add edge vet1 - vet2\n        adjList[vet1].Add(vet2);\n        adjList[vet2].Add(vet1);\n    }\n\n    /* Remove edge */\n    public void RemoveEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // Remove edge vet1 - vet2\n        adjList[vet1].Remove(vet2);\n        adjList[vet2].Remove(vet1);\n    }\n\n    /* Add vertex */\n    public void AddVertex(Vertex vet) {\n        if (adjList.ContainsKey(vet))\n            return;\n        // Add a new linked list in the adjacency list\n        adjList.Add(vet, []);\n    }\n\n    /* Remove vertex */\n    public void RemoveVertex(Vertex vet) {\n        if (!adjList.ContainsKey(vet))\n            throw new InvalidOperationException();\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.Remove(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        foreach (List<Vertex> list in adjList.Values) {\n            list.Remove(vet);\n        }\n    }\n\n    /* Print adjacency list */\n    public void Print() {\n        Console.WriteLine(\"Adjacency list =\");\n        foreach (KeyValuePair<Vertex, List<Vertex>> pair in adjList) {\n            List<int> tmp = [];\n            foreach (Vertex vertex in pair.Value)\n                tmp.Add(vertex.val);\n            Console.WriteLine(pair.Key.val + \": [\" + string.Join(\", \", tmp) + \"],\");\n        }\n    }\n}\n
graph_adjacency_list.go
/* Undirected graph class based on adjacency list */\ntype graphAdjList struct {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList map[Vertex][]Vertex\n}\n\n/* Constructor */\nfunc newGraphAdjList(edges [][]Vertex) *graphAdjList {\n    g := &graphAdjList{\n        adjList: make(map[Vertex][]Vertex),\n    }\n    // Add all vertices and edges\n    for _, edge := range edges {\n        g.addVertex(edge[0])\n        g.addVertex(edge[1])\n        g.addEdge(edge[0], edge[1])\n    }\n    return g\n}\n\n/* Get the number of vertices */\nfunc (g *graphAdjList) size() int {\n    return len(g.adjList)\n}\n\n/* Add edge */\nfunc (g *graphAdjList) addEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // Add edge vet1 - vet2, add anonymous struct{},\n    g.adjList[vet1] = append(g.adjList[vet1], vet2)\n    g.adjList[vet2] = append(g.adjList[vet2], vet1)\n}\n\n/* Remove edge */\nfunc (g *graphAdjList) removeEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // Remove edge vet1 - vet2\n    g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2)\n    g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1)\n}\n\n/* Add vertex */\nfunc (g *graphAdjList) addVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if ok {\n        return\n    }\n    // Add a new linked list in the adjacency list\n    g.adjList[vet] = make([]Vertex, 0)\n}\n\n/* Remove vertex */\nfunc (g *graphAdjList) removeVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if !ok {\n        panic(\"error\")\n    }\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    delete(g.adjList, vet)\n    // Traverse the linked lists of other vertices and remove all edges containing vet\n    for v, list := range g.adjList {\n        g.adjList[v] = DeleteSliceElms(list, vet)\n    }\n}\n\n/* Print adjacency list */\nfunc (g *graphAdjList) print() {\n    var builder strings.Builder\n    fmt.Printf(\"Adjacency list = \\n\")\n    for k, v := range g.adjList {\n        builder.WriteString(\"\\t\\t\" + strconv.Itoa(k.Val) + \": \")\n        for _, vet := range v {\n            builder.WriteString(strconv.Itoa(vet.Val) + \" \")\n        }\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
graph_adjacency_list.swift
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    public private(set) var adjList: [Vertex: [Vertex]]\n\n    /* Constructor */\n    public init(edges: [[Vertex]]) {\n        adjList = [:]\n        // Add all vertices and edges\n        for edge in edges {\n            addVertex(vet: edge[0])\n            addVertex(vet: edge[1])\n            addEdge(vet1: edge[0], vet2: edge[1])\n        }\n    }\n\n    /* Get the number of vertices */\n    public func size() -> Int {\n        adjList.count\n    }\n\n    /* Add edge */\n    public func addEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Invalid parameter\")\n        }\n        // Add edge vet1 - vet2\n        adjList[vet1]?.append(vet2)\n        adjList[vet2]?.append(vet1)\n    }\n\n    /* Remove edge */\n    public func removeEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"Invalid parameter\")\n        }\n        // Remove edge vet1 - vet2\n        adjList[vet1]?.removeAll { $0 == vet2 }\n        adjList[vet2]?.removeAll { $0 == vet1 }\n    }\n\n    /* Add vertex */\n    public func addVertex(vet: Vertex) {\n        if adjList[vet] != nil {\n            return\n        }\n        // Add a new linked list in the adjacency list\n        adjList[vet] = []\n    }\n\n    /* Remove vertex */\n    public func removeVertex(vet: Vertex) {\n        if adjList[vet] == nil {\n            fatalError(\"Invalid parameter\")\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.removeValue(forKey: vet)\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for key in adjList.keys {\n            adjList[key]?.removeAll { $0 == vet }\n        }\n    }\n\n    /* Print adjacency list */\n    public func print() {\n        Swift.print(\"Adjacency list =\")\n        for (vertex, list) in adjList {\n            let list = list.map { $0.val }\n            Swift.print(\"\\(vertex.val): \\(list),\")\n        }\n    }\n}\n
graph_adjacency_list.js
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList;\n\n    /* Constructor */\n    constructor(edges) {\n        this.adjList = new Map();\n        // Add all vertices and edges\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size() {\n        return this.adjList.size;\n    }\n\n    /* Add edge */\n    addEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Add edge vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Remove edge */\n    removeEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove edge vet1 - vet2\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* Add vertex */\n    addVertex(vet) {\n        if (this.adjList.has(vet)) return;\n        // Add a new linked list in the adjacency list\n        this.adjList.set(vet, []);\n    }\n\n    /* Remove vertex */\n    removeVertex(vet) {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        this.adjList.delete(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (const set of this.adjList.values()) {\n            const index = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* Print adjacency list */\n    print() {\n        console.log('Adjacency list =');\n        for (const [key, value] of this.adjList) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
graph_adjacency_list.ts
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    adjList: Map<Vertex, Vertex[]>;\n\n    /* Constructor */\n    constructor(edges: Vertex[][]) {\n        this.adjList = new Map();\n        // Add all vertices and edges\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* Get the number of vertices */\n    size(): number {\n        return this.adjList.size;\n    }\n\n    /* Add edge */\n    addEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Add edge vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* Remove edge */\n    removeEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove edge vet1 - vet2\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* Add vertex */\n    addVertex(vet: Vertex): void {\n        if (this.adjList.has(vet)) return;\n        // Add a new linked list in the adjacency list\n        this.adjList.set(vet, []);\n    }\n\n    /* Remove vertex */\n    removeVertex(vet: Vertex): void {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        this.adjList.delete(vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (const set of this.adjList.values()) {\n            const index: number = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* Print adjacency list */\n    print(): void {\n        console.log('Adjacency list =');\n        for (const [key, value] of this.adjList.entries()) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
graph_adjacency_list.dart
/* Undirected graph class based on adjacency list */\nclass GraphAdjList {\n  // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n  Map<Vertex, List<Vertex>> adjList = {};\n\n  /* Constructor */\n  GraphAdjList(List<List<Vertex>> edges) {\n    for (List<Vertex> edge in edges) {\n      addVertex(edge[0]);\n      addVertex(edge[1]);\n      addEdge(edge[0], edge[1]);\n    }\n  }\n\n  /* Get the number of vertices */\n  int size() {\n    return adjList.length;\n  }\n\n  /* Add edge */\n  void addEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Add edge vet1 - vet2\n    adjList[vet1]!.add(vet2);\n    adjList[vet2]!.add(vet1);\n  }\n\n  /* Remove edge */\n  void removeEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // Remove edge vet1 - vet2\n    adjList[vet1]!.remove(vet2);\n    adjList[vet2]!.remove(vet1);\n  }\n\n  /* Add vertex */\n  void addVertex(Vertex vet) {\n    if (adjList.containsKey(vet)) return;\n    // Add a new linked list in the adjacency list\n    adjList[vet] = [];\n  }\n\n  /* Remove vertex */\n  void removeVertex(Vertex vet) {\n    if (!adjList.containsKey(vet)) {\n      throw ArgumentError;\n    }\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    adjList.remove(vet);\n    // Traverse the linked lists of other vertices and remove all edges containing vet\n    adjList.forEach((key, value) {\n      value.remove(vet);\n    });\n  }\n\n  /* Print adjacency list */\n  void printAdjList() {\n    print(\"Adjacency list =\");\n    adjList.forEach((key, value) {\n      List<int> tmp = [];\n      for (Vertex vertex in value) {\n        tmp.add(vertex.val);\n      }\n      print(\"${key.val}: $tmp,\");\n    });\n  }\n}\n
graph_adjacency_list.rs
/* Undirected graph type based on adjacency list */\npub struct GraphAdjList {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    pub adj_list: HashMap<Vertex, Vec<Vertex>>, // maybe HashSet<Vertex> for value part is better?\n}\n\nimpl GraphAdjList {\n    /* Constructor */\n    pub fn new(edges: Vec<[Vertex; 2]>) -> Self {\n        let mut graph = GraphAdjList {\n            adj_list: HashMap::new(),\n        };\n        // Add all vertices and edges\n        for edge in edges {\n            graph.add_vertex(edge[0]);\n            graph.add_vertex(edge[1]);\n            graph.add_edge(edge[0], edge[1]);\n        }\n\n        graph\n    }\n\n    /* Get the number of vertices */\n    #[allow(unused)]\n    pub fn size(&self) -> usize {\n        self.adj_list.len()\n    }\n\n    /* Add edge */\n    pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Add edge vet1 - vet2\n        self.adj_list.entry(vet1).or_default().push(vet2);\n        self.adj_list.entry(vet2).or_default().push(vet1);\n    }\n\n    /* Remove edge */\n    #[allow(unused)]\n    pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // Remove edge vet1 - vet2\n        self.adj_list\n            .entry(vet1)\n            .and_modify(|v| v.retain(|&e| e != vet2));\n        self.adj_list\n            .entry(vet2)\n            .and_modify(|v| v.retain(|&e| e != vet1));\n    }\n\n    /* Add vertex */\n    pub fn add_vertex(&mut self, vet: Vertex) {\n        if self.adj_list.contains_key(&vet) {\n            return;\n        }\n        // Add a new linked list in the adjacency list\n        self.adj_list.insert(vet, vec![]);\n    }\n\n    /* Remove vertex */\n    #[allow(unused)]\n    pub fn remove_vertex(&mut self, vet: Vertex) {\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        self.adj_list.remove(&vet);\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for list in self.adj_list.values_mut() {\n            list.retain(|&v| v != vet);\n        }\n    }\n\n    /* Print adjacency list */\n    pub fn print(&self) {\n        println!(\"Adjacency list =\");\n        for (vertex, list) in &self.adj_list {\n            let list = list.iter().map(|vertex| vertex.val).collect::<Vec<i32>>();\n            println!(\"{}: {:?},\", vertex.val, list);\n        }\n    }\n}\n
graph_adjacency_list.c
/* Node structure */\ntypedef struct AdjListNode {\n    Vertex *vertex;           // Vertex\n    struct AdjListNode *next; // Successor node\n} AdjListNode;\n\n/* Find node corresponding to vertex */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* Add edge helper function */\nvoid addEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode));\n    node->vertex = vet;\n    // Head insertion\n    node->next = head->next;\n    head->next = node;\n}\n\n/* Remove edge helper function */\nvoid removeEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *pre = head;\n    AdjListNode *cur = head->next;\n    // Search for node corresponding to vet in list\n    while (cur != NULL && cur->vertex != vet) {\n        pre = cur;\n        cur = cur->next;\n    }\n    if (cur == NULL)\n        return;\n    // Remove node corresponding to vet from list\n    pre->next = cur->next;\n    // Free memory\n    free(cur);\n}\n\n/* Undirected graph class based on adjacency list */\ntypedef struct {\n    AdjListNode *heads[MAX_SIZE]; // Node array\n    int size;                     // Node count\n} GraphAdjList;\n\n/* Constructor */\nGraphAdjList *newGraphAdjList() {\n    GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList));\n    if (!graph) {\n        return NULL;\n    }\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        graph->heads[i] = NULL;\n    }\n    return graph;\n}\n\n/* Destructor */\nvoid delGraphAdjList(GraphAdjList *graph) {\n    for (int i = 0; i < graph->size; i++) {\n        AdjListNode *cur = graph->heads[i];\n        while (cur != NULL) {\n            AdjListNode *next = cur->next;\n            if (cur != graph->heads[i]) {\n                free(cur);\n            }\n            cur = next;\n        }\n        free(graph->heads[i]->vertex);\n        free(graph->heads[i]);\n    }\n    free(graph);\n}\n\n/* Find node corresponding to vertex */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* Add edge */\nvoid addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL && head1 != head2);\n    // Add edge vet1 - vet2\n    addEdgeHelper(head1, vet2);\n    addEdgeHelper(head2, vet1);\n}\n\n/* Remove edge */\nvoid removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL);\n    // Remove edge vet1 - vet2\n    removeEdgeHelper(head1, head2->vertex);\n    removeEdgeHelper(head2, head1->vertex);\n}\n\n/* Add vertex */\nvoid addVertex(GraphAdjList *graph, Vertex *vet) {\n    assert(graph != NULL && graph->size < MAX_SIZE);\n    AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode));\n    head->vertex = vet;\n    head->next = NULL;\n    // Add a new linked list in the adjacency list\n    graph->heads[graph->size++] = head;\n}\n\n/* Remove vertex */\nvoid removeVertex(GraphAdjList *graph, Vertex *vet) {\n    AdjListNode *node = findNode(graph, vet);\n    assert(node != NULL);\n    // Remove the linked list corresponding to vertex vet in the adjacency list\n    AdjListNode *cur = node, *pre = NULL;\n    while (cur) {\n        pre = cur;\n        cur = cur->next;\n        free(pre);\n    }\n    // Traverse the linked lists of other vertices and remove all edges containing vet\n    for (int i = 0; i < graph->size; i++) {\n        cur = graph->heads[i];\n        pre = NULL;\n        while (cur) {\n            pre = cur;\n            cur = cur->next;\n            if (cur && cur->vertex == vet) {\n                pre->next = cur->next;\n                free(cur);\n                break;\n            }\n        }\n    }\n    // Move vertices after this vertex forward to fill gap\n    int i;\n    for (i = 0; i < graph->size; i++) {\n        if (graph->heads[i] == node)\n            break;\n    }\n    for (int j = i; j < graph->size - 1; j++) {\n        graph->heads[j] = graph->heads[j + 1];\n    }\n    graph->size--;\n    free(vet);\n}\n
graph_adjacency_list.kt
/* Undirected graph class based on adjacency list */\nclass GraphAdjList(edges: Array<Array<Vertex?>>) {\n    // Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    val adjList = HashMap<Vertex, MutableList<Vertex>>()\n\n    /* Constructor */\n    init {\n        // Add all vertices and edges\n        for (edge in edges) {\n            addVertex(edge[0]!!)\n            addVertex(edge[1]!!)\n            addEdge(edge[0]!!, edge[1]!!)\n        }\n    }\n\n    /* Get the number of vertices */\n    fun size(): Int {\n        return adjList.size\n    }\n\n    /* Add edge */\n    fun addEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Add edge vet1 - vet2\n        adjList[vet1]?.add(vet2)\n        adjList[vet2]?.add(vet1)\n    }\n\n    /* Remove edge */\n    fun removeEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // Remove edge vet1 - vet2\n        adjList[vet1]?.remove(vet2)\n        adjList[vet2]?.remove(vet1)\n    }\n\n    /* Add vertex */\n    fun addVertex(vet: Vertex) {\n        if (adjList.containsKey(vet))\n            return\n        // Add a new linked list in the adjacency list\n        adjList[vet] = mutableListOf()\n    }\n\n    /* Remove vertex */\n    fun removeVertex(vet: Vertex) {\n        if (!adjList.containsKey(vet))\n            throw IllegalArgumentException()\n        // Remove the linked list corresponding to vertex vet in the adjacency list\n        adjList.remove(vet)\n        // Traverse the linked lists of other vertices and remove all edges containing vet\n        for (list in adjList.values) {\n            list.remove(vet)\n        }\n    }\n\n    /* Print adjacency list */\n    fun print() {\n        println(\"Adjacency list =\")\n        for (pair in adjList.entries) {\n            val tmp = mutableListOf<Int>()\n            for (vertex in pair.value) {\n                tmp.add(vertex._val)\n            }\n            println(\"${pair.key._val}: $tmp,\")\n        }\n    }\n}\n
graph_adjacency_list.rb
### Undirected graph class based on adjacency list ###\nclass GraphAdjList\n  attr_reader :adj_list\n\n  ### Constructor ###\n  def initialize(edges)\n    # Adjacency list, key: vertex, value: all adjacent vertices of that vertex\n    @adj_list = {}\n    # Add all vertices and edges\n    for edge in edges\n      add_vertex(edge[0])\n      add_vertex(edge[1])\n      add_edge(edge[0], edge[1])\n    end\n  end\n\n  ### Get number of vertices ###\n  def size\n    @adj_list.length\n  end\n\n  ### Add edge ###\n  def add_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    @adj_list[vet1] << vet2\n    @adj_list[vet2] << vet1\n  end\n\n  ### Delete edge ###\n  def remove_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    # Remove edge vet1 - vet2\n    @adj_list[vet1].delete(vet2)\n    @adj_list[vet2].delete(vet1)\n  end\n\n  ### Add vertex ###\n  def add_vertex(vet)\n    return if @adj_list.include?(vet)\n\n    # Add a new linked list in the adjacency list\n    @adj_list[vet] = []\n  end\n\n  ### Delete vertex ###\n  def remove_vertex(vet)\n    raise ArgumentError unless @adj_list.include?(vet)\n\n    # Remove the linked list corresponding to vertex vet in the adjacency list\n    @adj_list.delete(vet)\n    # Traverse the linked lists of other vertices and remove all edges containing vet\n    for vertex in @adj_list\n      @adj_list[vertex.first].delete(vet) if @adj_list[vertex.first].include?(vet)\n    end\n  end\n\n  ### Print adjacency list ###\n  def __print__\n    puts 'Adjacency list ='\n    for vertex in @adj_list\n      tmp = @adj_list[vertex.first].map { |v| v.val }\n      puts \"#{vertex.first.val}: #{tmp},\"\n    end\n  end\nend\n
","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_operations/#923-efficiency-comparison","level":2,"title":"9.2.3   Efficiency Comparison","text":"

Assuming the graph has \\(n\\) vertices and \\(m\\) edges, Table 9-2 compares the time efficiency and space efficiency of adjacency matrices and adjacency lists. Note that the adjacency list (linked list) corresponds to the implementation in this text, while the adjacency list (hash table) refers specifically to the implementation where all linked lists are replaced with hash tables.

Table 9-2   Comparison of adjacency matrix and adjacency list

Adjacency matrix Adjacency list (linked list) Adjacency list (hash table) Determine adjacency \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Add an edge \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Remove an edge \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) Add a vertex \\(O(n)\\) \\(O(1)\\) \\(O(1)\\) Remove a vertex \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n)\\) Memory space usage \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n + m)\\)

Observing Table 9-2, it appears that the adjacency list (hash table) has the best time efficiency and space efficiency. However, in practice, operating on edges in the adjacency matrix is more efficient, requiring only a single array access or assignment operation. Overall, adjacency matrices embody the principle of \"trading space for time\", while adjacency lists embody \"trading time for space\".

","path":["Chapter 9. Graph","9.2   Basic Operations on Graphs"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   Graph Traversal","text":"

Trees represent \"one-to-many\" relationships, while graphs have a higher degree of freedom and can represent any \"many-to-many\" relationships. Therefore, we can view trees as a special case of graphs. Clearly, tree traversal operations are also a special case of graph traversal operations.

Both graphs and trees require the application of search algorithms to implement traversal operations. Graph traversal methods can also be divided into two types: breadth-first traversal and depth-first traversal.

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931-breadth-first-search","level":2,"title":"9.3.1   Breadth-First Search","text":"

Breadth-first search is a near-to-far traversal method that, starting from a certain node, always prioritizes visiting the nearest vertices and expands outward layer by layer. As shown in Figure 9-9, starting from the top-left vertex, first traverse all adjacent vertices of that vertex, then traverse all adjacent vertices of the next vertex, and so on, until all vertices have been visited.

Figure 9-9   Breadth-first search of a graph

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1-algorithm-implementation","level":3,"title":"1.   Algorithm Implementation","text":"

BFS is typically implemented with the help of a queue, as shown in the code below. The queue has a \"first in, first out\" property, which aligns with the BFS idea of \"near to far\".

  1. Add the starting vertex startVet to the queue and begin the loop.
  2. In each iteration of the loop, pop the vertex at the front of the queue and record it as visited, then add all adjacent vertices of that vertex to the back of the queue.
  3. Repeat step 2. until all vertices have been visited.

To prevent revisiting vertices, we use a hash set visited to record which nodes have been visited.

Tip

A hash set can be viewed as a hash table that stores only key without storing value. It can perform addition, deletion, lookup, and modification operations on key in \\(O(1)\\) time complexity. Based on the uniqueness of key, hash sets are typically used for data deduplication and similar scenarios.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_bfs.py
def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Breadth-first traversal\"\"\"\n    # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n    # Vertex traversal sequence\n    res = []\n    # Hash set for recording vertices that have been visited\n    visited = set[Vertex]([start_vet])\n    # Queue used to implement BFS\n    que = deque[Vertex]([start_vet])\n    # Starting from vertex vet, loop until all vertices are visited\n    while len(que) > 0:\n        vet = que.popleft()  # Dequeue the front vertex\n        res.append(vet)  # Record visited vertex\n        # Traverse all adjacent vertices of this vertex\n        for adj_vet in graph.adj_list[vet]:\n            if adj_vet in visited:\n                continue  # Skip vertices that have been visited\n            que.append(adj_vet)  # Only enqueue unvisited vertices\n            visited.add(adj_vet)  # Mark this vertex as visited\n    # Return vertex traversal sequence\n    return res\n
graph_bfs.cpp
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {\n    // Vertex traversal sequence\n    vector<Vertex *> res;\n    // Hash set for recording vertices that have been visited\n    unordered_set<Vertex *> visited = {startVet};\n    // Queue used to implement BFS\n    queue<Vertex *> que;\n    que.push(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.empty()) {\n        Vertex *vet = que.front();\n        que.pop();          // Dequeue the front vertex\n        res.push_back(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (auto adjVet : graph.adjList[vet]) {\n            if (visited.count(adjVet))\n                continue;            // Skip vertices that have been visited\n            que.push(adjVet);        // Only enqueue unvisited vertices\n            visited.emplace(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
graph_bfs.java
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = new ArrayList<>();\n    // Hash set for recording vertices that have been visited\n    Set<Vertex> visited = new HashSet<>();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    Queue<Vertex> que = new LinkedList<>();\n    que.offer(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.isEmpty()) {\n        Vertex vet = que.poll(); // Dequeue the front vertex\n        res.add(vet);            // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (Vertex adjVet : graph.adjList.get(vet)) {\n            if (visited.contains(adjVet))\n                continue;        // Skip vertices that have been visited\n            que.offer(adjVet);   // Only enqueue unvisited vertices\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
graph_bfs.cs
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = [];\n    // Hash set for recording vertices that have been visited\n    HashSet<Vertex> visited = [startVet];\n    // Queue used to implement BFS\n    Queue<Vertex> que = new();\n    que.Enqueue(startVet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.Count > 0) {\n        Vertex vet = que.Dequeue(); // Dequeue the front vertex\n        res.Add(vet);               // Record visited vertex\n        foreach (Vertex adjVet in graph.adjList[vet]) {\n            if (visited.Contains(adjVet)) {\n                continue;          // Skip vertices that have been visited\n            }\n            que.Enqueue(adjVet);   // Only enqueue unvisited vertices\n            visited.Add(adjVet);   // Mark this vertex as visited\n        }\n    }\n\n    // Return vertex traversal sequence\n    return res;\n}\n
graph_bfs.go
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphBFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Vertex traversal sequence\n    res := make([]Vertex, 0)\n    // Hash set for recording vertices that have been visited\n    visited := make(map[Vertex]struct{})\n    visited[startVet] = struct{}{}\n    // Queue used to implement BFS, using slice to simulate queue\n    queue := make([]Vertex, 0)\n    queue = append(queue, startVet)\n    // Starting from vertex vet, loop until all vertices are visited\n    for len(queue) > 0 {\n        // Dequeue the front vertex\n        vet := queue[0]\n        queue = queue[1:]\n        // Record visited vertex\n        res = append(res, vet)\n        // Traverse all adjacent vertices of this vertex\n        for _, adjVet := range g.adjList[vet] {\n            _, isExist := visited[adjVet]\n            // Only enqueue unvisited vertices\n            if !isExist {\n                queue = append(queue, adjVet)\n                visited[adjVet] = struct{}{}\n            }\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
graph_bfs.swift
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Vertex traversal sequence\n    var res: [Vertex] = []\n    // Hash set for recording vertices that have been visited\n    var visited: Set<Vertex> = [startVet]\n    // Queue used to implement BFS\n    var que: [Vertex] = [startVet]\n    // Starting from vertex vet, loop until all vertices are visited\n    while !que.isEmpty {\n        let vet = que.removeFirst() // Dequeue the front vertex\n        res.append(vet) // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for adjVet in graph.adjList[vet] ?? [] {\n            if visited.contains(adjVet) {\n                continue // Skip vertices that have been visited\n            }\n            que.append(adjVet) // Only enqueue unvisited vertices\n            visited.insert(adjVet) // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
graph_bfs.js
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphBFS(graph, startVet) {\n    // Vertex traversal sequence\n    const res = [];\n    // Hash set for recording vertices that have been visited\n    const visited = new Set();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    const que = [startVet];\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.length) {\n        const vet = que.shift(); // Dequeue the front vertex\n        res.push(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Skip vertices that have been visited\n            }\n            que.push(adjVet); // Only enqueue unvisited vertices\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
graph_bfs.ts
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Vertex traversal sequence\n    const res: Vertex[] = [];\n    // Hash set for recording vertices that have been visited\n    const visited: Set<Vertex> = new Set();\n    visited.add(startVet);\n    // Queue used to implement BFS\n    const que = [startVet];\n    // Starting from vertex vet, loop until all vertices are visited\n    while (que.length) {\n        const vet = que.shift(); // Dequeue the front vertex\n        res.push(vet); // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // Skip vertices that have been visited\n            }\n            que.push(adjVet); // Only enqueue unvisited\n            visited.add(adjVet); // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res;\n}\n
graph_bfs.dart
/* Breadth-first traversal */\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n  // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  // Vertex traversal sequence\n  List<Vertex> res = [];\n  // Hash set for recording vertices that have been visited\n  Set<Vertex> visited = {};\n  visited.add(startVet);\n  // Queue used to implement BFS\n  Queue<Vertex> que = Queue();\n  que.add(startVet);\n  // Starting from vertex vet, loop until all vertices are visited\n  while (que.isNotEmpty) {\n    Vertex vet = que.removeFirst(); // Dequeue the front vertex\n    res.add(vet); // Record visited vertex\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex adjVet in graph.adjList[vet]!) {\n      if (visited.contains(adjVet)) {\n        continue; // Skip vertices that have been visited\n      }\n      que.add(adjVet); // Only enqueue unvisited vertices\n      visited.add(adjVet); // Mark this vertex as visited\n    }\n  }\n  // Return vertex traversal sequence\n  return res;\n}\n
graph_bfs.rs
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Vertex traversal sequence\n    let mut res = vec![];\n    // Hash set for recording vertices that have been visited\n    let mut visited = HashSet::new();\n    visited.insert(start_vet);\n    // Queue used to implement BFS\n    let mut que = VecDeque::new();\n    que.push_back(start_vet);\n    // Starting from vertex vet, loop until all vertices are visited\n    while let Some(vet) = que.pop_front() {\n        res.push(vet); // Record visited vertex\n\n        // Traverse all adjacent vertices of this vertex\n        if let Some(adj_vets) = graph.adj_list.get(&vet) {\n            for &adj_vet in adj_vets {\n                if visited.contains(&adj_vet) {\n                    continue; // Skip vertices that have been visited\n                }\n                que.push_back(adj_vet); // Only enqueue unvisited vertices\n                visited.insert(adj_vet); // Mark this vertex as visited\n            }\n        }\n    }\n    // Return vertex traversal sequence\n    res\n}\n
graph_bfs.c
/* Node queue structure */\ntypedef struct {\n    Vertex *vertices[MAX_SIZE];\n    int front, rear, size;\n} Queue;\n\n/* Constructor */\nQueue *newQueue() {\n    Queue *q = (Queue *)malloc(sizeof(Queue));\n    q->front = q->rear = q->size = 0;\n    return q;\n}\n\n/* Check if the queue is empty */\nint isEmpty(Queue *q) {\n    return q->size == 0;\n}\n\n/* Enqueue operation */\nvoid enqueue(Queue *q, Vertex *vet) {\n    q->vertices[q->rear] = vet;\n    q->rear = (q->rear + 1) % MAX_SIZE;\n    q->size++;\n}\n\n/* Dequeue operation */\nVertex *dequeue(Queue *q) {\n    Vertex *vet = q->vertices[q->front];\n    q->front = (q->front + 1) % MAX_SIZE;\n    q->size--;\n    return vet;\n}\n\n/* Check if vertex has been visited */\nint isVisited(Vertex **visited, int size, Vertex *vet) {\n    // Traverse to find node using O(n) time\n    for (int i = 0; i < size; i++) {\n        if (visited[i] == vet)\n            return 1;\n    }\n    return 0;\n}\n\n/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvoid graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) {\n    // Queue used to implement BFS\n    Queue *queue = newQueue();\n    enqueue(queue, startVet);\n    visited[(*visitedSize)++] = startVet;\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!isEmpty(queue)) {\n        Vertex *vet = dequeue(queue); // Dequeue the front vertex\n        res[(*resSize)++] = vet;      // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        AdjListNode *node = findNode(graph, vet);\n        while (node != NULL) {\n            // Skip vertices that have been visited\n            if (!isVisited(visited, *visitedSize, node->vertex)) {\n                enqueue(queue, node->vertex);             // Only enqueue unvisited vertices\n                visited[(*visitedSize)++] = node->vertex; // Mark this vertex as visited\n            }\n            node = node->next;\n        }\n    }\n    // Free memory\n    free(queue);\n}\n
graph_bfs.kt
/* Breadth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {\n    // Vertex traversal sequence\n    val res = mutableListOf<Vertex?>()\n    // Hash set for recording vertices that have been visited\n    val visited = HashSet<Vertex>()\n    visited.add(startVet)\n    // Queue used to implement BFS\n    val que = LinkedList<Vertex>()\n    que.offer(startVet)\n    // Starting from vertex vet, loop until all vertices are visited\n    while (!que.isEmpty()) {\n        val vet = que.poll() // Dequeue the front vertex\n        res.add(vet)         // Record visited vertex\n        // Traverse all adjacent vertices of this vertex\n        for (adjVet in graph.adjList[vet]!!) {\n            if (visited.contains(adjVet))\n                continue        // Skip vertices that have been visited\n            que.offer(adjVet)   // Only enqueue unvisited vertices\n            visited.add(adjVet) // Mark this vertex as visited\n        }\n    }\n    // Return vertex traversal sequence\n    return res\n}\n
graph_bfs.rb
### Breadth-first traversal ###\ndef graph_bfs(graph, start_vet)\n  # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  # Vertex traversal sequence\n  res = []\n  # Hash set for recording vertices that have been visited\n  visited = Set.new([start_vet])\n  # Queue used to implement BFS\n  que = [start_vet]\n  # Starting from vertex vet, loop until all vertices are visited\n  while que.length > 0\n    vet = que.shift # Dequeue the front vertex\n    res << vet # Record visited vertex\n    # Traverse all adjacent vertices of this vertex\n    for adj_vet in graph.adj_list[vet]\n      next if visited.include?(adj_vet) # Skip vertices that have been visited\n      que << adj_vet # Only enqueue unvisited vertices\n      visited.add(adj_vet) # Mark this vertex as visited\n    end\n  end\n  # Return vertex traversal sequence\n  res\nend\n

The code is relatively abstract; it is recommended to refer to Figure 9-10 to deepen understanding.

<1><2><3><4><5><6><7><8><9><10><11>

Figure 9-10   Steps of breadth-first search of a graph

Is the breadth-first traversal sequence unique?

Not unique. Breadth-first search only requires traversing in a \"near to far\" order, and the traversal order of vertices at the same distance can be arbitrarily shuffled. Taking Figure 9-10 as an example, the visit order of vertices \\(1\\) and \\(3\\) can be swapped, as can the visit order of vertices \\(2\\), \\(4\\), and \\(6\\).

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2-complexity-analysis","level":3,"title":"2.   Complexity Analysis","text":"

Time complexity: All vertices will be enqueued and dequeued once, using \\(O(|V|)\\) time; in the process of traversing adjacent vertices, since it is an undirected graph, all edges will be visited \\(2\\) times, using \\(O(2|E|)\\) time; overall using \\(O(|V| + |E|)\\) time.

Space complexity: The list res, hash set visited, and queue que can contain at most \\(|V|\\) vertices, using \\(O(|V|)\\) space.

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#932-depth-first-search","level":2,"title":"9.3.2   Depth-First Search","text":"

Depth-first search is a traversal method that prioritizes going as far as possible, then backtracks when no path remains. As shown in Figure 9-11, starting from the top-left vertex, visit an adjacent vertex of the current vertex, continuing until reaching a dead end, then return and continue going as far as possible before returning again, and so on, until all vertices have been traversed.

Figure 9-11   Depth-first search of a graph

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1-algorithm-implementation_1","level":3,"title":"1.   Algorithm Implementation","text":"

This \"go as far as possible then return\" algorithm paradigm is typically implemented using recursion. Similar to breadth-first search, in depth-first search we also need a hash set visited to record visited vertices and avoid revisiting.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_dfs.py
def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):\n    \"\"\"Depth-first traversal helper function\"\"\"\n    res.append(vet)  # Record visited vertex\n    visited.add(vet)  # Mark this vertex as visited\n    # Traverse all adjacent vertices of this vertex\n    for adjVet in graph.adj_list[vet]:\n        if adjVet in visited:\n            continue  # Skip vertices that have been visited\n        # Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet)\n\ndef graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"Depth-first traversal\"\"\"\n    # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n    # Vertex traversal sequence\n    res = []\n    # Hash set for recording vertices that have been visited\n    visited = set[Vertex]()\n    dfs(graph, visited, res, start_vet)\n    return res\n
graph_dfs.cpp
/* Depth-first traversal helper function */\nvoid dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {\n    res.push_back(vet);   // Record visited vertex\n    visited.emplace(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex *adjVet : graph.adjList[vet]) {\n        if (visited.count(adjVet))\n            continue; // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {\n    // Vertex traversal sequence\n    vector<Vertex *> res;\n    // Hash set for recording vertices that have been visited\n    unordered_set<Vertex *> visited;\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.java
/* Depth-first traversal helper function */\nvoid dfs(GraphAdjList graph, Set<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.add(vet);     // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (Vertex adjVet : graph.adjList.get(vet)) {\n        if (visited.contains(adjVet))\n            continue; // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = new ArrayList<>();\n    // Hash set for recording vertices that have been visited\n    Set<Vertex> visited = new HashSet<>();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.cs
/* Depth-first traversal helper function */\nvoid DFS(GraphAdjList graph, HashSet<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.Add(vet);     // Record visited vertex\n    visited.Add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    foreach (Vertex adjVet in graph.adjList[vet]) {\n        if (visited.Contains(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        DFS(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nList<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {\n    // Vertex traversal sequence\n    List<Vertex> res = [];\n    // Hash set for recording vertices that have been visited\n    HashSet<Vertex> visited = [];\n    DFS(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.go
/* Depth-first traversal helper function */\nfunc dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) {\n    // append operation returns a new reference, must reassign original reference to new slice's reference\n    *res = append(*res, vet)\n    visited[vet] = struct{}{}\n    // Traverse all adjacent vertices of this vertex\n    for _, adjVet := range g.adjList[vet] {\n        _, isExist := visited[adjVet]\n        // Recursively visit adjacent vertices\n        if !isExist {\n            dfs(g, visited, res, adjVet)\n        }\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphDFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // Vertex traversal sequence\n    res := make([]Vertex, 0)\n    // Hash set for recording vertices that have been visited\n    visited := make(map[Vertex]struct{})\n    dfs(g, visited, &res, startVet)\n    // Return vertex traversal sequence\n    return res\n}\n
graph_dfs.swift
/* Depth-first traversal helper function */\nfunc dfs(graph: GraphAdjList, visited: inout Set<Vertex>, res: inout [Vertex], vet: Vertex) {\n    res.append(vet) // Record visited vertex\n    visited.insert(vet) // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for adjVet in graph.adjList[vet] ?? [] {\n        if visited.contains(adjVet) {\n            continue // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph: graph, visited: &visited, res: &res, vet: adjVet)\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunc graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // Vertex traversal sequence\n    var res: [Vertex] = []\n    // Hash set for recording vertices that have been visited\n    var visited: Set<Vertex> = []\n    dfs(graph: graph, visited: &visited, res: &res, vet: startVet)\n    return res\n}\n
graph_dfs.js
/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction dfs(graph, visited, res, vet) {\n    res.push(vet); // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphDFS(graph, startVet) {\n    // Vertex traversal sequence\n    const res = [];\n    // Hash set for recording vertices that have been visited\n    const visited = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.ts
/* Depth-first traversal helper function */\nfunction dfs(\n    graph: GraphAdjList,\n    visited: Set<Vertex>,\n    res: Vertex[],\n    vet: Vertex\n): void {\n    res.push(vet); // Record visited vertex\n    visited.add(vet); // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // Skip vertices that have been visited\n        }\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfunction graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // Vertex traversal sequence\n    const res: Vertex[] = [];\n    // Hash set for recording vertices that have been visited\n    const visited: Set<Vertex> = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.dart
/* Depth-first traversal helper function */\nvoid dfs(\n  GraphAdjList graph,\n  Set<Vertex> visited,\n  List<Vertex> res,\n  Vertex vet,\n) {\n  res.add(vet); // Record visited vertex\n  visited.add(vet); // Mark this vertex as visited\n  // Traverse all adjacent vertices of this vertex\n  for (Vertex adjVet in graph.adjList[vet]!) {\n    if (visited.contains(adjVet)) {\n      continue; // Skip vertices that have been visited\n    }\n    // Recursively visit adjacent vertices\n    dfs(graph, visited, res, adjVet);\n  }\n}\n\n/* Depth-first traversal */\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n  // Vertex traversal sequence\n  List<Vertex> res = [];\n  // Hash set for recording vertices that have been visited\n  Set<Vertex> visited = {};\n  dfs(graph, visited, res, startVet);\n  return res;\n}\n
graph_dfs.rs
/* Depth-first traversal helper function */\nfn dfs(graph: &GraphAdjList, visited: &mut HashSet<Vertex>, res: &mut Vec<Vertex>, vet: Vertex) {\n    res.push(vet); // Record visited vertex\n    visited.insert(vet); // Mark this vertex as visited\n                         // Traverse all adjacent vertices of this vertex\n    if let Some(adj_vets) = graph.adj_list.get(&vet) {\n        for &adj_vet in adj_vets {\n            if visited.contains(&adj_vet) {\n                continue; // Skip vertices that have been visited\n            }\n            // Recursively visit adjacent vertices\n            dfs(graph, visited, res, adj_vet);\n        }\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // Vertex traversal sequence\n    let mut res = vec![];\n    // Hash set for recording vertices that have been visited\n    let mut visited = HashSet::new();\n    dfs(&graph, &mut visited, &mut res, start_vet);\n\n    res\n}\n
graph_dfs.c
/* Check if vertex has been visited */\nint isVisited(Vertex **res, int size, Vertex *vet) {\n    // Traverse to find node using O(n) time\n    for (int i = 0; i < size; i++) {\n        if (res[i] == vet) {\n            return 1;\n        }\n    }\n    return 0;\n}\n\n/* Depth-first traversal helper function */\nvoid dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) {\n    // Record visited vertex\n    res[(*resSize)++] = vet;\n    // Traverse all adjacent vertices of this vertex\n    AdjListNode *node = findNode(graph, vet);\n    while (node != NULL) {\n        // Skip vertices that have been visited\n        if (!isVisited(res, *resSize, node->vertex)) {\n            // Recursively visit adjacent vertices\n            dfs(graph, res, resSize, node->vertex);\n        }\n        node = node->next;\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nvoid graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) {\n    dfs(graph, res, resSize, startVet);\n}\n
graph_dfs.kt
/* Depth-first traversal helper function */\nfun dfs(\n    graph: GraphAdjList,\n    visited: MutableSet<Vertex?>,\n    res: MutableList<Vertex?>,\n    vet: Vertex?\n) {\n    res.add(vet)     // Record visited vertex\n    visited.add(vet) // Mark this vertex as visited\n    // Traverse all adjacent vertices of this vertex\n    for (adjVet in graph.adjList[vet]!!) {\n        if (visited.contains(adjVet))\n            continue  // Skip vertices that have been visited\n        // Recursively visit adjacent vertices\n        dfs(graph, visited, res, adjVet)\n    }\n}\n\n/* Depth-first traversal */\n// Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\nfun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {\n    // Vertex traversal sequence\n    val res = mutableListOf<Vertex?>()\n    // Hash set for recording vertices that have been visited\n    val visited = HashSet<Vertex?>()\n    dfs(graph, visited, res, startVet)\n    return res\n}\n
graph_dfs.rb
### Depth-first traversal helper function ###\ndef dfs(graph, visited, res, vet)\n  res << vet # Record visited vertex\n  visited.add(vet) # Mark this vertex as visited\n  # Traverse all adjacent vertices of this vertex\n  for adj_vet in graph.adj_list[vet]\n    next if visited.include?(adj_vet) # Skip vertices that have been visited\n    # Recursively visit adjacent vertices\n    dfs(graph, visited, res, adj_vet)\n  end\nend\n\n### Depth-first traversal ###\ndef graph_dfs(graph, start_vet)\n  # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex\n  # Vertex traversal sequence\n  res = []\n  # Hash set for recording vertices that have been visited\n  visited = Set.new\n  dfs(graph, visited, res, start_vet)\n  res\nend\n

The algorithm flow of depth-first search is shown in Figure 9-12.

  • Straight dashed lines represent downward recursion, indicating that a new recursive method has been initiated to visit a new vertex.
  • Curved dashed lines represent upward backtracking, indicating that this recursive method has returned to the position where it was initiated.

To deepen understanding, it is recommended to combine Figure 9-12 with the code to mentally simulate (or draw out) the entire DFS process, including when each recursive method is initiated and when it returns.

<1><2><3><4><5><6><7><8><9><10><11>

Figure 9-12   Steps of depth-first search of a graph

Is the depth-first traversal sequence unique?

Similar to breadth-first search, the order of depth-first traversal sequences is also not unique. Given a certain vertex, exploring in any direction first is valid, meaning the order of adjacent vertices can be arbitrarily shuffled, all being depth-first search.

Taking tree traversal as an example, \"root \\(\\rightarrow\\) left \\(\\rightarrow\\) right\", \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\", and \"left \\(\\rightarrow\\) right \\(\\rightarrow\\) root\" correspond to pre-order, in-order, and post-order traversals, respectively. They represent three different traversal priorities, yet all three belong to depth-first search.

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2-complexity-analysis_1","level":3,"title":"2.   Complexity Analysis","text":"

Time complexity: All vertices will be visited \\(1\\) time, using \\(O(|V|)\\) time; all edges will be visited \\(2\\) times, using \\(O(2|E|)\\) time; overall using \\(O(|V| + |E|)\\) time.

Space complexity: The list res and hash set visited can contain at most \\(|V|\\) vertices, and the maximum recursion depth is \\(|V|\\), therefore using \\(O(|V|)\\) space.

","path":["Chapter 9. Graph","9.3   Graph Traversal"],"tags":[]},{"location":"chapter_graph/summary/","level":1,"title":"9.4   Summary","text":"","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_graph/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Graphs consist of vertices and edges and can be represented as a set of vertices and a set of edges.
  • Compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex.
  • Directed graphs have edges with directionality, connected graphs have all vertices reachable from any vertex, and weighted graphs have edges that each contain a weight variable.
  • Adjacency matrices use matrices to represent graphs, where each row (column) represents a vertex, and matrix elements represent edges, using \\(1\\) or \\(0\\) to indicate whether two vertices have an edge or not. Adjacency matrices are highly efficient for addition, deletion, lookup, and modification operations, but consume significant space.
  • Adjacency lists use multiple linked lists to represent graphs, where the \\(i\\)-th linked list corresponds to vertex \\(i\\) and stores all adjacent vertices of that vertex. Adjacency lists are more space-efficient than adjacency matrices, but have lower time efficiency because they require traversing linked lists to find edges.
  • When linked lists in adjacency lists become too long, they can be converted to red-black trees or hash tables, thereby improving lookup efficiency.
  • From an algorithmic perspective, adjacency matrices embody \"trading space for time\", while adjacency lists embody \"trading time for space\".
  • Graphs can be used to model various real-world systems, such as social networks and subway lines.
  • Trees are a special case of graphs, and tree traversal is a special case of graph traversal.
  • Breadth-first search of graphs is a near-to-far, layer-by-layer expansion search method, typically implemented using a queue.
  • Depth-first search of graphs is a search method that prioritizes going as far as possible and backtracks when no path remains, commonly implemented using recursion.
","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Is a path defined as a sequence of vertices or a sequence of edges?

The definitions in different language versions of Wikipedia are inconsistent: the English version states \"a path is a sequence of edges\", while the Chinese version states \"a path is a sequence of vertices\". The following is the original English text: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

In this text, a path is viewed as a sequence of edges, not a sequence of vertices. This is because there may be multiple edges connecting two vertices, in which case each edge corresponds to a path.

Q: In a disconnected graph, will there be unreachable vertices?

In a disconnected graph, starting from a certain vertex, at least one vertex cannot be reached. Traversing a disconnected graph requires setting multiple starting points to traverse all connected components of the graph.

Q: In an adjacency list, is there a requirement for the order of \"all vertices connected to that vertex\"?

It can be in any order. However, in practical applications, it may be necessary to sort according to specified rules, such as the order in which vertices were added, or the order of vertex values, which helps quickly find vertices \"with certain extreme values\".

","path":["Chapter 9. Graph","9.4   Summary"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"Chapter 15.   Greedy","text":"

Abstract

Sunflowers turn toward the sun, constantly pursuing the maximum potential for their own growth.

Through rounds of simple choices, greedy strategies gradually lead to the best answer.

","path":["Chapter 15. Greedy","Chapter 15.   Greedy"],"tags":[]},{"location":"chapter_greedy/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 15.1   Greedy Algorithm
  • 15.2   Fractional Knapsack Problem
  • 15.3   Maximum Capacity Problem
  • 15.4   Maximum Product Cutting Problem
  • 15.5   Summary
","path":["Chapter 15. Greedy","Chapter 15.   Greedy"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   Fractional Knapsack Problem","text":"

Question

Given \\(n\\) items, where the weight of the \\(i\\)-th item is \\(wgt[i-1]\\) and its value is \\(val[i-1]\\), and a knapsack with capacity \\(cap\\). Each item can be selected only once, but a portion of an item can be selected, with the value calculated based on the proportion of weight selected, what is the maximum value of items in the knapsack under the limited capacity? An example is shown in Figure 15-3.

Figure 15-3   Example data for the fractional knapsack problem

The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, with states including the current item \\(i\\) and capacity \\(c\\), and the goal being to maximize value under the limited knapsack capacity.

The difference is that this problem allows selecting only a portion of an item. As shown in Figure 15-4, we can arbitrarily split items and calculate the corresponding value based on the weight proportion.

  1. For item \\(i\\), its value per unit weight is \\(val[i-1] / wgt[i-1]\\), referred to as unit value.
  2. Suppose we put a portion of item \\(i\\) with weight \\(w\\) into the knapsack, then the value added to the knapsack is \\(w \\times val[i-1] / wgt[i-1]\\).

Figure 15-4   Value of items per unit weight

","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#1-greedy-strategy-determination","level":3,"title":"1.   Greedy Strategy Determination","text":"

Maximizing the total value of items in the knapsack is essentially maximizing the value per unit weight of items. From this, we can derive the greedy strategy shown in Figure 15-5.

  1. Sort items by unit value from high to low.
  2. Iterate through all items, greedily selecting the item with the highest unit value in each round.
  3. If the remaining knapsack capacity is insufficient, use a portion of the current item to fill the knapsack.

Figure 15-5   Greedy strategy for the fractional knapsack problem

","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

We created an Item class to facilitate sorting items by unit value. We loop to make greedy selections, breaking when the knapsack is full and returning the solution:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fractional_knapsack.py
class Item:\n    \"\"\"Item\"\"\"\n\n    def __init__(self, w: int, v: int):\n        self.w = w  # Item weight\n        self.v = v  # Item value\n\ndef fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"Fractional knapsack: Greedy algorithm\"\"\"\n    # Create item list with two attributes: weight, value\n    items = [Item(w, v) for w, v in zip(wgt, val)]\n    # Sort by unit value item.v / item.w from high to low\n    items.sort(key=lambda item: item.v / item.w, reverse=True)\n    # Loop for greedy selection\n    res = 0\n    for item in items:\n        if item.w <= cap:\n            # If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v\n            cap -= item.w\n        else:\n            # If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap\n            # No remaining capacity, so break out of the loop\n            break\n    return res\n
fractional_knapsack.cpp
/* Item */\nclass Item {\n  public:\n    int w; // Item weight\n    int v; // Item value\n\n    Item(int w, int v) : w(w), v(v) {\n    }\n};\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {\n    // Create item list with two attributes: weight, value\n    vector<Item> items;\n    for (int i = 0; i < wgt.size(); i++) {\n        items.push_back(Item(wgt[i], val[i]));\n    }\n    // Sort by unit value item.v / item.w from high to low\n    sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });\n    // Loop for greedy selection\n    double res = 0;\n    for (auto &item : items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double)item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.java
/* Item */\nclass Item {\n    int w; // Item weight\n    int v; // Item value\n\n    public Item(int w, int v) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Create item list with two attributes: weight, value\n    Item[] items = new Item[wgt.length];\n    for (int i = 0; i < wgt.length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // Sort by unit value item.v / item.w from high to low\n    Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));\n    // Loop for greedy selection\n    double res = 0;\n    for (Item item : items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double) item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.cs
/* Item */\nclass Item(int w, int v) {\n    public int w = w; // Item weight\n    public int v = v; // Item value\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble FractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // Create item list with two attributes: weight, value\n    Item[] items = new Item[wgt.Length];\n    for (int i = 0; i < wgt.Length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // Sort by unit value item.v / item.w from high to low\n    Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));\n    // Loop for greedy selection\n    double res = 0;\n    foreach (Item item in items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (double)item.v / item.w * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.go
/* Item */\ntype Item struct {\n    w int // Item weight\n    v int // Item value\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunc fractionalKnapsack(wgt []int, val []int, cap int) float64 {\n    // Create item list with two attributes: weight, value\n    items := make([]Item, len(wgt))\n    for i := 0; i < len(wgt); i++ {\n        items[i] = Item{wgt[i], val[i]}\n    }\n    // Sort by unit value item.v / item.w from high to low\n    sort.Slice(items, func(i, j int) bool {\n        return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)\n    })\n    // Loop for greedy selection\n    res := 0.0\n    for _, item := range items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += float64(item.v)\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += float64(item.v) / float64(item.w) * float64(cap)\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.swift
/* Item */\nclass Item {\n    var w: Int // Item weight\n    var v: Int // Item value\n\n    init(w: Int, v: Int) {\n        self.w = w\n        self.v = v\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunc fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {\n    // Create item list with two attributes: weight, value\n    var items = zip(wgt, val).map { Item(w: $0, v: $1) }\n    // Sort by unit value item.v / item.w from high to low\n    items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }\n    // Loop for greedy selection\n    var res = 0.0\n    var cap = cap\n    for item in items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += Double(item.v)\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += Double(item.v) / Double(item.w) * Double(cap)\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.js
/* Item */\nclass Item {\n    constructor(w, v) {\n        this.w = w; // Item weight\n        this.v = v; // Item value\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunction fractionalKnapsack(wgt, val, cap) {\n    // Create item list with two attributes: weight, value\n    const items = wgt.map((w, i) => new Item(w, val[i]));\n    // Sort by unit value item.v / item.w from high to low\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Loop for greedy selection\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.ts
/* Item */\nclass Item {\n    w: number; // Item weight\n    v: number; // Item value\n\n    constructor(w: number, v: number) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfunction fractionalKnapsack(wgt: number[], val: number[], cap: number): number {\n    // Create item list with two attributes: weight, value\n    const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));\n    // Sort by unit value item.v / item.w from high to low\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // Loop for greedy selection\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (item.v / item.w) * cap;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.dart
/* Item */\nclass Item {\n  int w; // Item weight\n  int v; // Item value\n\n  Item(this.w, this.v);\n}\n\n/* Fractional knapsack: Greedy algorithm */\ndouble fractionalKnapsack(List<int> wgt, List<int> val, int cap) {\n  // Create item list with two attributes: weight, value\n  List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));\n  // Sort by unit value item.v / item.w from high to low\n  items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));\n  // Loop for greedy selection\n  double res = 0;\n  for (Item item in items) {\n    if (item.w <= cap) {\n      // If remaining capacity is sufficient, put the entire current item into the knapsack\n      res += item.v;\n      cap -= item.w;\n    } else {\n      // If remaining capacity is insufficient, put part of the current item into the knapsack\n      res += item.v / item.w * cap;\n      // No remaining capacity, so break out of the loop\n      break;\n    }\n  }\n  return res;\n}\n
fractional_knapsack.rs
/* Item */\nstruct Item {\n    w: i32, // Item weight\n    v: i32, // Item value\n}\n\nimpl Item {\n    fn new(w: i32, v: i32) -> Self {\n        Self { w, v }\n    }\n}\n\n/* Fractional knapsack: Greedy algorithm */\nfn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {\n    // Create item list with two attributes: weight, value\n    let mut items = wgt\n        .iter()\n        .zip(val.iter())\n        .map(|(&w, &v)| Item::new(w, v))\n        .collect::<Vec<Item>>();\n    // Sort by unit value item.v / item.w from high to low\n    items.sort_by(|a, b| {\n        (b.v as f64 / b.w as f64)\n            .partial_cmp(&(a.v as f64 / a.w as f64))\n            .unwrap()\n    });\n    // Loop for greedy selection\n    let mut res = 0.0;\n    for item in &items {\n        if item.w <= cap {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v as f64;\n            cap -= item.w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += item.v as f64 / item.w as f64 * cap as f64;\n            // No remaining capacity, so break out of the loop\n            break;\n        }\n    }\n    res\n}\n
fractional_knapsack.c
/* Item */\ntypedef struct {\n    int w; // Item weight\n    int v; // Item value\n} Item;\n\n/* Fractional knapsack: Greedy algorithm */\nfloat fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {\n    // Create item list with two attributes: weight, value\n    Item *items = malloc(sizeof(Item) * itemCount);\n    for (int i = 0; i < itemCount; i++) {\n        items[i] = (Item){.w = wgt[i], .v = val[i]};\n    }\n    // Sort by unit value item.v / item.w from high to low\n    qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);\n    // Loop for greedy selection\n    float res = 0.0;\n    for (int i = 0; i < itemCount; i++) {\n        if (items[i].w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += items[i].v;\n            cap -= items[i].w;\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += (float)cap / items[i].w * items[i].v;\n            cap = 0;\n            break;\n        }\n    }\n    free(items);\n    return res;\n}\n
fractional_knapsack.kt
/* Item */\nclass Item(\n    val w: Int, // Item\n    val v: Int  // Item value\n)\n\n/* Fractional knapsack: Greedy algorithm */\nfun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {\n    // Create item list with two attributes: weight, value\n    var cap = c\n    val items = arrayOfNulls<Item>(wgt.size)\n    for (i in wgt.indices) {\n        items[i] = Item(wgt[i], _val[i])\n    }\n    // Sort by unit value item.v / item.w from high to low\n    items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }\n    // Loop for greedy selection\n    var res = 0.0\n    for (item in items) {\n        if (item!!.w <= cap) {\n            // If remaining capacity is sufficient, put the entire current item into the knapsack\n            res += item.v\n            cap -= item.w\n        } else {\n            // If remaining capacity is insufficient, put part of the current item into the knapsack\n            res += item.v.toDouble() / item.w * cap\n            // No remaining capacity, so break out of the loop\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.rb
### Item ###\nclass Item\n  attr_accessor :w # Item weight\n  attr_accessor :v # Item value\n\n  def initialize(w, v)\n    @w = w\n    @v = v\n  end\nend\n\n### Fractional knapsack: greedy ###\ndef fractional_knapsack(wgt, val, cap)\n  # Create item list with two attributes: weight, value\n  items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }\n  # Sort by unit value item.v / item.w from high to low\n  items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }\n  # Loop for greedy selection\n  res = 0\n  for item in items\n    if item.w <= cap\n      # If remaining capacity is sufficient, put the entire current item into the knapsack\n      res += item.v\n      cap -= item.w\n    else\n      # If remaining capacity is insufficient, put part of the current item into the knapsack\n      res += (item.v.to_f / item.w) * cap\n      # No remaining capacity, so break out of the loop\n      break\n    end\n  end\n  res\nend\n

The time complexity of built-in sorting algorithms is usually \\(O(\\log n)\\), and the space complexity is usually \\(O(\\log n)\\) or \\(O(n)\\), depending on the specific implementation of the programming language.

Apart from sorting, in the worst case the entire item list needs to be traversed, therefore the time complexity is \\(O(n)\\), where \\(n\\) is the number of items.

Since an Item object list is initialized, the space complexity is \\(O(n)\\).

","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

Using proof by contradiction. Suppose item \\(x\\) has the highest unit value, and some algorithm yields a maximum value of res, but this solution does not include item \\(x\\).

Now remove a unit weight of any item from the knapsack and replace it with a unit weight of item \\(x\\). Since item \\(x\\) has the highest unit value, the total value after replacement will definitely be greater than res. This contradicts the assumption that res is the optimal solution, proving that the optimal solution must include item \\(x\\).

For other items in this solution, we can also construct the above contradiction. In summary, items with greater unit value are always better choices, which proves that the greedy strategy is effective.

As shown in Figure 15-6, if we view item weight and item unit value as the horizontal and vertical axes of a two-dimensional chart respectively, then the fractional knapsack problem can be transformed into \"finding the maximum area enclosed within a limited horizontal axis range\". This analogy can help us understand the effectiveness of the greedy strategy from a geometric perspective.

Figure 15-6   Geometric representation of the fractional knapsack problem

","path":["Chapter 15. Greedy","15.2   Fractional Knapsack Problem"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/","level":1,"title":"15.1   Greedy Algorithm","text":"

Greedy algorithm is a common algorithm for solving optimization problems. Its basic idea is to make the seemingly best choice at each decision stage of the problem, that is, to greedily make locally optimal decisions in hopes of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely applied in many practical problems.

Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as both relying on the optimal substructure property, but they work differently.

  • Dynamic programming considers all previous decisions when making the current decision, and uses solutions to past subproblems to construct the solution to the current subproblem.
  • Greedy algorithms do not consider past decisions, but instead make greedy choices moving forward, continually reducing the problem size until the problem is solved.

We will first understand how greedy algorithms work through the example problem \"coin change\". This problem has already been introduced in the \"Complete Knapsack Problem\" chapter, so I believe you are not unfamiliar with it.

Question

Given \\(n\\) types of coins, where the denomination of the \\(i\\)-th type of coin is \\(coins[i - 1]\\), and the target amount is \\(amt\\), with each type of coin available for repeated selection, what is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return \\(-1\\).

The greedy strategy adopted for this problem is shown in Figure 15-1. Given a target amount, we greedily select the coin that is not greater than and closest to it, and continuously repeat this step until the target amount is reached.

Figure 15-1   Greedy strategy for coin change

The implementation code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_greedy.py
def coin_change_greedy(coins: list[int], amt: int) -> int:\n    \"\"\"Coin change: Greedy algorithm\"\"\"\n    # Assume coins list is sorted\n    i = len(coins) - 1\n    count = 0\n    # Loop to make greedy choices until no remaining amount\n    while amt > 0:\n        # Find the coin that is less than and closest to the remaining amount\n        while i > 0 and coins[i] > amt:\n            i -= 1\n        # Choose coins[i]\n        amt -= coins[i]\n        count += 1\n    # If no feasible solution is found, return -1\n    return count if amt == 0 else -1\n
coin_change_greedy.cpp
/* Coin change: Greedy algorithm */\nint coinChangeGreedy(vector<int> &coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.size() - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.java
/* Coin change: Greedy algorithm */\nint coinChangeGreedy(int[] coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.length - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.cs
/* Coin change: Greedy algorithm */\nint CoinChangeGreedy(int[] coins, int amt) {\n    // Assume coins list is sorted\n    int i = coins.Length - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.go
/* Coin change: Greedy algorithm */\nfunc coinChangeGreedy(coins []int, amt int) int {\n    // Assume coins list is sorted\n    i := len(coins) - 1\n    count := 0\n    // Loop to make greedy choices until no remaining amount\n    for amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        for i > 0 && coins[i] > amt {\n            i--\n        }\n        // Choose coins[i]\n        amt -= coins[i]\n        count++\n    }\n    // If no feasible solution is found, return -1\n    if amt != 0 {\n        return -1\n    }\n    return count\n}\n
coin_change_greedy.swift
/* Coin change: Greedy algorithm */\nfunc coinChangeGreedy(coins: [Int], amt: Int) -> Int {\n    // Assume coins list is sorted\n    var i = coins.count - 1\n    var count = 0\n    var amt = amt\n    // Loop to make greedy choices until no remaining amount\n    while amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        while i > 0 && coins[i] > amt {\n            i -= 1\n        }\n        // Choose coins[i]\n        amt -= coins[i]\n        count += 1\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1\n}\n
coin_change_greedy.js
/* Coin change: Greedy algorithm */\nfunction coinChangeGreedy(coins, amt) {\n    // Assume coins array is sorted\n    let i = coins.length - 1;\n    let count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt === 0 ? count : -1;\n}\n
coin_change_greedy.ts
/* Coin change: Greedy algorithm */\nfunction coinChangeGreedy(coins: number[], amt: number): number {\n    // Assume coins array is sorted\n    let i = coins.length - 1;\n    let count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt === 0 ? count : -1;\n}\n
coin_change_greedy.dart
/* Coin change: Greedy algorithm */\nint coinChangeGreedy(List<int> coins, int amt) {\n  // Assume coins list is sorted\n  int i = coins.length - 1;\n  int count = 0;\n  // Loop to make greedy choices until no remaining amount\n  while (amt > 0) {\n    // Find the coin that is less than and closest to the remaining amount\n    while (i > 0 && coins[i] > amt) {\n      i--;\n    }\n    // Choose coins[i]\n    amt -= coins[i];\n    count++;\n  }\n  // If no feasible solution is found, return -1\n  return amt == 0 ? count : -1;\n}\n
coin_change_greedy.rs
/* Coin change: Greedy algorithm */\nfn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {\n    // Assume coins list is sorted\n    let mut i = coins.len() - 1;\n    let mut count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while amt > 0 {\n        // Find the coin that is less than and closest to the remaining amount\n        while i > 0 && coins[i] > amt {\n            i -= 1;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count += 1;\n    }\n    // If no feasible solution is found, return -1\n    if amt == 0 {\n        count\n    } else {\n        -1\n    }\n}\n
coin_change_greedy.c
/* Coin change: Greedy algorithm */\nint coinChangeGreedy(int *coins, int size, int amt) {\n    // Assume coins list is sorted\n    int i = size - 1;\n    int count = 0;\n    // Loop to make greedy choices until no remaining amount\n    while (amt > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // Choose coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // If no feasible solution is found, return -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.kt
/* Coin change: Greedy algorithm */\nfun coinChangeGreedy(coins: IntArray, amt: Int): Int {\n    // Assume coins list is sorted\n    var am = amt\n    var i = coins.size - 1\n    var count = 0\n    // Loop to make greedy choices until no remaining amount\n    while (am > 0) {\n        // Find the coin that is less than and closest to the remaining amount\n        while (i > 0 && coins[i] > am) {\n            i--\n        }\n        // Choose coins[i]\n        am -= coins[i]\n        count++\n    }\n    // If no feasible solution is found, return -1\n    return if (am == 0) count else -1\n}\n
coin_change_greedy.rb
### Coin change: greedy ###\ndef coin_change_greedy(coins, amt)\n  # Assume coins list is sorted\n  i = coins.length - 1\n  count = 0\n  # Loop to make greedy choices until no remaining amount\n  while amt > 0\n    # Find the coin that is less than and closest to the remaining amount\n    while i > 0 && coins[i] > amt\n      i -= 1\n    end\n    # Choose coins[i]\n    amt -= coins[i]\n    count += 1\n  end\n  # Return -1 if no solution found\n  amt == 0 ? count : -1\nend\n

You might exclaim: So clean! The greedy algorithm solves the coin change problem in about ten lines of code.

","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1511-advantages-and-limitations-of-greedy-algorithms","level":2,"title":"15.1.1   Advantages and Limitations of Greedy Algorithms","text":"

Greedy algorithms are not only straightforward and simple to implement, but are also usually very efficient. In the code above, if the smallest coin denomination is \\(\\min(coins)\\), the greedy choice loops at most \\(amt / \\min(coins)\\) times, giving a time complexity of \\(O(amt / \\min(coins))\\). This is an order of magnitude smaller than the time complexity of the dynamic programming solution \\(O(n \\times amt)\\).

However, for certain coin denomination combinations, greedy algorithms cannot find the optimal solution. Figure 15-2 provides two examples.

  • Positive example \\(coins = [1, 5, 10, 20, 50, 100]\\): With this coin combination, given any \\(amt\\), the greedy algorithm can find the optimal solution.
  • Negative example \\(coins = [1, 20, 50]\\): Suppose \\(amt = 60\\), the greedy algorithm can only find the combination \\(50 + 1 \\times 10\\), totaling \\(11\\) coins, but dynamic programming can find the optimal solution \\(20 + 20 + 20\\), requiring only \\(3\\) coins.
  • Negative example \\(coins = [1, 49, 50]\\): Suppose \\(amt = 98\\), the greedy algorithm can only find the combination \\(50 + 1 \\times 48\\), totaling \\(49\\) coins, but dynamic programming can find the optimal solution \\(49 + 49\\), requiring only \\(2\\) coins.

Figure 15-2   Examples where greedy algorithms cannot find the optimal solution

In other words, for the coin change problem, greedy algorithms cannot guarantee finding the global optimal solution, and may even find very poor solutions. It is better suited for solving with dynamic programming.

Generally, the applicability of greedy algorithms falls into the following two situations.

  1. Can guarantee finding the optimal solution: In this situation, greedy algorithms are often the best choice, because they tend to be more efficient than backtracking and dynamic programming.
  2. Can find an approximate optimal solution: Greedy algorithms are also applicable in this situation. For many complex problems, finding the global optimal solution is very difficult, and being able to find a suboptimal solution with high efficiency is also very good.
","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512-characteristics-of-greedy-algorithms","level":2,"title":"15.1.2   Characteristics of Greedy Algorithms","text":"

So the question arises: what kind of problems are suitable for solving with greedy algorithms? Or in other words, under what conditions can greedy algorithms guarantee finding the optimal solution?

Compared to dynamic programming, the conditions for using greedy algorithms are stricter, mainly focusing on two properties of the problem.

  • Greedy choice property: Only when locally optimal choices can always lead to a globally optimal solution can greedy algorithms guarantee obtaining the optimal solution.
  • Optimal substructure: The optimal solution to the original problem contains the optimal solutions to subproblems.

Optimal substructure has already been introduced in the \"Dynamic Programming\" chapter, so we won't elaborate on it here. It's worth noting that the optimal substructure of some problems is not obvious, but they can still be solved using greedy algorithms.

We mainly explore methods for determining the greedy choice property. Although its description seems relatively simple, in practice, for many problems, proving the greedy choice property is not easy.

For example, in the coin change problem, although we can easily provide counterexamples to disprove the greedy choice property, proving it is quite difficult. If asked: what conditions must a coin combination satisfy to be solvable using a greedy algorithm? We often can only rely on intuition or examples to give an ambiguous answer, and find it difficult to provide a rigorous mathematical proof.

Quote

There is a paper that presents an algorithm with \\(O(n^3)\\) time complexity for determining whether a coin combination can use a greedy algorithm to find the optimal solution for any amount.

Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.

","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1513-steps-for-solving-problems-with-greedy-algorithms","level":2,"title":"15.1.3   Steps for Solving Problems with Greedy Algorithms","text":"

The problem-solving process for greedy problems can generally be divided into the following three steps.

  1. Problem analysis: Sort out and understand the problem characteristics, including state definition, optimization objectives, and constraints, etc. This step is also involved in backtracking and dynamic programming.
  2. Determine the greedy strategy: Determine how to make greedy choices at each step. This strategy should be able to reduce the problem size at each step, ultimately solving the entire problem.
  3. Correctness proof: It is usually necessary to prove that the problem has both greedy choice property and optimal substructure. This step may require mathematical proofs, such as mathematical induction or proof by contradiction.

Determining the greedy strategy is the core step in solving the problem, but it may not be easy to implement, mainly for the following reasons.

  • Greedy strategies differ greatly between different problems. For many problems, the greedy strategy is relatively straightforward, and we can derive it through some general thinking and attempts. However, for some complex problems, the greedy strategy may be very elusive, which really tests one's problem-solving experience and algorithmic ability.
  • Some greedy strategies are highly misleading. When we confidently design a greedy strategy, write the solution code and submit it for testing, we may find that some test cases cannot pass. This is because the designed greedy strategy is only \"partially correct\", as exemplified by the coin change problem discussed above.

To ensure correctness, we should rigorously mathematically prove the greedy strategy, usually using proof by contradiction or mathematical induction.

However, correctness proofs may also not be easy. If we have no clue, we usually choose to debug the code based on test cases, step by step modifying and verifying the greedy strategy.

","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514-typical-problems-solved-by-greedy-algorithms","level":2,"title":"15.1.4   Typical Problems Solved by Greedy Algorithms","text":"

Greedy algorithms are often applied to optimization problems that satisfy greedy choice property and optimal substructure. Below are some typical greedy algorithm problems.

  • Coin change problem: With certain coin combinations, greedy algorithms can always obtain the optimal solution.
  • Interval scheduling problem: Suppose you have some tasks, each taking place during a period of time, and your goal is to complete as many tasks as possible. If you always choose the task that ends earliest, then the greedy algorithm can obtain the optimal solution.
  • Fractional knapsack problem: Given a set of items and a carrying capacity, your goal is to select a set of items such that the total weight does not exceed the carrying capacity and the total value is maximized. If you always choose the item with the highest value-to-weight ratio (value / weight), then the greedy algorithm can obtain the optimal solution in some cases.
  • Stock trading problem: Given a set of historical stock prices, you can make multiple trades, but if you already hold stocks, you cannot buy again before selling, and the goal is to obtain the maximum profit.
  • Huffman coding: Huffman coding is a greedy algorithm used for lossless data compression. By constructing a Huffman tree and always merging the two nodes with the lowest frequency, the resulting Huffman tree has the minimum weighted path length (encoding length).
  • Dijkstra's algorithm: It is a greedy algorithm for solving the shortest path problem from a given source vertex to all other vertices.
","path":["Chapter 15. Greedy","15.1   Greedy Algorithm"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   Max Capacity Problem","text":"

Question

Input an array \\(ht\\), where each element represents the height of a vertical partition. Any two partitions in the array, along with the space between them, can form a container.

The capacity of the container equals the product of height and width (area), where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions.

Please select two partitions in the array such that the capacity of the formed container is maximized, and return the maximum capacity. An example is shown in Figure 15-7.

Figure 15-7   Example data for the max capacity problem

The container is formed by any two partitions, therefore the state of this problem is the indices of two partitions, denoted as \\([i, j]\\).

According to the problem description, capacity equals height multiplied by width, where height is determined by the shorter partition, and width is the difference in array indices between the two partitions. Let the capacity be \\(cap[i, j]\\), then the calculation formula is:

\\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\]

Let the array length be \\(n\\), then the number of combinations of two partitions (total number of states) is \\(C_n^2 = \\frac{n(n - 1)}{2}\\). Most directly, we can exhaustively enumerate all states to find the maximum capacity, with time complexity \\(O(n^2)\\).

","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#1-greedy-strategy-determination","level":3,"title":"1.   Greedy Strategy Determination","text":"

This problem has a more efficient solution. As shown in Figure 15-8, select a state \\([i, j]\\) where index \\(i < j\\) and height \\(ht[i] < ht[j]\\), meaning \\(i\\) is the short partition and \\(j\\) is the long partition.

Figure 15-8   Initial state

As shown in Figure 15-9, if we now move the long partition \\(j\\) closer to the short partition \\(i\\), the capacity will definitely decrease.

This is because after moving the long partition \\(j\\), the width \\(j-i\\) definitely decreases; and since height is determined by the short partition, the height can only remain unchanged (\\(i\\) is still the short partition) or decrease (the moved \\(j\\) becomes the short partition).

Figure 15-9   State after moving the long partition inward

Conversely, we can only possibly increase capacity by contracting the short partition \\(i\\) inward. Because although width will definitely decrease, height may increase (the moved short partition \\(i\\) may become taller). For example, in Figure 15-10, the area increases after moving the short partition.

Figure 15-10   State after moving the short partition inward

From this we can derive the greedy strategy for this problem: initialize two pointers at both ends of the container, and in each round contract the pointer corresponding to the short partition inward, until the two pointers meet.

Figure 15-11 shows the execution process of the greedy strategy.

  1. In the initial state, pointers \\(i\\) and \\(j\\) are at both ends of the array.
  2. Calculate the capacity of the current state \\(cap[i, j]\\), and update the maximum capacity.
  3. Compare the heights of partition \\(i\\) and partition \\(j\\), and move the short partition inward by one position.
  4. Loop through steps 2. and 3. until \\(i\\) and \\(j\\) meet.
<1><2><3><4><5><6><7><8><9>

Figure 15-11   Greedy process for the max capacity problem

","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

The code loops at most \\(n\\) rounds, therefore the time complexity is \\(O(n)\\).

Variables \\(i\\), \\(j\\), and \\(res\\) use a constant amount of extra space, therefore the space complexity is \\(O(1)\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_capacity.py
def max_capacity(ht: list[int]) -> int:\n    \"\"\"Max capacity: Greedy algorithm\"\"\"\n    # Initialize i, j to be at both ends of the array\n    i, j = 0, len(ht) - 1\n    # Initial max capacity is 0\n    res = 0\n    # Loop for greedy selection until the two boards meet\n    while i < j:\n        # Update max capacity\n        cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        # Move the shorter board inward\n        if ht[i] < ht[j]:\n            i += 1\n        else:\n            j -= 1\n    return res\n
max_capacity.cpp
/* Max capacity: Greedy algorithm */\nint maxCapacity(vector<int> &ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.size() - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = min(ht[i], ht[j]) * (j - i);\n        res = max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.java
/* Max capacity: Greedy algorithm */\nint maxCapacity(int[] ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.length - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.cs
/* Max capacity: Greedy algorithm */\nint MaxCapacity(int[] ht) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0, j = ht.Length - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int cap = Math.Min(ht[i], ht[j]) * (j - i);\n        res = Math.Max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.go
/* Max capacity: Greedy algorithm */\nfunc maxCapacity(ht []int) int {\n    // Initialize i, j to be at both ends of the array\n    i, j := 0, len(ht)-1\n    // Initial max capacity is 0\n    res := 0\n    // Loop for greedy selection until the two boards meet\n    for i < j {\n        // Update max capacity\n        capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)\n        res = int(math.Max(float64(res), float64(capacity)))\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
max_capacity.swift
/* Max capacity: Greedy algorithm */\nfunc maxCapacity(ht: [Int]) -> Int {\n    // Initialize i, j to be at both ends of the array\n    var i = ht.startIndex, j = ht.endIndex - 1\n    // Initial max capacity is 0\n    var res = 0\n    // Loop for greedy selection until the two boards meet\n    while i < j {\n        // Update max capacity\n        let cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i += 1\n        } else {\n            j -= 1\n        }\n    }\n    return res\n}\n
max_capacity.js
/* Max capacity: Greedy algorithm */\nfunction maxCapacity(ht) {\n    // Initialize i, j to be at both ends of the array\n    let i = 0,\n        j = ht.length - 1;\n    // Initial max capacity is 0\n    let res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        const cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
max_capacity.ts
/* Max capacity: Greedy algorithm */\nfunction maxCapacity(ht: number[]): number {\n    // Initialize i, j to be at both ends of the array\n    let i = 0,\n        j = ht.length - 1;\n    // Initial max capacity is 0\n    let res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        const cap: number = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
max_capacity.dart
/* Max capacity: Greedy algorithm */\nint maxCapacity(List<int> ht) {\n  // Initialize i, j to be at both ends of the array\n  int i = 0, j = ht.length - 1;\n  // Initial max capacity is 0\n  int res = 0;\n  // Loop for greedy selection until the two boards meet\n  while (i < j) {\n    // Update max capacity\n    int cap = min(ht[i], ht[j]) * (j - i);\n    res = max(res, cap);\n    // Move the shorter board inward\n    if (ht[i] < ht[j]) {\n      i++;\n    } else {\n      j--;\n    }\n  }\n  return res;\n}\n
max_capacity.rs
/* Max capacity: Greedy algorithm */\nfn max_capacity(ht: &[i32]) -> i32 {\n    // Initialize i, j to be at both ends of the array\n    let mut i = 0;\n    let mut j = ht.len() - 1;\n    // Initial max capacity is 0\n    let mut res = 0;\n    // Loop for greedy selection until the two boards meet\n    while i < j {\n        // Update max capacity\n        let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;\n        res = std::cmp::max(res, cap);\n        // Move the shorter board inward\n        if ht[i] < ht[j] {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    res\n}\n
max_capacity.c
/* Max capacity: Greedy algorithm */\nint maxCapacity(int ht[], int htLength) {\n    // Initialize i, j to be at both ends of the array\n    int i = 0;\n    int j = htLength - 1;\n    // Initial max capacity is 0\n    int res = 0;\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        int capacity = myMin(ht[i], ht[j]) * (j - i);\n        res = myMax(res, capacity);\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.kt
/* Max capacity: Greedy algorithm */\nfun maxCapacity(ht: IntArray): Int {\n    // Initialize i, j to be at both ends of the array\n    var i = 0\n    var j = ht.size - 1\n    // Initial max capacity is 0\n    var res = 0\n    // Loop for greedy selection until the two boards meet\n    while (i < j) {\n        // Update max capacity\n        val cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // Move the shorter board inward\n        if (ht[i] < ht[j]) {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
max_capacity.rb
### Maximum capacity: greedy ###\ndef max_capacity(ht)\n  # Initialize i, j to be at both ends of the array\n  i, j = 0, ht.length - 1\n  # Initial max capacity is 0\n  res = 0\n\n  # Loop for greedy selection until the two boards meet\n  while i < j\n    # Update max capacity\n    cap = [ht[i], ht[j]].min * (j - i)\n    res = [res, cap].max\n    # Move the shorter board inward\n    if ht[i] < ht[j]\n      i += 1\n    else\n      j -= 1\n    end\n  end\n\n  res\nend\n
","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

The reason greedy is faster than exhaustive enumeration is that each round of greedy selection \"skips\" some states.

For example, in state \\(cap[i, j]\\) where \\(i\\) is the short partition and \\(j\\) is the long partition, if we greedily move the short partition \\(i\\) inward by one position, the states shown in Figure 15-12 will be \"skipped\". This means that the capacities of these states cannot be verified later.

\\[ cap[i, i+1], cap[i, i+2], \\dots, cap[i, j-2], cap[i, j-1] \\]

Figure 15-12   States skipped by moving the short partition

Observing carefully, these skipped states are actually all the states obtained by moving the long partition \\(j\\) inward. We have already proven that moving the long partition inward will definitely decrease capacity. That is, the skipped states cannot possibly be the optimal solution, skipping them will not cause us to miss the optimal solution.

The above analysis shows that the operation of moving the short partition is \"safe\", and the greedy strategy is effective.

","path":["Chapter 15. Greedy","15.3   Max Capacity Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/","level":1,"title":"15.4   Max Product Cutting Problem","text":"

Question

Given a positive integer \\(n\\), split it into the sum of at least two positive integers, and find the maximum product of all integers after splitting, as shown in Figure 15-13.

Figure 15-13   Problem definition of max product cutting

Suppose we split \\(n\\) into \\(m\\) integer factors, where the \\(i\\)-th factor is denoted as \\(n_i\\), that is

\\[ n = \\sum_{i=1}^{m}n_i \\]

The goal of this problem is to find the maximum product of all integer factors, namely

\\[ \\max(\\prod_{i=1}^{m}n_i) \\]

We need to think about: how large should the splitting count \\(m\\) be, and what should each \\(n_i\\) be?

","path":["Chapter 15. Greedy","15.4   Max Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#1-greedy-strategy-determination","level":3,"title":"1.   Greedy Strategy Determination","text":"

Based on experience, the product of two integers is often greater than their sum. Suppose we split out a factor of \\(2\\) from \\(n\\), then their product is \\(2(n-2)\\). We compare this product with \\(n\\):

\\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\]

As shown in Figure 15-14, when \\(n \\geq 4\\), splitting out a \\(2\\) will increase the product, which indicates that integers greater than or equal to \\(4\\) should all be split.

Greedy strategy one: If the splitting scheme includes factors \\(\\geq 4\\), then they should continue to be split. The final splitting scheme should only contain factors \\(1\\), \\(2\\), and \\(3\\).

Figure 15-14   Splitting causes product to increase

Next, consider which factor is optimal. Among the three factors \\(1\\), \\(2\\), and \\(3\\), clearly \\(1\\) is the worst, because \\(1 \\times (n-1) < n\\) always holds, meaning splitting out \\(1\\) will actually decrease the product.

As shown in Figure 15-15, when \\(n = 6\\), we have \\(3 \\times 3 > 2 \\times 2 \\times 2\\). This means that splitting out \\(3\\) is better than splitting out \\(2\\).

Greedy strategy two: In the splitting scheme, there should be at most two \\(2\\)s. Because three \\(2\\)s can always be replaced by two \\(3\\)s to obtain a larger product.

Figure 15-15   Optimal splitting factor

In summary, the following greedy strategies can be derived.

  1. Input integer \\(n\\), continuously split out factor \\(3\\) until the remainder is \\(0\\), \\(1\\), or \\(2\\).
  2. When the remainder is \\(0\\), it means \\(n\\) is a multiple of \\(3\\), so no further action is needed.
  3. When the remainder is \\(2\\), do not continue splitting, keep it.
  4. When the remainder is \\(1\\), since \\(2 \\times 2 > 1 \\times 3\\), the last \\(3\\) should be replaced with \\(2\\).
","path":["Chapter 15. Greedy","15.4   Max Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#2-code-implementation","level":3,"title":"2.   Code Implementation","text":"

As shown in Figure 15-16, we don't need to use loops to split the integer, but can use integer division to get the count of \\(3\\)s as \\(a\\), and modulo operation to get the remainder as \\(b\\), at which point we have:

\\[ n = 3 a + b \\]

Please note that for the edge case of \\(n \\leq 3\\), a \\(1\\) must be split out, with product \\(1 \\times (n - 1)\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_product_cutting.py
def max_product_cutting(n: int) -> int:\n    \"\"\"Max product cutting: Greedy algorithm\"\"\"\n    # When n <= 3, must cut out a 1\n    if n <= 3:\n        return 1 * (n - 1)\n    # Greedily cut out 3, a is the number of 3s, b is the remainder\n    a, b = n // 3, n % 3\n    if b == 1:\n        # When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return int(math.pow(3, a - 1)) * 2 * 2\n    if b == 2:\n        # When the remainder is 2, do nothing\n        return int(math.pow(3, a)) * 2\n    # When the remainder is 0, do nothing\n    return int(math.pow(3, a))\n
max_product_cutting.cpp
/* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int)pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int)pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int)pow(3, a);\n}\n
max_product_cutting.java
/* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int) Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int) Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int) Math.pow(3, a);\n}\n
max_product_cutting.cs
/* Max product cutting: Greedy algorithm */\nint MaxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return (int)Math.Pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return (int)Math.Pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return (int)Math.Pow(3, a);\n}\n
max_product_cutting.go
/* Max product cutting: Greedy algorithm */\nfunc maxProductCutting(n int) int {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    a := n / 3\n    b := n % 3\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return int(math.Pow(3, float64(a-1))) * 2 * 2\n    }\n    if b == 2 {\n        // When the remainder is 2, do nothing\n        return int(math.Pow(3, float64(a))) * 2\n    }\n    // When the remainder is 0, do nothing\n    return int(math.Pow(3, float64(a)))\n}\n
max_product_cutting.swift
/* Max product cutting: Greedy algorithm */\nfunc maxProductCutting(n: Int) -> Int {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = n / 3\n    let b = n % 3\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return pow(3, a - 1) * 2 * 2\n    }\n    if b == 2 {\n        // When the remainder is 2, do nothing\n        return pow(3, a) * 2\n    }\n    // When the remainder is 0, do nothing\n    return pow(3, a)\n}\n
max_product_cutting.js
/* Max product cutting: Greedy algorithm */\nfunction maxProductCutting(n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = Math.floor(n / 3);\n    let b = n % 3;\n    if (b === 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // When the remainder is 2, do nothing\n        return Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return Math.pow(3, a);\n}\n
max_product_cutting.ts
/* Max product cutting: Greedy algorithm */\nfunction maxProductCutting(n: number): number {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a: number = Math.floor(n / 3);\n    let b: number = n % 3;\n    if (b === 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // When the remainder is 2, do nothing\n        return Math.pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return Math.pow(3, a);\n}\n
max_product_cutting.dart
/* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n  // When n <= 3, must cut out a 1\n  if (n <= 3) {\n    return 1 * (n - 1);\n  }\n  // Greedily cut out 3, a is the number of 3s, b is the remainder\n  int a = n ~/ 3;\n  int b = n % 3;\n  if (b == 1) {\n    // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n    return (pow(3, a - 1) * 2 * 2).toInt();\n  }\n  if (b == 2) {\n    // When the remainder is 2, do nothing\n    return (pow(3, a) * 2).toInt();\n  }\n  // When the remainder is 0, do nothing\n  return pow(3, a).toInt();\n}\n
max_product_cutting.rs
/* Max product cutting: Greedy algorithm */\nfn max_product_cutting(n: i32) -> i32 {\n    // When n <= 3, must cut out a 1\n    if n <= 3 {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    let a = n / 3;\n    let b = n % 3;\n    if b == 1 {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        3_i32.pow(a as u32 - 1) * 2 * 2\n    } else if b == 2 {\n        // When the remainder is 2, do nothing\n        3_i32.pow(a as u32) * 2\n    } else {\n        // When the remainder is 0, do nothing\n        3_i32.pow(a as u32)\n    }\n}\n
max_product_cutting.c
/* Max product cutting: Greedy algorithm */\nint maxProductCutting(int n) {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return pow(3, a) * 2;\n    }\n    // When the remainder is 0, do nothing\n    return pow(3, a);\n}\n
max_product_cutting.kt
/* Max product cutting: Greedy algorithm */\nfun maxProductCutting(n: Int): Int {\n    // When n <= 3, must cut out a 1\n    if (n <= 3) {\n        return 1 * (n - 1)\n    }\n    // Greedily cut out 3, a is the number of 3s, b is the remainder\n    val a = n / 3\n    val b = n % 3\n    if (b == 1) {\n        // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n        return 3.0.pow((a - 1)).toInt() * 2 * 2\n    }\n    if (b == 2) {\n        // When the remainder is 2, do nothing\n        return 3.0.pow(a).toInt() * 2 * 2\n    }\n    // When the remainder is 0, do nothing\n    return 3.0.pow(a).toInt()\n}\n
max_product_cutting.rb
### Maximum cutting product: greedy ###\ndef max_product_cutting(n)\n  # When n <= 3, must cut out a 1\n  return 1 * (n - 1) if n <= 3\n  # Greedily cut out 3, a is the number of 3s, b is the remainder\n  a, b = n / 3, n % 3\n  # When the remainder is 1, convert a pair of 1 * 3 to 2 * 2\n  return (3.pow(a - 1) * 2 * 2).to_i if b == 1\n  # When the remainder is 2, do nothing\n  return (3.pow(a) * 2).to_i if b == 2\n  # When the remainder is 0, do nothing\n  3.pow(a).to_i\nend\n

Figure 15-16   Calculation method for max product cutting

The time complexity depends on the implementation of the exponentiation operation in the programming language. Taking Python as an example, there are three commonly used power calculation functions.

  • Both the operator ** and the function pow() have time complexity \\(O(\\log⁡ a)\\).
  • The function math.pow() internally calls the C library's pow() function, which performs floating-point exponentiation, with time complexity \\(O(1)\\).

Variables \\(a\\) and \\(b\\) use a constant amount of extra space, therefore the space complexity is \\(O(1)\\).

","path":["Chapter 15. Greedy","15.4   Max Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#3-correctness-proof","level":3,"title":"3.   Correctness Proof","text":"

Using proof by contradiction, only analyzing the case where \\(n \\geq 4\\).

  1. All factors \\(\\leq 3\\): Suppose the optimal splitting scheme includes a factor \\(x \\geq 4\\), then it can definitely continue to be split into \\(2(x-2)\\) to obtain a larger (or equal) product. This contradicts the assumption.
  2. The splitting scheme does not contain \\(1\\): Suppose the optimal splitting scheme includes a factor of \\(1\\), then it can definitely be merged into another factor to obtain a larger product. This contradicts the assumption.
  3. The splitting scheme contains at most two \\(2\\)s: Suppose the optimal splitting scheme includes three \\(2\\)s, then they can definitely be replaced by two \\(3\\)s for a larger product. This contradicts the assumption.
","path":["Chapter 15. Greedy","15.4   Max Product Cutting Problem"],"tags":[]},{"location":"chapter_greedy/summary/","level":1,"title":"15.5   Summary","text":"","path":["Chapter 15. Greedy","15.5   Summary"],"tags":[]},{"location":"chapter_greedy/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Greedy algorithms are typically used to solve optimization problems. The principle is to make locally optimal decisions at each decision stage in hopes of obtaining a globally optimal solution.
  • Greedy algorithms iteratively make one greedy choice after another, transforming the problem into a smaller subproblem in each round, until the problem is solved.
  • Greedy algorithms are not only simple to implement, but also have high problem-solving efficiency. Compared to dynamic programming, greedy algorithms typically have lower time complexity.
  • In the coin change problem, for certain coin combinations, greedy algorithms can guarantee finding the optimal solution; for other coin combinations, however, greedy algorithms may find very poor solutions.
  • Problems suitable for solving with greedy algorithms have two major properties: greedy choice property and optimal substructure. The greedy choice property represents the effectiveness of the greedy strategy.
  • For some complex problems, proving the greedy choice property is not simple. Relatively speaking, disproving it is easier, such as in the coin change problem.
  • Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and correctness proof. Among these, determining the greedy strategy is the core step, and correctness proof is often the difficult point.
  • The fractional knapsack problem, based on the 0-1 knapsack problem, allows selecting a portion of items, and therefore can be solved using greedy algorithms. The correctness of the greedy strategy can be proven using proof by contradiction.
  • The max capacity problem can be solved using exhaustive enumeration with time complexity \\(O(n^2)\\). By designing a greedy strategy to move the short partition inward in each round, the time complexity can be optimized to \\(O(n)\\).
  • In the max product cutting problem, we successively derive two greedy strategies: integers \\(\\geq 4\\) should all continue to be split, and the optimal splitting factor is \\(3\\). The code includes exponentiation operations, and the time complexity depends on the implementation method of exponentiation, typically being \\(O(1)\\) or \\(O(\\log n)\\).
","path":["Chapter 15. Greedy","15.5   Summary"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"Chapter 6.   Hashing","text":"

Abstract

In the world of computing, a hash table is like a clever librarian.

They know how to calculate call numbers, enabling them to quickly locate the target book.

","path":["Chapter 6. Hashing","Chapter 6.   Hashing"],"tags":[]},{"location":"chapter_hashing/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 6.1   Hash Table
  • 6.2   Hash Collision
  • 6.3   Hash Algorithm
  • 6.4   Summary
","path":["Chapter 6. Hashing","Chapter 6.   Hashing"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   Hash Algorithm","text":"

The previous two sections introduced the working principle of hash tables and the methods to handle hash collisions. However, both open addressing and separate chaining can only ensure that the hash table functions normally when hash collisions occur, but cannot reduce the frequency of hash collisions.

If hash collisions occur too frequently, the performance of the hash table will deteriorate drastically. As shown in Figure 6-8, for a separate chaining hash table, in the ideal case, the key-value pairs are evenly distributed across the buckets, achieving optimal query efficiency; in the worst case, all key-value pairs are stored in the same bucket, degrading the time complexity to \\(O(n)\\).

Figure 6-8   Ideal and worst cases of hash collisions

The distribution of key-value pairs is determined by the hash function. Recalling the calculation steps of the hash function, first compute the hash value, then take the modulo by the array length:

index = hash(key) % capacity\n

Observing the above formula, when the hash table capacity capacity is fixed, the hash algorithm hash() determines the output value, thereby determining the distribution of key-value pairs in the hash table.

This means that, to reduce the probability of hash collisions, we should focus on the design of the hash algorithm hash().

","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#631-goals-of-hash-algorithms","level":2,"title":"6.3.1   Goals of Hash Algorithms","text":"

To achieve a \"fast and stable\" hash table data structure, hash algorithms should have the following characteristics:

  • Determinism: For the same input, the hash algorithm should always produce the same output. Only then can the hash table be reliable.
  • High efficiency: The process of computing the hash value should be fast enough. The smaller the computational overhead, the more practical the hash table.
  • Uniform distribution: The hash algorithm should ensure that key-value pairs are evenly distributed in the hash table. The more uniform the distribution, the lower the probability of hash collisions.

In fact, hash algorithms are not only used to implement hash tables but are also widely applied in other fields.

  • Password storage: To protect the security of user passwords, systems usually do not store the plaintext passwords but rather the hash values of the passwords. When a user enters a password, the system calculates the hash value of the input and compares it with the stored hash value. If they match, the password is considered correct.
  • Data integrity check: The data sender can calculate the hash value of the data and send it along; the receiver can recalculate the hash value of the received data and compare it with the received hash value. If they match, the data is considered intact.

For cryptographic applications, to prevent reverse engineering such as deducing the original password from the hash value, hash algorithms need higher-level security features.

  • Unidirectionality: It should be impossible to deduce any information about the input data from the hash value.
  • Collision resistance: It should be extremely difficult to find two different inputs that produce the same hash value.
  • Avalanche effect: Minor changes in the input should lead to significant and unpredictable changes in the output.

Note that \"uniform distribution\" and \"collision resistance\" are two independent concepts. Satisfying uniform distribution does not necessarily mean collision resistance. For example, under random input key, the hash function key % 100 can produce a uniformly distributed output. However, this hash algorithm is too simple, and all key with the same last two digits will have the same output, making it easy to deduce a usable key from the hash value, thereby cracking the password.

","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632-design-of-hash-algorithms","level":2,"title":"6.3.2   Design of Hash Algorithms","text":"

The design of hash algorithms is a complex issue that requires consideration of many factors. However, for some less demanding scenarios, we can also design some simple hash algorithms.

  • Additive hash: Add up the ASCII codes of each character in the input and use the total sum as the hash value.
  • Multiplicative hash: Utilize the non-correlation of multiplication, multiplying each round by a constant, accumulating the ASCII codes of each character into the hash value.
  • XOR hash: Accumulate the hash value by XORing each element of the input data.
  • Rotating hash: Accumulate the ASCII code of each character into a hash value, performing a rotation operation on the hash value before each accumulation.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby simple_hash.py
def add_hash(key: str) -> int:\n    \"\"\"Additive hash\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash += ord(c)\n    return hash % modulus\n\ndef mul_hash(key: str) -> int:\n    \"\"\"Multiplicative hash\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = 31 * hash + ord(c)\n    return hash % modulus\n\ndef xor_hash(key: str) -> int:\n    \"\"\"XOR hash\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash ^= ord(c)\n    return hash % modulus\n\ndef rot_hash(key: str) -> int:\n    \"\"\"Rotational hash\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = (hash << 4) ^ (hash >> 28) ^ ord(c)\n    return hash % modulus\n
simple_hash.cpp
/* Additive hash */\nint addHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Multiplicative hash */\nint mulHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (31 * hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR hash */\nint xorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash ^= (int)c;\n    }\n    return hash & MODULUS;\n}\n\n/* Rotational hash */\nint rotHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n
simple_hash.java
/* Additive hash */\nint addHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* Multiplicative hash */\nint mulHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (31 * hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* XOR hash */\nint xorHash(String key) {\n    int hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash ^= (int) c;\n    }\n    return hash & MODULUS;\n}\n\n/* Rotational hash */\nint rotHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n
simple_hash.cs
/* Additive hash */\nint AddHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Multiplicative hash */\nint MulHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (31 * hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR hash */\nint XorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash ^= c;\n    }\n    return hash & MODULUS;\n}\n\n/* Rotational hash */\nint RotHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS;\n    }\n    return (int)hash;\n}\n
simple_hash.go
/* Additive hash */\nfunc addHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* Multiplicative hash */\nfunc mulHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (31*hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* XOR hash */\nfunc xorHash(key string) int {\n    hash := 0\n    modulus := 1000000007\n    for _, b := range []byte(key) {\n        fmt.Println(int(b))\n        hash ^= int(b)\n        hash = (31*hash + int(b)) % modulus\n    }\n    return hash & modulus\n}\n\n/* Rotational hash */\nfunc rotHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus\n    }\n    return int(hash)\n}\n
simple_hash.swift
/* Additive hash */\nfunc addHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* Multiplicative hash */\nfunc mulHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (31 * hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* XOR hash */\nfunc xorHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash ^= Int(scalar.value)\n        }\n    }\n    return hash & MODULUS\n}\n\n/* Rotational hash */\nfunc rotHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n
simple_hash.js
/* Additive hash */\nfunction addHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* Multiplicative hash */\nfunction mulHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR hash */\nfunction xorHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* Rotational hash */\nfunction rotHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
simple_hash.ts
/* Additive hash */\nfunction addHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* Multiplicative hash */\nfunction mulHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR hash */\nfunction xorHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* Rotational hash */\nfunction rotHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
simple_hash.dart
/* Additive hash */\nint addHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* Multiplicative hash */\nint mulHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (31 * hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* XOR hash */\nint xorHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash ^= key.codeUnitAt(i);\n  }\n  return hash & MODULUS;\n}\n\n/* Rotational hash */\nint rotHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n
simple_hash.rs
/* Additive hash */\nfn add_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* Multiplicative hash */\nfn mul_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (31 * hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* XOR hash */\nfn xor_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash ^= c as i64;\n    }\n\n    (hash & MODULUS) as i32\n}\n\n/* Rotational hash */\nfn rot_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n
simple_hash.c
/* Additive hash */\nint addHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* Multiplicative hash */\nint mulHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (31 * hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR hash */\nint xorHash(char *key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n\n    for (int i = 0; i < strlen(key); i++) {\n        hash ^= (unsigned char)key[i];\n    }\n    return hash & MODULUS;\n}\n\n/* Rotational hash */\nint rotHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS;\n    }\n\n    return (int)hash;\n}\n
simple_hash.kt
/* Additive hash */\nfun addHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* Multiplicative hash */\nfun mulHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (31 * hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* XOR hash */\nfun xorHash(key: String): Int {\n    var hash = 0\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = hash xor c.code\n    }\n    return hash and MODULUS\n}\n\n/* Rotational hash */\nfun rotHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS\n    }\n    return hash.toInt()\n}\n
simple_hash.rb
### Additive hash ###\ndef add_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash += c.ord }\n\n  hash % modulus\nend\n\n### Multiplicative hash ###\ndef mul_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = 31 * hash + c.ord }\n\n  hash % modulus\nend\n\n### XOR hash ###\ndef xor_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash ^= c.ord }\n\n  hash % modulus\nend\n\n### Rotational hash ###\ndef rot_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord }\n\n  hash % modulus\nend\n

It is observed that the last step of each hash algorithm is to take the modulus of the large prime number \\(1000000007\\) to ensure that the hash value is within an appropriate range. It is worth pondering why emphasis is placed on modulo a prime number, or what are the disadvantages of modulo a composite number? This is an interesting question.

To conclude: Using a large prime number as the modulus can maximize the uniform distribution of hash values. Since a prime number does not share common factors with other numbers, it can reduce the periodic patterns caused by the modulo operation, thus avoiding hash collisions.

For example, suppose we choose the composite number \\(9\\) as the modulus, which can be divided by \\(3\\), then all key divisible by \\(3\\) will be mapped to hash values \\(0\\), \\(3\\), \\(6\\).

\\[ \\begin{aligned} \\text{modulus} & = 9 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\\dots \\} \\end{aligned} \\]

If the input key happens to have this kind of arithmetic sequence distribution, then the hash values will cluster, thereby exacerbating hash collisions. Now, suppose we replace modulus with the prime number \\(13\\), since there are no common factors between key and modulus, the uniformity of the output hash values will be significantly improved.

\\[ \\begin{aligned} \\text{modulus} & = 13 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \\dots \\} \\end{aligned} \\]

It is worth noting that if the key is guaranteed to be randomly and uniformly distributed, then choosing a prime number or a composite number as the modulus can both produce uniformly distributed hash values. However, when the distribution of key has some periodicity, modulo a composite number is more likely to result in clustering.

In summary, we usually choose a prime number as the modulus, and this prime number should be large enough to eliminate periodic patterns as much as possible, enhancing the robustness of the hash algorithm.

","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#633-common-hash-algorithms","level":2,"title":"6.3.3   Common Hash Algorithms","text":"

It is not hard to see that the simple hash algorithms mentioned above are quite \"fragile\" and far from reaching the design goals of hash algorithms. For example, since addition and XOR obey the commutative law, additive hash and XOR hash cannot distinguish strings with the same content but in different order, which may exacerbate hash collisions and cause security issues.

In practice, we usually use some standard hash algorithms, such as MD5, SHA-1, SHA-2, and SHA-3. They can map input data of any length to a fixed-length hash value.

Over the past century, hash algorithms have been in a continuous process of upgrading and optimization. Some researchers strive to improve the performance of hash algorithms, while others, including hackers, are dedicated to finding security issues in hash algorithms. Table 6-2 shows hash algorithms commonly used in practical applications.

  • MD5 and SHA-1 have been successfully attacked multiple times and are thus abandoned in various security applications.
  • SHA-2 series, especially SHA-256, is one of the most secure hash algorithms to date, with no successful attacks reported, hence commonly used in various security applications and protocols.
  • SHA-3 has lower implementation costs and higher computational efficiency compared to SHA-2, but its current usage coverage is not as extensive as the SHA-2 series.

Table 6-2   Common hash algorithms

MD5 SHA-1 SHA-2 SHA-3 Release Year 1992 1995 2002 2008 Output Length 128 bit 160 bit 256/512 bit 224/256/384/512 bit Hash Collisions Frequent Frequent Rare Rare Security Level Low, has been successfully attacked Low, has been successfully attacked High High Applications Abandoned, still used for data integrity checks Abandoned Cryptocurrency transaction verification, digital signatures, etc. Can be used to replace SHA-2","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#hash-values-in-data-structures","level":1,"title":"Hash Values in Data Structures","text":"

We know that the keys in a hash table can be of various data types such as integers, decimals, or strings. Programming languages usually provide built-in hash algorithms for these data types to calculate the bucket indices in the hash table. Taking Python as an example, we can use the hash() function to compute the hash values for various data types.

  • The hash values of integers and booleans are their own values.
  • The calculation of hash values for floating-point numbers and strings is more complex, and interested readers are encouraged to study this on their own.
  • The hash value of a tuple is a combination of the hash values of each of its elements, resulting in a single hash value.
  • The hash value of an object is generated based on its memory address. By overriding the hash method of an object, hash values can be generated based on content.

Tip

Be aware that the definition and methods of the built-in hash value calculation functions in different programming languages vary.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby built_in_hash.py
num = 3\nhash_num = hash(num)\n# Hash value of integer 3 is 3\n\nbol = True\nhash_bol = hash(bol)\n# Hash value of boolean True is 1\n\ndec = 3.14159\nhash_dec = hash(dec)\n# Hash value of decimal 3.14159 is 326484311674566659\n\nstr = \"Hello 算法\"\nhash_str = hash(str)\n# Hash value of string \"Hello 算法\" is 4617003410720528961\n\ntup = (12836, \"小哈\")\nhash_tup = hash(tup)\n# Hash value of tuple (12836, '小哈') is 1029005403108185979\n\nobj = ListNode(0)\nhash_obj = hash(obj)\n# Hash value of ListNode object at 0x1058fd810 is 274267521\n
built_in_hash.cpp
int num = 3;\nsize_t hashNum = hash<int>()(num);\n// Hash value of integer 3 is 3\n\nbool bol = true;\nsize_t hashBol = hash<bool>()(bol);\n// Hash value of boolean 1 is 1\n\ndouble dec = 3.14159;\nsize_t hashDec = hash<double>()(dec);\n// Hash value of decimal 3.14159 is 4614256650576692846\n\nstring str = \"Hello 算法\";\nsize_t hashStr = hash<string>()(str);\n// Hash value of string \"Hello 算法\" is 15466937326284535026\n\n// In C++, built-in std::hash() only provides hash values for basic data types\n// Hash values for arrays and objects need to be implemented separately\n
built_in_hash.java
int num = 3;\nint hashNum = Integer.hashCode(num);\n// Hash value of integer 3 is 3\n\nboolean bol = true;\nint hashBol = Boolean.hashCode(bol);\n// Hash value of boolean true is 1231\n\ndouble dec = 3.14159;\nint hashDec = Double.hashCode(dec);\n// Hash value of decimal 3.14159 is -1340954729\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode();\n// Hash value of string \"Hello 算法\" is -727081396\n\nObject[] arr = { 12836, \"小哈\" };\nint hashTup = Arrays.hashCode(arr);\n// Hash value of array [12836, 小哈] is 1151158\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode();\n// Hash value of ListNode object utils.ListNode@7dc5e7b4 is 2110121908\n
built_in_hash.cs
int num = 3;\nint hashNum = num.GetHashCode();\n// Hash value of integer 3 is 3;\n\nbool bol = true;\nint hashBol = bol.GetHashCode();\n// Hash value of boolean true is 1;\n\ndouble dec = 3.14159;\nint hashDec = dec.GetHashCode();\n// Hash value of decimal 3.14159 is -1340954729;\n\nstring str = \"Hello 算法\";\nint hashStr = str.GetHashCode();\n// Hash value of string \"Hello 算法\" is -586107568;\n\nobject[] arr = [12836, \"小哈\"];\nint hashTup = arr.GetHashCode();\n// Hash value of array [12836, 小哈] is 42931033;\n\nListNode obj = new(0);\nint hashObj = obj.GetHashCode();\n// Hash value of ListNode object 0 is 39053774;\n
built_in_hash.go
// Go does not provide built-in hash code functions\n
built_in_hash.swift
let num = 3\nlet hashNum = num.hashValue\n// Hash value of integer 3 is 9047044699613009734\n\nlet bol = true\nlet hashBol = bol.hashValue\n// Hash value of boolean true is -4431640247352757451\n\nlet dec = 3.14159\nlet hashDec = dec.hashValue\n// Hash value of decimal 3.14159 is -2465384235396674631\n\nlet str = \"Hello 算法\"\nlet hashStr = str.hashValue\n// Hash value of string \"Hello 算法\" is -7850626797806988787\n\nlet arr = [AnyHashable(12836), AnyHashable(\"小哈\")]\nlet hashTup = arr.hashValue\n// Hash value of array [AnyHashable(12836), AnyHashable(\"小哈\")] is -2308633508154532996\n\nlet obj = ListNode(x: 0)\nlet hashObj = obj.hashValue\n// Hash value of ListNode object utils.ListNode is -2434780518035996159\n
built_in_hash.js
// JavaScript does not provide built-in hash code functions\n
built_in_hash.ts
// TypeScript does not provide built-in hash code functions\n
built_in_hash.dart
int num = 3;\nint hashNum = num.hashCode;\n// Hash value of integer 3 is 34803\n\nbool bol = true;\nint hashBol = bol.hashCode;\n// Hash value of boolean true is 1231\n\ndouble dec = 3.14159;\nint hashDec = dec.hashCode;\n// Hash value of decimal 3.14159 is 2570631074981783\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode;\n// Hash value of string \"Hello 算法\" is 468167534\n\nList arr = [12836, \"小哈\"];\nint hashArr = arr.hashCode;\n// Hash value of array [12836, 小哈] is 976512528\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode;\n// Hash value of ListNode object Instance of 'ListNode' is 1033450432\n
built_in_hash.rs
use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\nlet num = 3;\nlet mut num_hasher = DefaultHasher::new();\nnum.hash(&mut num_hasher);\nlet hash_num = num_hasher.finish();\n// Hash value of integer 3 is 568126464209439262\n\nlet bol = true;\nlet mut bol_hasher = DefaultHasher::new();\nbol.hash(&mut bol_hasher);\nlet hash_bol = bol_hasher.finish();\n// Hash value of boolean true is 4952851536318644461\n\nlet dec: f32 = 3.14159;\nlet mut dec_hasher = DefaultHasher::new();\ndec.to_bits().hash(&mut dec_hasher);\nlet hash_dec = dec_hasher.finish();\n// Hash value of decimal 3.14159 is 2566941990314602357\n\nlet str = \"Hello 算法\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// Hash value of string \"Hello 算法\" is 16092673739211250988\n\nlet arr = (&12836, &\"小哈\");\nlet mut tup_hasher = DefaultHasher::new();\narr.hash(&mut tup_hasher);\nlet hash_tup = tup_hasher.finish();\n// Hash value of tuple (12836, \"小哈\") is 1885128010422702749\n\nlet node = ListNode::new(42);\nlet mut hasher = DefaultHasher::new();\nnode.borrow().val.hash(&mut hasher);\nlet hash = hasher.finish();\n// Hash value of ListNode object RefCell { value: ListNode { val: 42, next: None } } is 15387811073369036852\n
built_in_hash.c
// C does not provide built-in hash code functions\n
built_in_hash.kt
val num = 3\nval hashNum = num.hashCode()\n// Hash value of integer 3 is 3\n\nval bol = true\nval hashBol = bol.hashCode()\n// Hash value of boolean true is 1231\n\nval dec = 3.14159\nval hashDec = dec.hashCode()\n// Hash value of decimal 3.14159 is -1340954729\n\nval str = \"Hello 算法\"\nval hashStr = str.hashCode()\n// Hash value of string \"Hello 算法\" is -727081396\n\nval arr = arrayOf<Any>(12836, \"小哈\")\nval hashTup = arr.hashCode()\n// Hash value of array [12836, 小哈] is 189568618\n\nval obj = ListNode(0)\nval hashObj = obj.hashCode()\n// Hash value of ListNode object utils.ListNode@1d81eb93 is 495053715\n
built_in_hash.rb
num = 3\nhash_num = num.hash\n# Hash value of integer 3 is -4385856518450339636\n\nbol = true\nhash_bol = bol.hash\n# Hash value of boolean true is -1617938112149317027\n\ndec = 3.14159\nhash_dec = dec.hash\n# Hash value of decimal 3.14159 is -1479186995943067893\n\nstr = \"Hello 算法\"\nhash_str = str.hash\n# Hash value of string \"Hello 算法\" is -4075943250025831763\n\ntup = [12836, '小哈']\nhash_tup = tup.hash\n# Hash value of tuple (12836, '小哈') is 1999544809202288822\n\nobj = ListNode.new(0)\nhash_obj = obj.hash\n# Hash value of ListNode object #<ListNode:0x000078133140ab70> is 4302940560806366381\n
Visualized Execution

https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

In many programming languages, only immutable objects can serve as the key in a hash table. If we use a list (dynamic array) as a key, when the contents of the list change, its hash value also changes, and we would no longer be able to find the original value in the hash table.

Although the member variables of a custom object (such as a linked list node) are mutable, it is hashable. This is because the hash value of an object is usually generated based on its memory address, and even if the contents of the object change, the memory address remains the same, so the hash value remains unchanged.

You might have noticed that the hash values output in different consoles are different. This is because the Python interpreter adds a random salt to the string hash function each time it starts up. This approach effectively prevents HashDoS attacks and enhances the security of the hash algorithm.

","path":["Chapter 6. Hashing","6.3   Hash Algorithm"],"tags":[]},{"location":"chapter_hashing/hash_collision/","level":1,"title":"6.2   Hash Collision","text":"

The previous section mentioned that, in most cases, the input space of a hash function is much larger than the output space, so theoretically, hash collisions are inevitable. For example, if the input space is all integers and the output space is the array capacity size, then multiple integers will inevitably be mapped to the same bucket index.

Hash collisions can lead to incorrect query results, severely impacting the usability of the hash table. To address this issue, whenever a hash collision occurs, we can perform hash table expansion until the collision disappears. This approach is simple, straightforward, and effective, but it is very inefficient because hash table expansion involves a large amount of data migration and hash value recalculation. To improve efficiency, we can adopt the following strategies:

  1. Improve the hash table data structure so that the hash table can function normally when hash collisions occur.
  2. Only expand when necessary, that is, only when hash collisions are severe.

The main methods for improving the structure of hash tables include \"separate chaining\" and \"open addressing\".

","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#621-separate-chaining","level":2,"title":"6.2.1   Separate Chaining","text":"

In the original hash table, each bucket can store only one key-value pair. Separate chaining converts a single element into a linked list, treating key-value pairs as linked list nodes and storing all colliding key-value pairs in the same linked list. Figure 6-5 shows an example of a separate chaining hash table.

Figure 6-5   Separate chaining hash table

The operations of a hash table implemented with separate chaining have changed as follows:

  • Querying elements: Input key, obtain the bucket index through the hash function, then access the head node of the linked list, then traverse the linked list and compare key to find the target key-value pair.
  • Adding elements: First access the linked list head node through the hash function, then append the node (key-value pair) to the linked list.
  • Deleting elements: Access the head of the linked list based on the result of the hash function, then traverse the linked list to find the target node and delete it.

Separate chaining has the following limitations:

  • Increased Space Usage: The linked list contains node pointers, which consume more memory space than arrays.
  • Reduced Query Efficiency: This is because linear traversal of the linked list is required to find the corresponding element.

The code below provides a simple implementation of a separate chaining hash table, with two things to note:

  • Lists (dynamic arrays) are used instead of linked lists to simplify the code. In this setup, the hash table (array) contains multiple buckets, each of which is a list.
  • This implementation includes a hash table expansion method. When the load factor exceeds \\(\\frac{2}{3}\\), we expand the hash table to \\(2\\) times its original size.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_chaining.py
class HashMapChaining:\n    \"\"\"Hash table with separate chaining\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self.size = 0  # Number of key-value pairs\n        self.capacity = 4  # Hash table capacity\n        self.load_thres = 2.0 / 3.0  # Load factor threshold for triggering expansion\n        self.extend_ratio = 2  # Expansion multiplier\n        self.buckets = [[] for _ in range(self.capacity)]  # Bucket array\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Load factor\"\"\"\n        return self.size / self.capacity\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Query operation\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket, if key is found, return corresponding val\n        for pair in bucket:\n            if pair.key == key:\n                return pair.val\n        # If key is not found, return None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Add operation\"\"\"\n        # When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in bucket:\n            if pair.key == key:\n                pair.val = val\n                return\n        # If key does not exist, append key-value pair to the end\n        pair = Pair(key, val)\n        bucket.append(pair)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Remove operation\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # Traverse bucket and remove key-value pair from it\n        for pair in bucket:\n            if pair.key == key:\n                bucket.remove(pair)\n                self.size -= 1\n                break\n\n    def extend(self):\n        \"\"\"Expand hash table\"\"\"\n        # Temporarily store the original hash table\n        buckets = self.buckets\n        # Initialize expanded new hash table\n        self.capacity *= self.extend_ratio\n        self.buckets = [[] for _ in range(self.capacity)]\n        self.size = 0\n        # Move key-value pairs from original hash table to new hash table\n        for bucket in buckets:\n            for pair in bucket:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"Print hash table\"\"\"\n        for bucket in self.buckets:\n            res = []\n            for pair in bucket:\n                res.append(str(pair.key) + \" -> \" + pair.val)\n            print(res)\n
hash_map_chaining.cpp
/* Hash table with separate chaining */\nclass HashMapChaining {\n  private:\n    int size;                       // Number of key-value pairs\n    int capacity;                   // Hash table capacity\n    double loadThres;               // Load factor threshold for triggering expansion\n    int extendRatio;                // Expansion multiplier\n    vector<vector<Pair *>> buckets; // Bucket array\n\n  public:\n    /* Constructor */\n    HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {\n        buckets.resize(capacity);\n    }\n\n    /* Destructor */\n    ~HashMapChaining() {\n        for (auto &bucket : buckets) {\n            for (Pair *pair : bucket) {\n                // Free memory\n                delete pair;\n            }\n        }\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double)size / (double)capacity;\n    }\n\n    /* Query operation */\n    string get(int key) {\n        int index = hashFunc(key);\n        // Traverse bucket, if key is found, return corresponding val\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                return pair->val;\n            }\n        }\n        // Return empty string if key not found\n        return \"\";\n    }\n\n    /* Add operation */\n    void put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                pair->val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        buckets[index].push_back(new Pair(key, val));\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        auto &bucket = buckets[index];\n        // Traverse bucket and remove key-value pair from it\n        for (int i = 0; i < bucket.size(); i++) {\n            if (bucket[i]->key == key) {\n                Pair *tmp = bucket[i];\n                bucket.erase(bucket.begin() + i); // Remove key-value pair from it\n                delete tmp;                       // Free memory\n                size--;\n                return;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        vector<vector<Pair *>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets.clear();\n        buckets.resize(capacity);\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (auto &bucket : bucketsTmp) {\n            for (Pair *pair : bucket) {\n                put(pair->key, pair->val);\n                // Free memory\n                delete pair;\n            }\n        }\n    }\n\n    /* Print hash table */\n    void print() {\n        for (auto &bucket : buckets) {\n            cout << \"[\";\n            for (Pair *pair : bucket) {\n                cout << pair->key << \" -> \" << pair->val << \", \";\n            }\n            cout << \"]\\n\";\n        }\n    }\n};\n
hash_map_chaining.java
/* Hash table with separate chaining */\nclass HashMapChaining {\n    int size; // Number of key-value pairs\n    int capacity; // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio; // Expansion multiplier\n    List<List<Pair>> buckets; // Bucket array\n\n    /* Constructor */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Query operation */\n    String get(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket, if key is found, return corresponding val\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    void put(int key, String val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        Pair pair = new Pair(key, val);\n        bucket.add(pair);\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // Traverse bucket and remove key-value pair from it\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        List<List<Pair>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (List<Pair> bucket : bucketsTmp) {\n            for (Pair pair : bucket) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    void print() {\n        for (List<Pair> bucket : buckets) {\n            List<String> res = new ArrayList<>();\n            for (Pair pair : bucket) {\n                res.add(pair.key + \" -> \" + pair.val);\n            }\n            System.out.println(res);\n        }\n    }\n}\n
hash_map_chaining.cs
/* Hash table with separate chaining */\nclass HashMapChaining {\n    int size; // Number of key-value pairs\n    int capacity; // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio; // Expansion multiplier\n    List<List<Pair>> buckets; // Bucket array\n\n    /* Constructor */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n    }\n\n    /* Hash function */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Query operation */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        // Traverse bucket, if key is found, return corresponding val\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void Put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        int index = HashFunc(key);\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        buckets[index].Add(new Pair(key, val));\n        size++;\n    }\n\n    /* Remove operation */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Traverse bucket and remove key-value pair from it\n        foreach (Pair pair in buckets[index].ToList()) {\n            if (pair.key == key) {\n                buckets[index].Remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    void Extend() {\n        // Temporarily store the original hash table\n        List<List<Pair>> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        foreach (List<Pair> bucket in bucketsTmp) {\n            foreach (Pair pair in bucket) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    public void Print() {\n        foreach (List<Pair> bucket in buckets) {\n            List<string> res = [];\n            foreach (Pair pair in bucket) {\n                res.Add(pair.key + \" -> \" + pair.val);\n            }\n            foreach (string kv in res) {\n                Console.WriteLine(kv);\n            }\n        }\n    }\n}\n
hash_map_chaining.go
/* Hash table with separate chaining */\ntype hashMapChaining struct {\n    size        int      // Number of key-value pairs\n    capacity    int      // Hash table capacity\n    loadThres   float64  // Load factor threshold for triggering expansion\n    extendRatio int      // Expansion multiplier\n    buckets     [][]pair // Bucket array\n}\n\n/* Constructor */\nfunc newHashMapChaining() *hashMapChaining {\n    buckets := make([][]pair, 4)\n    for i := 0; i < 4; i++ {\n        buckets[i] = make([]pair, 0)\n    }\n    return &hashMapChaining{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     buckets,\n    }\n}\n\n/* Hash function */\nfunc (m *hashMapChaining) hashFunc(key int) int {\n    return key % m.capacity\n}\n\n/* Load factor */\nfunc (m *hashMapChaining) loadFactor() float64 {\n    return float64(m.size) / float64(m.capacity)\n}\n\n/* Query operation */\nfunc (m *hashMapChaining) get(key int) string {\n    idx := m.hashFunc(key)\n    bucket := m.buckets[idx]\n    // Traverse bucket, if key is found, return corresponding val\n    for _, p := range bucket {\n        if p.key == key {\n            return p.val\n        }\n    }\n    // Return empty string if key not found\n    return \"\"\n}\n\n/* Add operation */\nfunc (m *hashMapChaining) put(key int, val string) {\n    // When load factor exceeds threshold, perform expansion\n    if m.loadFactor() > m.loadThres {\n        m.extend()\n    }\n    idx := m.hashFunc(key)\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\n    for i := range m.buckets[idx] {\n        if m.buckets[idx][i].key == key {\n            m.buckets[idx][i].val = val\n            return\n        }\n    }\n    // If key does not exist, append key-value pair to the end\n    p := pair{\n        key: key,\n        val: val,\n    }\n    m.buckets[idx] = append(m.buckets[idx], p)\n    m.size += 1\n}\n\n/* Remove operation */\nfunc (m *hashMapChaining) remove(key int) {\n    idx := m.hashFunc(key)\n    // Traverse bucket and remove key-value pair from it\n    for i, p := range m.buckets[idx] {\n        if p.key == key {\n            // Slice deletion\n            m.buckets[idx] = append(m.buckets[idx][:i], m.buckets[idx][i+1:]...)\n            m.size -= 1\n            break\n        }\n    }\n}\n\n/* Expand hash table */\nfunc (m *hashMapChaining) extend() {\n    // Temporarily store the original hash table\n    tmpBuckets := make([][]pair, len(m.buckets))\n    for i := 0; i < len(m.buckets); i++ {\n        tmpBuckets[i] = make([]pair, len(m.buckets[i]))\n        copy(tmpBuckets[i], m.buckets[i])\n    }\n    // Initialize expanded new hash table\n    m.capacity *= m.extendRatio\n    m.buckets = make([][]pair, m.capacity)\n    for i := 0; i < m.capacity; i++ {\n        m.buckets[i] = make([]pair, 0)\n    }\n    m.size = 0\n    // Move key-value pairs from original hash table to new hash table\n    for _, bucket := range tmpBuckets {\n        for _, p := range bucket {\n            m.put(p.key, p.val)\n        }\n    }\n}\n\n/* Print hash table */\nfunc (m *hashMapChaining) print() {\n    var builder strings.Builder\n\n    for _, bucket := range m.buckets {\n        builder.WriteString(\"[\")\n        for _, p := range bucket {\n            builder.WriteString(strconv.Itoa(p.key) + \" -> \" + p.val + \" \")\n        }\n        builder.WriteString(\"]\")\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
hash_map_chaining.swift
/* Hash table with separate chaining */\nclass HashMapChaining {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    var loadThres: Double // Load factor threshold for triggering expansion\n    var extendRatio: Int // Expansion multiplier\n    var buckets: [[Pair]] // Bucket array\n\n    /* Constructor */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: [], count: capacity)\n    }\n\n    /* Hash function */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Load factor */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket, if key is found, return corresponding val\n        for pair in bucket {\n            if pair.key == key {\n                return pair.val\n            }\n        }\n        // Return nil if key not found\n        return nil\n    }\n\n    /* Add operation */\n    func put(key: Int, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if loadFactor() > loadThres {\n            extend()\n        }\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in bucket {\n            if pair.key == key {\n                pair.val = val\n                return\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        let pair = Pair(key: key, val: val)\n        buckets[index].append(pair)\n        size += 1\n    }\n\n    /* Remove operation */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // Traverse bucket and remove key-value pair from it\n        for (pairIndex, pair) in bucket.enumerated() {\n            if pair.key == key {\n                buckets[index].remove(at: pairIndex)\n                size -= 1\n                break\n            }\n        }\n    }\n\n    /* Expand hash table */\n    func extend() {\n        // Temporarily store the original hash table\n        let bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = Array(repeating: [], count: capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for bucket in bucketsTmp {\n            for pair in bucket {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Print hash table */\n    func print() {\n        for bucket in buckets {\n            let res = bucket.map { \"\\($0.key) -> \\($0.val)\" }\n            Swift.print(res)\n        }\n    }\n}\n
hash_map_chaining.js
/* Hash table with separate chaining */\nclass HashMapChaining {\n    #size; // Number of key-value pairs\n    #capacity; // Hash table capacity\n    #loadThres; // Load factor threshold for triggering expansion\n    #extendRatio; // Expansion multiplier\n    #buckets; // Bucket array\n\n    /* Constructor */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* Hash function */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Query operation */\n    get(key) {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if key is found, return corresponding val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key, val) {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key) {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Traverse bucket and remove key-value pair from it\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    #extend() {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    print() {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
hash_map_chaining.ts
/* Hash table with separate chaining */\nclass HashMapChaining {\n    #size: number; // Number of key-value pairs\n    #capacity: number; // Hash table capacity\n    #loadThres: number; // Load factor threshold for triggering expansion\n    #extendRatio: number; // Expansion multiplier\n    #buckets: Pair[][]; // Bucket array\n\n    /* Constructor */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* Hash function */\n    #hashFunc(key: number): number {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor(): number {\n        return this.#size / this.#capacity;\n    }\n\n    /* Query operation */\n    get(key: number): string | null {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if key is found, return corresponding val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // If key is not found, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key: number, val: string): void {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key: number): void {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // Traverse bucket and remove key-value pair from it\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* Expand hash table */\n    #extend(): void {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    print(): void {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
hash_map_chaining.dart
/* Hash table with separate chaining */\nclass HashMapChaining {\n  late int size; // Number of key-value pairs\n  late int capacity; // Hash table capacity\n  late double loadThres; // Load factor threshold for triggering expansion\n  late int extendRatio; // Expansion multiplier\n  late List<List<Pair>> buckets; // Bucket array\n\n  /* Constructor */\n  HashMapChaining() {\n    size = 0;\n    capacity = 4;\n    loadThres = 2.0 / 3.0;\n    extendRatio = 2;\n    buckets = List.generate(capacity, (_) => []);\n  }\n\n  /* Hash function */\n  int hashFunc(int key) {\n    return key % capacity;\n  }\n\n  /* Load factor */\n  double loadFactor() {\n    return size / capacity;\n  }\n\n  /* Query operation */\n  String? get(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket, if key is found, return corresponding val\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        return pair.val;\n      }\n    }\n    // If key is not found, return null\n    return null;\n  }\n\n  /* Add operation */\n  void put(int key, String val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor() > loadThres) {\n      extend();\n    }\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        pair.val = val;\n        return;\n      }\n    }\n    // If key does not exist, append key-value pair to the end\n    Pair pair = Pair(key, val);\n    bucket.add(pair);\n    size++;\n  }\n\n  /* Remove operation */\n  void remove(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // Traverse bucket and remove key-value pair from it\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        bucket.remove(pair);\n        size--;\n        break;\n      }\n    }\n  }\n\n  /* Expand hash table */\n  void extend() {\n    // Temporarily store the original hash table\n    List<List<Pair>> bucketsTmp = buckets;\n    // Initialize expanded new hash table\n    capacity *= extendRatio;\n    buckets = List.generate(capacity, (_) => []);\n    size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (List<Pair> bucket in bucketsTmp) {\n      for (Pair pair in bucket) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Print hash table */\n  void printHashMap() {\n    for (List<Pair> bucket in buckets) {\n      List<String> res = [];\n      for (Pair pair in bucket) {\n        res.add(\"${pair.key} -> ${pair.val}\");\n      }\n      print(res);\n    }\n  }\n}\n
hash_map_chaining.rs
/* Hash table with separate chaining */\nstruct HashMapChaining {\n    size: usize,\n    capacity: usize,\n    load_thres: f32,\n    extend_ratio: usize,\n    buckets: Vec<Vec<Pair>>,\n}\n\nimpl HashMapChaining {\n    /* Constructor */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![vec![]; 4],\n        }\n    }\n\n    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % self.capacity\n    }\n\n    /* Load factor */\n    fn load_factor(&self) -> f32 {\n        self.size as f32 / self.capacity as f32\n    }\n\n    /* Remove operation */\n    fn remove(&mut self, key: i32) -> Option<String> {\n        let index = self.hash_func(key);\n\n        // Traverse bucket and remove key-value pair from it\n        for (i, p) in self.buckets[index].iter_mut().enumerate() {\n            if p.key == key {\n                let pair = self.buckets[index].remove(i);\n                self.size -= 1;\n                return Some(pair.val);\n            }\n        }\n\n        // If key is not found, return None\n        None\n    }\n\n    /* Expand hash table */\n    fn extend(&mut self) {\n        // Temporarily store the original hash table\n        let buckets_tmp = std::mem::take(&mut self.buckets);\n\n        // Initialize expanded new hash table\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![Vec::new(); self.capacity as usize];\n        self.size = 0;\n\n        // Move key-value pairs from original hash table to new hash table\n        for bucket in buckets_tmp {\n            for pair in bucket {\n                self.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    fn print(&self) {\n        for bucket in &self.buckets {\n            let mut res = Vec::new();\n            for pair in bucket {\n                res.push(format!(\"{} -> {}\", pair.key, pair.val));\n            }\n            println!(\"{:?}\", res);\n        }\n    }\n\n    /* Add operation */\n    fn put(&mut self, key: i32, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n\n        let index = self.hash_func(key);\n\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for pair in self.buckets[index].iter_mut() {\n            if pair.key == key {\n                pair.val = val;\n                return;\n            }\n        }\n\n        // If key does not exist, append key-value pair to the end\n        let pair = Pair { key, val };\n        self.buckets[index].push(pair);\n        self.size += 1;\n    }\n\n    /* Query operation */\n    fn get(&self, key: i32) -> Option<&str> {\n        let index = self.hash_func(key);\n\n        // Traverse bucket, if key is found, return corresponding val\n        for pair in self.buckets[index].iter() {\n            if pair.key == key {\n                return Some(&pair.val);\n            }\n        }\n\n        // If key is not found, return None\n        None\n    }\n}\n
hash_map_chaining.c
/* Linked list node */\ntypedef struct Node {\n    Pair *pair;\n    struct Node *next;\n} Node;\n\n/* Hash table with separate chaining */\ntypedef struct {\n    int size;         // Number of key-value pairs\n    int capacity;     // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio;  // Expansion multiplier\n    Node **buckets;   // Bucket array\n} HashMapChaining;\n\n/* Constructor */\nHashMapChaining *newHashMapChaining() {\n    HashMapChaining *hashMap = (HashMapChaining *)malloc(sizeof(HashMapChaining));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    return hashMap;\n}\n\n/* Destructor */\nvoid delHashMapChaining(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        while (cur) {\n            Node *tmp = cur;\n            cur = cur->next;\n            free(tmp->pair);\n            free(tmp);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap);\n}\n\n/* Hash function */\nint hashFunc(HashMapChaining *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Load factor */\ndouble loadFactor(HashMapChaining *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Query operation */\nchar *get(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    // Traverse bucket, if key is found, return corresponding val\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            return cur->pair->val;\n        }\n        cur = cur->next;\n    }\n    return \"\"; // Return empty string if key not found\n}\n\n/* Add operation */\nvoid put(HashMapChaining *hashMap, int key, const char *val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    int index = hashFunc(hashMap, key);\n    // Traverse bucket, if specified key is encountered, update corresponding val and return\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            strcpy(cur->pair->val, val); // If specified key is found, update corresponding val and return\n            return;\n        }\n        cur = cur->next;\n    }\n    // If key not found, add key-value pair to list head\n    Pair *newPair = (Pair *)malloc(sizeof(Pair));\n    newPair->key = key;\n    strcpy(newPair->val, val);\n    Node *newNode = (Node *)malloc(sizeof(Node));\n    newNode->pair = newPair;\n    newNode->next = hashMap->buckets[index];\n    hashMap->buckets[index] = newNode;\n    hashMap->size++;\n}\n\n/* Expand hash table */\nvoid extend(HashMapChaining *hashMap) {\n    // Temporarily store the original hash table\n    int oldCapacity = hashMap->capacity;\n    Node **oldBuckets = hashMap->buckets;\n    // Initialize expanded new hash table\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    hashMap->size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (int i = 0; i < oldCapacity; i++) {\n        Node *cur = oldBuckets[i];\n        while (cur) {\n            put(hashMap, cur->pair->key, cur->pair->val);\n            Node *temp = cur;\n            cur = cur->next;\n            // Free memory\n            free(temp->pair);\n            free(temp);\n        }\n    }\n\n    free(oldBuckets);\n}\n\n/* Remove operation */\nvoid removeItem(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    Node *cur = hashMap->buckets[index];\n    Node *pre = NULL;\n    while (cur) {\n        if (cur->pair->key == key) {\n            // Remove key-value pair from it\n            if (pre) {\n                pre->next = cur->next;\n            } else {\n                hashMap->buckets[index] = cur->next;\n            }\n            // Free memory\n            free(cur->pair);\n            free(cur);\n            hashMap->size--;\n            return;\n        }\n        pre = cur;\n        cur = cur->next;\n    }\n}\n\n/* Print hash table */\nvoid print(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        printf(\"[\");\n        while (cur) {\n            printf(\"%d -> %s, \", cur->pair->key, cur->pair->val);\n            cur = cur->next;\n        }\n        printf(\"]\\n\");\n    }\n}\n
hash_map_chaining.kt
/* Hash table with separate chaining */\nclass HashMapChaining {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    val loadThres: Double // Load factor threshold for triggering expansion\n    val extendRatio: Int // Expansion multiplier\n    var buckets: MutableList<MutableList<Pair>> // Bucket array\n\n    /* Constructor */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n    }\n\n    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Load factor */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Query operation */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket, if key is found, return corresponding val\n        for (pair in bucket) {\n            if (pair.key == key) return pair._val\n        }\n        // If key is not found, return null\n        return null\n    }\n\n    /* Add operation */\n    fun put(key: Int, _val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket, if specified key is encountered, update corresponding val and return\n        for (pair in bucket) {\n            if (pair.key == key) {\n                pair._val = _val\n                return\n            }\n        }\n        // If key does not exist, append key-value pair to the end\n        val pair = Pair(key, _val)\n        bucket.add(pair)\n        size++\n    }\n\n    /* Remove operation */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // Traverse bucket and remove key-value pair from it\n        for (pair in bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair)\n                size--\n                break\n            }\n        }\n    }\n\n    /* Expand hash table */\n    fun extend() {\n        // Temporarily store the original hash table\n        val bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        // mutablelist has no fixed size\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for (bucket in bucketsTmp) {\n            for (pair in bucket) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Print hash table */\n    fun print() {\n        for (bucket in buckets) {\n            val res = mutableListOf<String>()\n            for (pair in bucket) {\n                val k = pair.key\n                val v = pair._val\n                res.add(\"$k -> $v\")\n            }\n            println(res)\n        }\n    }\n}\n
hash_map_chaining.rb
### Hash map with chaining ###\nclass HashMapChaining\n  ### Constructor ###\n  def initialize\n    @size = 0 # Number of key-value pairs\n    @capacity = 4 # Hash table capacity\n    @load_thres = 2.0 / 3.0 # Load factor threshold for triggering expansion\n    @extend_ratio = 2 # Expansion multiplier\n    @buckets = Array.new(@capacity) { [] } # Bucket array\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Load factor ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Query operation ###\n  def get(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket, if key is found, return corresponding val\n    for pair in bucket\n      return pair.val if pair.key == key\n    end\n    # Return nil if key not found\n    nil\n  end\n\n  ### Add operation ###\n  def put(key, val)\n    # When load factor exceeds threshold, perform expansion\n    extend if load_factor > @load_thres\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket, if specified key is encountered, update corresponding val and return\n    for pair in bucket\n      if pair.key == key\n        pair.val = val\n        return\n      end\n    end\n    # If key does not exist, append key-value pair to the end\n    pair = Pair.new(key, val)\n    bucket << pair\n    @size += 1\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # Traverse bucket and remove key-value pair from it\n    for pair in bucket\n      if pair.key == key\n        bucket.delete(pair)\n        @size -= 1\n        break\n      end\n    end\n  end\n\n  ### Expand hash table ###\n  def extend\n    # Temporarily store original hash table\n    buckets = @buckets\n    # Initialize expanded new hash table\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity) { [] }\n    @size = 0\n    # Move key-value pairs from original hash table to new hash table\n    for bucket in buckets\n      for pair in bucket\n        put(pair.key, pair.val)\n      end\n    end\n  end\n\n  ### Print hash table ###\n  def print\n    for bucket in @buckets\n      res = []\n      for pair in bucket\n        res << \"#{pair.key} -> #{pair.val}\"\n      end\n      pp res\n    end\n  end\nend\n

It's worth noting that when the linked list is very long, the query efficiency \\(O(n)\\) is poor. In this case, the list can be converted to an \"AVL tree\" or \"Red-Black tree\" to optimize the time complexity of the query operation to \\(O(\\log n)\\).

","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#622-open-addressing","level":2,"title":"6.2.2   Open Addressing","text":"

Open addressing does not introduce additional data structures but instead handles hash collisions through \"multiple probes\". The probing methods mainly include linear probing, quadratic probing, and double hashing.

Let's use linear probing as an example to introduce the mechanism of open addressing hash tables.

","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#1-linear-probing","level":3,"title":"1.   Linear Probing","text":"

Linear probing uses a fixed-step linear search for probing, and its operation method differs from ordinary hash tables.

  • Inserting elements: Calculate the bucket index using the hash function. If the bucket already contains an element, linearly traverse forward from the conflict position (usually with a step size of \\(1\\)) until an empty bucket is found, then insert the element.
  • Searching for elements: If a hash collision is encountered, use the same step size to linearly traverse forward until the corresponding element is found and return value; if an empty bucket is encountered, it means the target element is not in the hash table, so return None.

Figure 6-6 shows the distribution of key-value pairs in an open addressing (linear probing) hash table. According to this hash function, keys with the same last two digits will be mapped to the same bucket. Through linear probing, they are stored sequentially in that bucket and the buckets below it.

Figure 6-6   Distribution of key-value pairs in open addressing (linear probing) hash table

However, linear probing is prone to create \"clustering\". Specifically, the longer the continuously occupied positions in the array, the greater the probability of hash collisions occurring in these continuous positions, further promoting clustering growth at that position, forming a vicious cycle, and ultimately leading to degraded efficiency of insertion, deletion, query, and update operations.

It's important to note that we cannot directly delete elements in an open addressing hash table. Deleting an element creates an empty bucket None in the array. When searching for elements, if linear probing encounters this empty bucket, it will return, making the elements below this empty bucket inaccessible. The program may incorrectly assume these elements do not exist, as shown in Figure 6-7.

Figure 6-7   Query issues caused by deletion in open addressing

To solve this problem, we can adopt the lazy deletion mechanism: instead of directly removing elements from the hash table, use a constant TOMBSTONE to mark the bucket. In this mechanism, both None and TOMBSTONE represent empty buckets and can hold key-value pairs. However, when linear probing encounters TOMBSTONE, it should continue traversing since there may still be key-value pairs below it.

However, lazy deletion may accelerate the performance degradation of the hash table. Every deletion operation produces a deletion mark, and as TOMBSTONE increases, the search time will also increase because linear probing may need to skip multiple TOMBSTONE to find the target element.

To address this, consider recording the index of the first encountered TOMBSTONE during linear probing and swapping the searched target element with that TOMBSTONE. The benefit of doing this is that each time an element is queried or added, the element will be moved to a bucket closer to its ideal position (the starting point of probing), thereby optimizing query efficiency.

The code below implements an open addressing (linear probing) hash table with lazy deletion. To make better use of the hash table space, we treat the hash table as a \"circular array\". When going beyond the end of the array, we return to the beginning and continue traversing.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_open_addressing.py
class HashMapOpenAddressing:\n    \"\"\"Hash table with open addressing\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self.size = 0  # Number of key-value pairs\n        self.capacity = 4  # Hash table capacity\n        self.load_thres = 2.0 / 3.0  # Load factor threshold for triggering expansion\n        self.extend_ratio = 2  # Expansion multiplier\n        self.buckets: list[Pair | None] = [None] * self.capacity  # Bucket array\n        self.TOMBSTONE = Pair(-1, \"-1\")  # Removal marker\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"Load factor\"\"\"\n        return self.size / self.capacity\n\n    def find_bucket(self, key: int) -> int:\n        \"\"\"Search for bucket index corresponding to key\"\"\"\n        index = self.hash_func(key)\n        first_tombstone = -1\n        # Linear probing, break when encountering an empty bucket\n        while self.buckets[index] is not None:\n            # If key is encountered, return the corresponding bucket index\n            if self.buckets[index].key == key:\n                # If a removal marker was encountered before, move the key-value pair to that index\n                if first_tombstone != -1:\n                    self.buckets[first_tombstone] = self.buckets[index]\n                    self.buckets[index] = self.TOMBSTONE\n                    return first_tombstone  # Return the moved bucket index\n                return index  # Return bucket index\n            # Record the first removal marker encountered\n            if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:\n                first_tombstone = index\n            # Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % self.capacity\n        # If key does not exist, return the index for insertion\n        return index if first_tombstone == -1 else first_tombstone\n\n    def get(self, key: int) -> str:\n        \"\"\"Query operation\"\"\"\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, return corresponding val\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            return self.buckets[index].val\n        # If key-value pair does not exist, return None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"Add operation\"\"\"\n        # When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, overwrite val and return\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index].val = val\n            return\n        # If key-value pair does not exist, add the key-value pair\n        self.buckets[index] = Pair(key, val)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"Remove operation\"\"\"\n        # Search for bucket index corresponding to key\n        index = self.find_bucket(key)\n        # If key-value pair is found, overwrite it with removal marker\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index] = self.TOMBSTONE\n            self.size -= 1\n\n    def extend(self):\n        \"\"\"Expand hash table\"\"\"\n        # Temporarily store the original hash table\n        buckets_tmp = self.buckets\n        # Initialize expanded new hash table\n        self.capacity *= self.extend_ratio\n        self.buckets = [None] * self.capacity\n        self.size = 0\n        # Move key-value pairs from original hash table to new hash table\n        for pair in buckets_tmp:\n            if pair not in [None, self.TOMBSTONE]:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"Print hash table\"\"\"\n        for pair in self.buckets:\n            if pair is None:\n                print(\"None\")\n            elif pair is self.TOMBSTONE:\n                print(\"TOMBSTONE\")\n            else:\n                print(pair.key, \"->\", pair.val)\n
hash_map_open_addressing.cpp
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n  private:\n    int size;                             // Number of key-value pairs\n    int capacity = 4;                     // Hash table capacity\n    const double loadThres = 2.0 / 3.0;     // Load factor threshold for triggering expansion\n    const int extendRatio = 2;            // Expansion multiplier\n    vector<Pair *> buckets;               // Bucket array\n    Pair *TOMBSTONE = new Pair(-1, \"-1\"); // Removal marker\n\n  public:\n    /* Constructor */\n    HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {\n    }\n\n    /* Destructor */\n    ~HashMapOpenAddressing() {\n        for (Pair *pair : buckets) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                delete pair;\n            }\n        }\n        delete TOMBSTONE;\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double loadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != nullptr) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index]->key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    string get(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            return buckets[index]->val;\n        }\n        // Return empty string if key-value pair does not exist\n        return \"\";\n    }\n\n    /* Add operation */\n    void put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            buckets[index]->val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            delete buckets[index];\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    void extend() {\n        // Temporarily store the original hash table\n        vector<Pair *> bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = vector<Pair *>(capacity, nullptr);\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (Pair *pair : bucketsTmp) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                put(pair->key, pair->val);\n                delete pair;\n            }\n        }\n    }\n\n    /* Print hash table */\n    void print() {\n        for (Pair *pair : buckets) {\n            if (pair == nullptr) {\n                cout << \"nullptr\" << endl;\n            } else if (pair == TOMBSTONE) {\n                cout << \"TOMBSTONE\" << endl;\n            } else {\n                cout << pair->key << \" -> \" << pair->val << endl;\n            }\n        }\n    }\n};\n
hash_map_open_addressing.java
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private int size; // Number of key-value pairs\n    private int capacity = 4; // Hash table capacity\n    private final double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n    private final int extendRatio = 2; // Expansion multiplier\n    private Pair[] buckets; // Bucket array\n    private final Pair TOMBSTONE = new Pair(-1, \"-1\"); // Removal marker\n\n    /* Constructor */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Hash function */\n    private int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    private double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    private int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index].key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    public String get(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void put(int key, String val) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    public void remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    private void extend() {\n        // Temporarily store the original hash table\n        Pair[] bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (Pair pair : bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    public void print() {\n        for (Pair pair : buckets) {\n            if (pair == null) {\n                System.out.println(\"null\");\n            } else if (pair == TOMBSTONE) {\n                System.out.println(\"TOMBSTONE\");\n            } else {\n                System.out.println(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.cs
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    int size; // Number of key-value pairs\n    int capacity = 4; // Hash table capacity\n    double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n    int extendRatio = 2; // Expansion multiplier\n    Pair[] buckets; // Bucket array\n    Pair TOMBSTONE = new(-1, \"-1\"); // Removal marker\n\n    /* Constructor */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* Hash function */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* Load factor */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    int FindBucket(int key) {\n        int index = HashFunc(key);\n        int firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index].key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    public string? Get(int key) {\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    public void Put(int key, string val) {\n        // When load factor exceeds threshold, perform expansion\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* Remove operation */\n    public void Remove(int key) {\n        // Search for bucket index corresponding to key\n        int index = FindBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* Expand hash table */\n    void Extend() {\n        // Temporarily store the original hash table\n        Pair[] bucketsTmp = buckets;\n        // Initialize expanded new hash table\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        foreach (Pair pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    public void Print() {\n        foreach (Pair pair in buckets) {\n            if (pair == null) {\n                Console.WriteLine(\"null\");\n            } else if (pair == TOMBSTONE) {\n                Console.WriteLine(\"TOMBSTONE\");\n            } else {\n                Console.WriteLine(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.go
/* Hash table with open addressing */\ntype hashMapOpenAddressing struct {\n    size        int     // Number of key-value pairs\n    capacity    int     // Hash table capacity\n    loadThres   float64 // Load factor threshold for triggering expansion\n    extendRatio int     // Expansion multiplier\n    buckets     []*pair // Bucket array\n    TOMBSTONE   *pair   // Removal marker\n}\n\n/* Constructor */\nfunc newHashMapOpenAddressing() *hashMapOpenAddressing {\n    return &hashMapOpenAddressing{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     make([]*pair, 4),\n        TOMBSTONE:   &pair{-1, \"-1\"},\n    }\n}\n\n/* Hash function */\nfunc (h *hashMapOpenAddressing) hashFunc(key int) int {\n    return key % h.capacity // Calculate hash value based on key\n}\n\n/* Load factor */\nfunc (h *hashMapOpenAddressing) loadFactor() float64 {\n    return float64(h.size) / float64(h.capacity) // Calculate current load factor\n}\n\n/* Search for bucket index corresponding to key */\nfunc (h *hashMapOpenAddressing) findBucket(key int) int {\n    index := h.hashFunc(key) // Get initial index\n    firstTombstone := -1     // Record position of first TOMBSTONE encountered\n    for h.buckets[index] != nil {\n        if h.buckets[index].key == key {\n            if firstTombstone != -1 {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                h.buckets[firstTombstone] = h.buckets[index]\n                h.buckets[index] = h.TOMBSTONE\n                return firstTombstone // Return the moved bucket index\n            }\n            return index // Return found index\n        }\n        if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE {\n            firstTombstone = index // Record position of first deletion marker encountered\n        }\n        index = (index + 1) % h.capacity // Linear probing, wrap around to head if past tail\n    }\n    // If key does not exist, return the index for insertion\n    if firstTombstone != -1 {\n        return firstTombstone\n    }\n    return index\n}\n\n/* Query operation */\nfunc (h *hashMapOpenAddressing) get(key int) string {\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        return h.buckets[index].val // If key-value pair is found, return corresponding val\n    }\n    return \"\" // Return \"\" if key-value pair does not exist\n}\n\n/* Add operation */\nfunc (h *hashMapOpenAddressing) put(key int, val string) {\n    if h.loadFactor() > h.loadThres {\n        h.extend() // When load factor exceeds threshold, perform expansion\n    }\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE {\n        h.buckets[index] = &pair{key, val} // If key-value pair does not exist, add the key-value pair\n        h.size++\n    } else {\n        h.buckets[index].val = val // If key-value pair found, overwrite val\n    }\n}\n\n/* Remove operation */\nfunc (h *hashMapOpenAddressing) remove(key int) {\n    index := h.findBucket(key) // Search for bucket index corresponding to key\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        h.buckets[index] = h.TOMBSTONE // If key-value pair is found, overwrite it with removal marker\n        h.size--\n    }\n}\n\n/* Expand hash table */\nfunc (h *hashMapOpenAddressing) extend() {\n    oldBuckets := h.buckets               // Temporarily store the original hash table\n    h.capacity *= h.extendRatio           // Update capacity\n    h.buckets = make([]*pair, h.capacity) // Initialize expanded new hash table\n    h.size = 0                            // Reset size\n    // Move key-value pairs from original hash table to new hash table\n    for _, pair := range oldBuckets {\n        if pair != nil && pair != h.TOMBSTONE {\n            h.put(pair.key, pair.val)\n        }\n    }\n}\n\n/* Print hash table */\nfunc (h *hashMapOpenAddressing) print() {\n    for _, pair := range h.buckets {\n        if pair == nil {\n            fmt.Println(\"nil\")\n        } else if pair == h.TOMBSTONE {\n            fmt.Println(\"TOMBSTONE\")\n        } else {\n            fmt.Printf(\"%d -> %s\\n\", pair.key, pair.val)\n        }\n    }\n}\n
hash_map_open_addressing.swift
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    var size: Int // Number of key-value pairs\n    var capacity: Int // Hash table capacity\n    var loadThres: Double // Load factor threshold for triggering expansion\n    var extendRatio: Int // Expansion multiplier\n    var buckets: [Pair?] // Bucket array\n    var TOMBSTONE: Pair // Removal marker\n\n    /* Constructor */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: nil, count: capacity)\n        TOMBSTONE = Pair(key: -1, val: \"-1\")\n    }\n\n    /* Hash function */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* Load factor */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* Search for bucket index corresponding to key */\n    func findBucket(key: Int) -> Int {\n        var index = hashFunc(key: key)\n        var firstTombstone = -1\n        // Linear probing, break when encountering an empty bucket\n        while buckets[index] != nil {\n            // If key is encountered, return the corresponding bucket index\n            if buckets[index]!.key == key {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if firstTombstone != -1 {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Return the moved bucket index\n                }\n                return index // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if firstTombstone == -1 && buckets[index] == TOMBSTONE {\n                firstTombstone = index\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone == -1 ? index : firstTombstone\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, return corresponding val\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            return buckets[index]!.val\n        }\n        // If key-value pair does not exist, return null\n        return nil\n    }\n\n    /* Add operation */\n    func put(key: Int, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if loadFactor() > loadThres {\n            extend()\n        }\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, overwrite val and return\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index]!.val = val\n            return\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = Pair(key: key, val: val)\n        size += 1\n    }\n\n    /* Remove operation */\n    func remove(key: Int) {\n        // Search for bucket index corresponding to key\n        let index = findBucket(key: key)\n        // If key-value pair is found, overwrite it with removal marker\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index] = TOMBSTONE\n            size -= 1\n        }\n    }\n\n    /* Expand hash table */\n    func extend() {\n        // Temporarily store the original hash table\n        let bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = Array(repeating: nil, count: capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for pair in bucketsTmp {\n            if let pair, pair != TOMBSTONE {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* Print hash table */\n    func print() {\n        for pair in buckets {\n            if pair == nil {\n                Swift.print(\"null\")\n            } else if pair == TOMBSTONE {\n                Swift.print(\"TOMBSTONE\")\n            } else {\n                Swift.print(\"\\(pair!.key) -> \\(pair!.val)\")\n            }\n        }\n    }\n}\n
hash_map_open_addressing.js
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    #size; // Number of key-value pairs\n    #capacity; // Hash table capacity\n    #loadThres; // Load factor threshold for triggering expansion\n    #extendRatio; // Expansion multiplier\n    #buckets; // Bucket array\n    #TOMBSTONE; // Removal marker\n\n    /* Constructor */\n    constructor() {\n        this.#size = 0; // Number of key-value pairs\n        this.#capacity = 4; // Hash table capacity\n        this.#loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n        this.#extendRatio = 2; // Expansion multiplier\n        this.#buckets = Array(this.#capacity).fill(null); // Bucket array\n        this.#TOMBSTONE = new Pair(-1, '-1'); // Removal marker\n    }\n\n    /* Hash function */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* Load factor */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    #findBucket(key) {\n        let index = this.#hashFunc(key);\n        let firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (this.#buckets[index] !== null) {\n            // If key is encountered, return the corresponding bucket index\n            if (this.#buckets[index].key === key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone !== -1) {\n                    this.#buckets[firstTombstone] = this.#buckets[index];\n                    this.#buckets[index] = this.#TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (\n                firstTombstone === -1 &&\n                this.#buckets[index] === this.#TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % this.#capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    get(key) {\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            return this.#buckets[index].val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key, val) {\n        // When load factor exceeds threshold, perform expansion\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index].val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        this.#buckets[index] = new Pair(key, val);\n        this.#size++;\n    }\n\n    /* Remove operation */\n    remove(key) {\n        // Search for bucket index corresponding to key\n        const index = this.#findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index] = this.#TOMBSTONE;\n            this.#size--;\n        }\n    }\n\n    /* Expand hash table */\n    #extend() {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.#buckets;\n        // Initialize expanded new hash table\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = Array(this.#capacity).fill(null);\n        this.#size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.#TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    print() {\n        for (const pair of this.#buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.#TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.ts
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private size: number; // Number of key-value pairs\n    private capacity: number; // Hash table capacity\n    private loadThres: number; // Load factor threshold for triggering expansion\n    private extendRatio: number; // Expansion multiplier\n    private buckets: Array<Pair | null>; // Bucket array\n    private TOMBSTONE: Pair; // Removal marker\n\n    /* Constructor */\n    constructor() {\n        this.size = 0; // Number of key-value pairs\n        this.capacity = 4; // Hash table capacity\n        this.loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n        this.extendRatio = 2; // Expansion multiplier\n        this.buckets = Array(this.capacity).fill(null); // Bucket array\n        this.TOMBSTONE = new Pair(-1, '-1'); // Removal marker\n    }\n\n    /* Hash function */\n    private hashFunc(key: number): number {\n        return key % this.capacity;\n    }\n\n    /* Load factor */\n    private loadFactor(): number {\n        return this.size / this.capacity;\n    }\n\n    /* Search for bucket index corresponding to key */\n    private findBucket(key: number): number {\n        let index = this.hashFunc(key);\n        let firstTombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while (this.buckets[index] !== null) {\n            // If key is encountered, return the corresponding bucket index\n            if (this.buckets[index]!.key === key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone !== -1) {\n                    this.buckets[firstTombstone] = this.buckets[index];\n                    this.buckets[index] = this.TOMBSTONE;\n                    return firstTombstone; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (\n                firstTombstone === -1 &&\n                this.buckets[index] === this.TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % this.capacity;\n        }\n        // If key does not exist, return the index for insertion\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* Query operation */\n    get(key: number): string | null {\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, return corresponding val\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            return this.buckets[index]!.val;\n        }\n        // If key-value pair does not exist, return null\n        return null;\n    }\n\n    /* Add operation */\n    put(key: number, val: string): void {\n        // When load factor exceeds threshold, perform expansion\n        if (this.loadFactor() > this.loadThres) {\n            this.extend();\n        }\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, overwrite val and return\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index]!.val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        this.buckets[index] = new Pair(key, val);\n        this.size++;\n    }\n\n    /* Remove operation */\n    remove(key: number): void {\n        // Search for bucket index corresponding to key\n        const index = this.findBucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index] = this.TOMBSTONE;\n            this.size--;\n        }\n    }\n\n    /* Expand hash table */\n    private extend(): void {\n        // Temporarily store the original hash table\n        const bucketsTmp = this.buckets;\n        // Initialize expanded new hash table\n        this.capacity *= this.extendRatio;\n        this.buckets = Array(this.capacity).fill(null);\n        this.size = 0;\n        // Move key-value pairs from original hash table to new hash table\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* Print hash table */\n    print(): void {\n        for (const pair of this.buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.dart
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n  late int _size; // Number of key-value pairs\n  int _capacity = 4; // Hash table capacity\n  double _loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion\n  int _extendRatio = 2; // Expansion multiplier\n  late List<Pair?> _buckets; // Bucket array\n  Pair _TOMBSTONE = Pair(-1, \"-1\"); // Removal marker\n\n  /* Constructor */\n  HashMapOpenAddressing() {\n    _size = 0;\n    _buckets = List.generate(_capacity, (index) => null);\n  }\n\n  /* Hash function */\n  int hashFunc(int key) {\n    return key % _capacity;\n  }\n\n  /* Load factor */\n  double loadFactor() {\n    return _size / _capacity;\n  }\n\n  /* Search for bucket index corresponding to key */\n  int findBucket(int key) {\n    int index = hashFunc(key);\n    int firstTombstone = -1;\n    // Linear probing, break when encountering an empty bucket\n    while (_buckets[index] != null) {\n      // If key is encountered, return the corresponding bucket index\n      if (_buckets[index]!.key == key) {\n        // If a removal marker was encountered before, move the key-value pair to that index\n        if (firstTombstone != -1) {\n          _buckets[firstTombstone] = _buckets[index];\n          _buckets[index] = _TOMBSTONE;\n          return firstTombstone; // Return the moved bucket index\n        }\n        return index; // Return bucket index\n      }\n      // Record the first removal marker encountered\n      if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) {\n        firstTombstone = index;\n      }\n      // Calculate bucket index, wrap around to the head if past the tail\n      index = (index + 1) % _capacity;\n    }\n    // If key does not exist, return the index for insertion\n    return firstTombstone == -1 ? index : firstTombstone;\n  }\n\n  /* Query operation */\n  String? get(int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, return corresponding val\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      return _buckets[index]!.val;\n    }\n    // If key-value pair does not exist, return null\n    return null;\n  }\n\n  /* Add operation */\n  void put(int key, String val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor() > _loadThres) {\n      extend();\n    }\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, overwrite val and return\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index]!.val = val;\n      return;\n    }\n    // If key-value pair does not exist, add the key-value pair\n    _buckets[index] = new Pair(key, val);\n    _size++;\n  }\n\n  /* Remove operation */\n  void remove(int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(key);\n    // If key-value pair is found, overwrite it with removal marker\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index] = _TOMBSTONE;\n      _size--;\n    }\n  }\n\n  /* Expand hash table */\n  void extend() {\n    // Temporarily store the original hash table\n    List<Pair?> bucketsTmp = _buckets;\n    // Initialize expanded new hash table\n    _capacity *= _extendRatio;\n    _buckets = List.generate(_capacity, (index) => null);\n    _size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (Pair? pair in bucketsTmp) {\n      if (pair != null && pair != _TOMBSTONE) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* Print hash table */\n  void printHashMap() {\n    for (Pair? pair in _buckets) {\n      if (pair == null) {\n        print(\"null\");\n      } else if (pair == _TOMBSTONE) {\n        print(\"TOMBSTONE\");\n      } else {\n        print(\"${pair.key} -> ${pair.val}\");\n      }\n    }\n  }\n}\n
hash_map_open_addressing.rs
/* Hash table with open addressing */\nstruct HashMapOpenAddressing {\n    size: usize,                // Number of key-value pairs\n    capacity: usize,            // Hash table capacity\n    load_thres: f64,            // Load factor threshold for triggering expansion\n    extend_ratio: usize,        // Expansion multiplier\n    buckets: Vec<Option<Pair>>, // Bucket array\n    TOMBSTONE: Option<Pair>,    // Removal marker\n}\n\nimpl HashMapOpenAddressing {\n    /* Constructor */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![None; 4],\n            TOMBSTONE: Some(Pair {\n                key: -1,\n                val: \"-1\".to_string(),\n            }),\n        }\n    }\n\n    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        (key % self.capacity as i32) as usize\n    }\n\n    /* Load factor */\n    fn load_factor(&self) -> f64 {\n        self.size as f64 / self.capacity as f64\n    }\n\n    /* Search for bucket index corresponding to key */\n    fn find_bucket(&mut self, key: i32) -> usize {\n        let mut index = self.hash_func(key);\n        let mut first_tombstone = -1;\n        // Linear probing, break when encountering an empty bucket\n        while self.buckets[index].is_some() {\n            // If key is found, return corresponding bucket index\n            if self.buckets[index].as_ref().unwrap().key == key {\n                // If deletion marker was encountered before, move key-value pair to that index\n                if first_tombstone != -1 {\n                    self.buckets[first_tombstone as usize] = self.buckets[index].take();\n                    self.buckets[index] = self.TOMBSTONE.clone();\n                    return first_tombstone as usize; // Return the moved bucket index\n                }\n                return index; // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE {\n                first_tombstone = index as i32;\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % self.capacity;\n        }\n        // If key does not exist, return the index for insertion\n        if first_tombstone == -1 {\n            index\n        } else {\n            first_tombstone as usize\n        }\n    }\n\n    /* Query operation */\n    fn get(&mut self, key: i32) -> Option<&str> {\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, return corresponding val\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            return self.buckets[index].as_ref().map(|pair| &pair.val as &str);\n        }\n        // If key-value pair does not exist, return null\n        None\n    }\n\n    /* Add operation */\n    fn put(&mut self, key: i32, val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, overwrite val and return\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index].as_mut().unwrap().val = val;\n            return;\n        }\n        // If key-value pair does not exist, add the key-value pair\n        self.buckets[index] = Some(Pair { key, val });\n        self.size += 1;\n    }\n\n    /* Remove operation */\n    fn remove(&mut self, key: i32) {\n        // Search for bucket index corresponding to key\n        let index = self.find_bucket(key);\n        // If key-value pair is found, overwrite it with removal marker\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index] = self.TOMBSTONE.clone();\n            self.size -= 1;\n        }\n    }\n\n    /* Expand hash table */\n    fn extend(&mut self) {\n        // Temporarily store the original hash table\n        let buckets_tmp = self.buckets.clone();\n        // Initialize expanded new hash table\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![None; self.capacity];\n        self.size = 0;\n\n        // Move key-value pairs from original hash table to new hash table\n        for pair in buckets_tmp {\n            if pair.is_none() || pair == self.TOMBSTONE {\n                continue;\n            }\n            let pair = pair.unwrap();\n\n            self.put(pair.key, pair.val);\n        }\n    }\n    /* Print hash table */\n    fn print(&self) {\n        for pair in &self.buckets {\n            if pair.is_none() {\n                println!(\"null\");\n            } else if pair == &self.TOMBSTONE {\n                println!(\"TOMBSTONE\");\n            } else {\n                let pair = pair.as_ref().unwrap();\n                println!(\"{} -> {}\", pair.key, pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.c
/* Hash table with open addressing */\ntypedef struct {\n    int size;         // Number of key-value pairs\n    int capacity;     // Hash table capacity\n    double loadThres; // Load factor threshold for triggering expansion\n    int extendRatio;  // Expansion multiplier\n    Pair **buckets;   // Bucket array\n    Pair *TOMBSTONE;  // Removal marker\n} HashMapOpenAddressing;\n\n/* Constructor */\nHashMapOpenAddressing *newHashMapOpenAddressing() {\n    HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->TOMBSTONE = (Pair *)malloc(sizeof(Pair));\n    hashMap->TOMBSTONE->key = -1;\n    hashMap->TOMBSTONE->val = \"-1\";\n\n    return hashMap;\n}\n\n/* Destructor */\nvoid delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap->TOMBSTONE);\n    free(hashMap);\n}\n\n/* Hash function */\nint hashFunc(HashMapOpenAddressing *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* Load factor */\ndouble loadFactor(HashMapOpenAddressing *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* Search for bucket index corresponding to key */\nint findBucket(HashMapOpenAddressing *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    int firstTombstone = -1;\n    // Linear probing, break when encountering an empty bucket\n    while (hashMap->buckets[index] != NULL) {\n        // If key is encountered, return the corresponding bucket index\n        if (hashMap->buckets[index]->key == key) {\n            // If a removal marker was encountered before, move the key-value pair to that index\n            if (firstTombstone != -1) {\n                hashMap->buckets[firstTombstone] = hashMap->buckets[index];\n                hashMap->buckets[index] = hashMap->TOMBSTONE;\n                return firstTombstone; // Return the moved bucket index\n            }\n            return index; // Return bucket index\n        }\n        // Record the first removal marker encountered\n        if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) {\n            firstTombstone = index;\n        }\n        // Calculate bucket index, wrap around to the head if past the tail\n        index = (index + 1) % hashMap->capacity;\n    }\n    // If key does not exist, return the index for insertion\n    return firstTombstone == -1 ? index : firstTombstone;\n}\n\n/* Query operation */\nchar *get(HashMapOpenAddressing *hashMap, int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, return corresponding val\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        return hashMap->buckets[index]->val;\n    }\n    // Return empty string if key-value pair does not exist\n    return \"\";\n}\n\n/* Add operation */\nvoid put(HashMapOpenAddressing *hashMap, int key, char *val) {\n    // When load factor exceeds threshold, perform expansion\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, overwrite val and return\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        free(hashMap->buckets[index]->val);\n        hashMap->buckets[index]->val = (char *)malloc(sizeof(strlen(val) + 1));\n        strcpy(hashMap->buckets[index]->val, val);\n        hashMap->buckets[index]->val[strlen(val)] = '\\0';\n        return;\n    }\n    // If key-value pair does not exist, add the key-value pair\n    Pair *pair = (Pair *)malloc(sizeof(Pair));\n    pair->key = key;\n    pair->val = (char *)malloc(sizeof(strlen(val) + 1));\n    strcpy(pair->val, val);\n    pair->val[strlen(val)] = '\\0';\n\n    hashMap->buckets[index] = pair;\n    hashMap->size++;\n}\n\n/* Remove operation */\nvoid removeItem(HashMapOpenAddressing *hashMap, int key) {\n    // Search for bucket index corresponding to key\n    int index = findBucket(hashMap, key);\n    // If key-value pair is found, overwrite it with removal marker\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        Pair *pair = hashMap->buckets[index];\n        free(pair->val);\n        free(pair);\n        hashMap->buckets[index] = hashMap->TOMBSTONE;\n        hashMap->size--;\n    }\n}\n\n/* Expand hash table */\nvoid extend(HashMapOpenAddressing *hashMap) {\n    // Temporarily store the original hash table\n    Pair **bucketsTmp = hashMap->buckets;\n    int oldCapacity = hashMap->capacity;\n    // Initialize expanded new hash table\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->size = 0;\n    // Move key-value pairs from original hash table to new hash table\n    for (int i = 0; i < oldCapacity; i++) {\n        Pair *pair = bucketsTmp[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            put(hashMap, pair->key, pair->val);\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(bucketsTmp);\n}\n\n/* Print hash table */\nvoid print(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair == NULL) {\n            printf(\"NULL\\n\");\n        } else if (pair == hashMap->TOMBSTONE) {\n            printf(\"TOMBSTONE\\n\");\n        } else {\n            printf(\"%d -> %s\\n\", pair->key, pair->val);\n        }\n    }\n}\n
hash_map_open_addressing.kt
/* Hash table with open addressing */\nclass HashMapOpenAddressing {\n    private var size: Int               // Number of key-value pairs\n    private var capacity: Int           // Hash table capacity\n    private val loadThres: Double       // Load factor threshold for triggering expansion\n    private val extendRatio: Int        // Expansion multiplier\n    private var buckets: Array<Pair?>   // Bucket array\n    private val TOMBSTONE: Pair         // Removal marker\n\n    /* Constructor */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = arrayOfNulls(capacity)\n        TOMBSTONE = Pair(-1, \"-1\")\n    }\n\n    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* Load factor */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* Search for bucket index corresponding to key */\n    fun findBucket(key: Int): Int {\n        var index = hashFunc(key)\n        var firstTombstone = -1\n        // Linear probing, break when encountering an empty bucket\n        while (buckets[index] != null) {\n            // If key is encountered, return the corresponding bucket index\n            if (buckets[index]?.key == key) {\n                // If a removal marker was encountered before, move the key-value pair to that index\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // Return the moved bucket index\n                }\n                return index // Return bucket index\n            }\n            // Record the first removal marker encountered\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index\n            }\n            // Calculate bucket index, wrap around to the head if past the tail\n            index = (index + 1) % capacity\n        }\n        // If key does not exist, return the index for insertion\n        return if (firstTombstone == -1) index else firstTombstone\n    }\n\n    /* Query operation */\n    fun get(key: Int): String? {\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, return corresponding val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index]?._val\n        }\n        // If key-value pair does not exist, return null\n        return null\n    }\n\n    /* Add operation */\n    fun put(key: Int, _val: String) {\n        // When load factor exceeds threshold, perform expansion\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, overwrite val and return\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index]!!._val = _val\n            return\n        }\n        // If key-value pair does not exist, add the key-value pair\n        buckets[index] = Pair(key, _val)\n        size++\n    }\n\n    /* Remove operation */\n    fun remove(key: Int) {\n        // Search for bucket index corresponding to key\n        val index = findBucket(key)\n        // If key-value pair is found, overwrite it with removal marker\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE\n            size--\n        }\n    }\n\n    /* Expand hash table */\n    fun extend() {\n        // Temporarily store the original hash table\n        val bucketsTmp = buckets\n        // Initialize expanded new hash table\n        capacity *= extendRatio\n        buckets = arrayOfNulls(capacity)\n        size = 0\n        // Move key-value pairs from original hash table to new hash table\n        for (pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* Print hash table */\n    fun print() {\n        for (pair in buckets) {\n            if (pair == null) {\n                println(\"null\")\n            } else if (pair == TOMBSTONE) {\n                println(\"TOMESTOME\")\n            } else {\n                println(\"${pair.key} -> ${pair._val}\")\n            }\n        }\n    }\n}\n
hash_map_open_addressing.rb
### Hash map with open addressing ###\nclass HashMapOpenAddressing\n  TOMBSTONE = Pair.new(-1, '-1') # Removal marker\n\n  ### Constructor ###\n  def initialize\n    @size = 0 # Number of key-value pairs\n    @capacity = 4 # Hash table capacity\n    @load_thres = 2.0 / 3.0 # Load factor threshold for triggering expansion\n    @extend_ratio = 2 # Expansion multiplier\n    @buckets = Array.new(@capacity) # Bucket array\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### Load factor ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### Search bucket index for key ###\n  def find_bucket(key)\n    index = hash_func(key)\n    first_tombstone = -1\n    # Linear probing, break when encountering an empty bucket\n    while !@buckets[index].nil?\n      # If key is encountered, return the corresponding bucket index\n      if @buckets[index].key == key\n        # If a removal marker was encountered before, move the key-value pair to that index\n        if first_tombstone != -1\n          @buckets[first_tombstone] = @buckets[index]\n          @buckets[index] = TOMBSTONE\n          return first_tombstone # Return the moved bucket index\n        end\n        return index # Return bucket index\n      end\n      # Record the first removal marker encountered\n      first_tombstone = index if first_tombstone == -1 && @buckets[index] == TOMBSTONE\n      # Calculate bucket index, wrap around to the head if past the tail\n      index = (index + 1) % @capacity\n    end\n    # If key does not exist, return the index for insertion\n    first_tombstone == -1 ? index : first_tombstone\n  end\n\n  ### Query operation ###\n  def get(key)\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair is found, return corresponding val\n    return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index])\n    # Return nil if key-value pair does not exist\n    nil\n  end\n\n  ### Add operation ###\n  def put(key, val)\n    # When load factor exceeds threshold, perform expansion\n    extend if load_factor > @load_thres\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair found, overwrite val and return\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index].val = val\n      return\n    end\n    # If key-value pair does not exist, add the key-value pair\n    @buckets[index] = Pair.new(key, val)\n    @size += 1\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    # Search for bucket index corresponding to key\n    index = find_bucket(key)\n    # If key-value pair is found, overwrite it with removal marker\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index] = TOMBSTONE\n      @size -= 1\n    end\n  end\n\n  ### Expand hash table ###\n  def extend\n    # Temporarily store the original hash table\n    buckets_tmp = @buckets\n    # Initialize expanded new hash table\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity)\n    @size = 0\n    # Move key-value pairs from original hash table to new hash table\n    for pair in buckets_tmp\n      put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair)\n    end\n  end\n\n  ### Print hash table ###\n  def print\n    for pair in @buckets\n      if pair.nil?\n        puts \"Nil\"\n      elsif pair == TOMBSTONE\n        puts \"TOMBSTONE\"\n      else\n        puts \"#{pair.key} -> #{pair.val}\"\n      end\n    end\n  end\nend\n
","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#2-quadratic-probing","level":3,"title":"2.   Quadratic Probing","text":"

Quadratic probing is similar to linear probing and is one of the common strategies for open addressing. When a collision occurs, quadratic probing does not simply skip a fixed number of steps but skips a number of steps equal to the \"square of the number of probes\", i.e., \\(1, 4, 9, \\dots\\) steps.

Quadratic probing has the following advantages:

  • Quadratic probing attempts to alleviate the clustering effect of linear probing by skipping distances equal to the square of the probe count.
  • Quadratic probing skips larger distances to find empty positions, which helps to distribute data more evenly.

However, quadratic probing is not perfect:

  • Clustering still exists, i.e., some positions are more likely to be occupied than others.
  • Due to the growth of squares, quadratic probing may not probe the entire hash table, meaning that even if there are empty buckets in the hash table, quadratic probing may not be able to access them.
","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#3-double-hashing","level":3,"title":"3.   Double Hashing","text":"

As the name suggests, the double hashing method uses multiple hash functions \\(f_1(x)\\), \\(f_2(x)\\), \\(f_3(x)\\), \\(\\dots\\) for probing.

  • Inserting elements: If hash function \\(f_1(x)\\) encounters a conflict, try \\(f_2(x)\\), and so on, until an empty position is found and the element is inserted.
  • Searching for elements: Search in the same order of hash functions until the target element is found and return it; if an empty position is encountered or all hash functions have been tried, it indicates the element is not in the hash table, then return None.

Compared to linear probing, the double hashing method is less prone to clustering, but multiple hash functions introduce additional computational overhead.

Tip

Please note that open addressing (linear probing, quadratic probing, and double hashing) hash tables all have the problem of \"cannot directly delete elements\".

","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_collision/#623-choice-of-programming-languages","level":2,"title":"6.2.3   Choice of Programming Languages","text":"

Different programming languages adopt different hash table implementation strategies. Here are a few examples:

  • Python uses open addressing. The dict dictionary uses pseudo-random numbers for probing.
  • Java uses separate chaining. Since JDK 1.8, when the array length in HashMap reaches 64 and the length of a linked list reaches 8, the linked list is converted to a red-black tree to improve search performance.
  • Go uses separate chaining. Go stipulates that each bucket can store up to 8 key-value pairs, and if the capacity is exceeded, an overflow bucket is linked; when there are too many overflow buckets, a special equal-capacity expansion operation is performed to ensure performance.
","path":["Chapter 6. Hashing","6.2   Hash Collision"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   Hash Table","text":"

A hash table, also known as a hash map, establishes a mapping between keys key and values value, enabling efficient element retrieval. Specifically, when we input a key key into a hash table, we can retrieve the corresponding value value in \\(O(1)\\) time.

As shown in Figure 6-1, given \\(n\\) students, each with two pieces of data: \"name\" and \"student ID\". If we want to implement a query function that \"inputs a student ID and returns the corresponding name\", we can use the hash table shown below.

Figure 6-1   Abstract representation of a hash table

In addition to hash tables, arrays and linked lists can also implement query functionality. Their efficiency comparison is shown in the following table.

  • Adding elements: Simply add elements to the end of the array (linked list), using \\(O(1)\\) time.
  • Querying elements: Since the array (linked list) is unordered, all elements need to be traversed, using \\(O(n)\\) time.
  • Deleting elements: The element must first be located, then deleted from the array (linked list), using \\(O(n)\\) time.

Table 6-1   Comparison of element query efficiency

Array Linked List Hash Table Find element \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) Add element \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) Delete element \\(O(n)\\) \\(O(n)\\) \\(O(1)\\)

As observed, the time complexity for insertion, deletion, search, and modification operations in a hash table is \\(O(1)\\), which is very efficient.

","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#611-common-hash-table-operations","level":2,"title":"6.1.1   Common Hash Table Operations","text":"

Common operations on hash tables include: initialization, query operations, adding key-value pairs, and deleting key-value pairs. Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
# Initialize hash table\nhmap: dict = {}\n\n# Add operation\n# Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n# Query operation\n# Input key into hash table to get value\nname: str = hmap[15937]\n\n# Delete operation\n# Delete key-value pair (key, value) from hash table\nhmap.pop(10583)\n
hash_map.cpp
/* Initialize hash table */\nunordered_map<int, string> map;\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\";\nmap[15937] = \"XiaoLuo\";\nmap[16750] = \"XiaoSuan\";\nmap[13276] = \"XiaoFa\";\nmap[10583] = \"XiaoYa\";\n\n/* Query operation */\n// Input key into hash table to get value\nstring name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.erase(10583);\n
hash_map.java
/* Initialize hash table */\nMap<Integer, String> map = new HashMap<>();\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.put(12836, \"XiaoHa\");\nmap.put(15937, \"XiaoLuo\");\nmap.put(16750, \"XiaoSuan\");\nmap.put(13276, \"XiaoFa\");\nmap.put(10583, \"XiaoYa\");\n\n/* Query operation */\n// Input key into hash table to get value\nString name = map.get(15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583);\n
hash_map.cs
/* Initialize hash table */\nDictionary<int, string> map = new() {\n    /* Add operation */\n    // Add key-value pair (key, value) to hash table\n    { 12836, \"XiaoHa\" },\n    { 15937, \"XiaoLuo\" },\n    { 16750, \"XiaoSuan\" },\n    { 13276, \"XiaoFa\" },\n    { 10583, \"XiaoYa\" }\n};\n\n/* Query operation */\n// Input key into hash table to get value\nstring name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.Remove(10583);\n
hash_map_test.go
/* Initialize hash table */\nhmap := make(map[int]string)\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nname := hmap[15937]\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\ndelete(hmap, 10583)\n
hash_map.swift
/* Initialize hash table */\nvar map: [Int: String] = [:]\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\"\nmap[15937] = \"XiaoLuo\"\nmap[16750] = \"XiaoSuan\"\nmap[13276] = \"XiaoFa\"\nmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map[15937]!\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.removeValue(forKey: 10583)\n
hash_map.js
/* Initialize hash table */\nconst map = new Map();\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.set(12836, 'XiaoHa');\nmap.set(15937, 'XiaoLuo');\nmap.set(16750, 'XiaoSuan');\nmap.set(13276, 'XiaoFa');\nmap.set(10583, 'XiaoYa');\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map.get(15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.delete(10583);\n
hash_map.ts
/* Initialize hash table */\nconst map = new Map<number, string>();\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.set(12836, 'XiaoHa');\nmap.set(15937, 'XiaoLuo');\nmap.set(16750, 'XiaoSuan');\nmap.set(13276, 'XiaoFa');\nmap.set(10583, 'XiaoYa');\nconsole.info('\\nAfter adding, hash table is\\nKey -> Value');\nconsole.info(map);\n\n/* Query operation */\n// Input key into hash table to get value\nlet name = map.get(15937);\nconsole.info('\\nInput student ID 15937, queried name ' + name);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.delete(10583);\nconsole.info('\\nAfter deleting 10583, hash table is\\nKey -> Value');\nconsole.info(map);\n
hash_map.dart
/* Initialize hash table */\nMap<int, String> map = {};\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\";\nmap[15937] = \"XiaoLuo\";\nmap[16750] = \"XiaoSuan\";\nmap[13276] = \"XiaoFa\";\nmap[10583] = \"XiaoYa\";\n\n/* Query operation */\n// Input key into hash table to get value\nString name = map[15937];\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583);\n
hash_map.rs
use std::collections::HashMap;\n\n/* Initialize hash table */\nlet mut map: HashMap<i32, String> = HashMap::new();\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap.insert(12836, \"XiaoHa\".to_string());\nmap.insert(15937, \"XiaoLuo\".to_string());\nmap.insert(16750, \"XiaoSuan\".to_string());\nmap.insert(13279, \"XiaoFa\".to_string());\nmap.insert(10583, \"XiaoYa\".to_string());\n\n/* Query operation */\n// Input key into hash table to get value\nlet _name: Option<&String> = map.get(&15937);\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nlet _removed_value: Option<String> = map.remove(&10583);\n
hash_map.c
// C does not provide a built-in hash table\n
hash_map.kt
/* Initialize hash table */\nval map = HashMap<Int,String>()\n\n/* Add operation */\n// Add key-value pair (key, value) to hash table\nmap[12836] = \"XiaoHa\"\nmap[15937] = \"XiaoLuo\"\nmap[16750] = \"XiaoSuan\"\nmap[13276] = \"XiaoFa\"\nmap[10583] = \"XiaoYa\"\n\n/* Query operation */\n// Input key into hash table to get value\nval name = map[15937]\n\n/* Delete operation */\n// Delete key-value pair (key, value) from hash table\nmap.remove(10583)\n
hash_map.rb
# Initialize hash table\nhmap = {}\n\n# Add operation\n# Add key-value pair (key, value) to hash table\nhmap[12836] = \"XiaoHa\"\nhmap[15937] = \"XiaoLuo\"\nhmap[16750] = \"XiaoSuan\"\nhmap[13276] = \"XiaoFa\"\nhmap[10583] = \"XiaoYa\"\n\n# Query operation\n# Input key into hash table to get value\nname = hmap[15937]\n\n# Delete operation\n# Delete key-value pair (key, value) from hash table\nhmap.delete(10583)\n
Visualized Execution

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

There are three common ways to traverse a hash table: traversing key-value pairs, traversing keys, and traversing values. Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
# Traverse hash table\n# Traverse key-value pairs key->value\nfor key, value in hmap.items():\n    print(key, \"->\", value)\n# Traverse keys only\nfor key in hmap.keys():\n    print(key)\n# Traverse values only\nfor value in hmap.values():\n    print(value)\n
hash_map.cpp
/* Traverse hash table */\n// Traverse key-value pairs key->value\nfor (auto kv: map) {\n    cout << kv.first << \" -> \" << kv.second << endl;\n}\n// Traverse using iterator key->value\nfor (auto iter = map.begin(); iter != map.end(); iter++) {\n    cout << iter->first << \"->\" << iter->second << endl;\n}\n
hash_map.java
/* Traverse hash table */\n// Traverse key-value pairs key->value\nfor (Map.Entry<Integer, String> kv: map.entrySet()) {\n    System.out.println(kv.getKey() + \" -> \" + kv.getValue());\n}\n// Traverse keys only\nfor (int key: map.keySet()) {\n    System.out.println(key);\n}\n// Traverse values only\nfor (String val: map.values()) {\n    System.out.println(val);\n}\n
hash_map.cs
/* Traverse hash table */\n// Traverse key-value pairs Key->Value\nforeach (var kv in map) {\n    Console.WriteLine(kv.Key + \" -> \" + kv.Value);\n}\n// Traverse keys only\nforeach (int key in map.Keys) {\n    Console.WriteLine(key);\n}\n// Traverse values only\nforeach (string val in map.Values) {\n    Console.WriteLine(val);\n}\n
hash_map_test.go
/* Traverse hash table */\n// Traverse key-value pairs key->value\nfor key, value := range hmap {\n    fmt.Println(key, \"->\", value)\n}\n// Traverse keys only\nfor key := range hmap {\n    fmt.Println(key)\n}\n// Traverse values only\nfor _, value := range hmap {\n    fmt.Println(value)\n}\n
hash_map.swift
/* Traverse hash table */\n// Traverse key-value pairs Key->Value\nfor (key, value) in map {\n    print(\"\\(key) -> \\(value)\")\n}\n// Traverse keys only\nfor key in map.keys {\n    print(key)\n}\n// Traverse values only\nfor value in map.values {\n    print(value)\n}\n
hash_map.js
/* Traverse hash table */\nconsole.info('\\nTraverse key-value pairs Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nTraverse keys only Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nTraverse values only Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
hash_map.ts
/* Traverse hash table */\nconsole.info('\\nTraverse key-value pairs Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nTraverse keys only Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\nTraverse values only Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
hash_map.dart
/* Traverse hash table */\n// Traverse key-value pairs Key->Value\nmap.forEach((key, value) {\n  print('$key -> $value');\n});\n\n// Traverse keys only\nmap.keys.forEach((key) {\n  print(key);\n});\n\n// Traverse values only\nmap.values.forEach((value) {\n  print(value);\n});\n
hash_map.rs
/* Traverse hash table */\n// Traverse key-value pairs Key->Value\nfor (key, value) in &map {\n    println!(\"{key} -> {value}\");\n}\n\n// Traverse keys only\nfor key in map.keys() {\n    println!(\"{key}\");\n}\n\n// Traverse values only\nfor value in map.values() {\n    println!(\"{value}\");\n}\n
hash_map.c
// C does not provide a built-in hash table\n
hash_map.kt
/* Traverse hash table */\n// Traverse key-value pairs key->value\nfor ((key, value) in map) {\n    println(\"$key -> $value\")\n}\n// Traverse keys only\nfor (key in map.keys) {\n    println(key)\n}\n// Traverse values only\nfor (_val in map.values) {\n    println(_val)\n}\n
hash_map.rb
# Traverse hash table\n# Traverse key-value pairs key->value\nhmap.entries.each { |key, value| puts \"#{key} -> #{value}\" }\n\n# Traverse keys only\nhmap.keys.each { |key| puts key }\n\n# Traverse values only\nhmap.values.each { |val| puts val }\n
Visualized Execution

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#612-simple-hash-table-implementation","level":2,"title":"6.1.2   Simple Hash Table Implementation","text":"

Let's first consider the simplest case: implementing a hash table using only an array. In a hash table, each empty position in the array is called a bucket, and each bucket can store a key-value pair. Therefore, the query operation is to find the bucket corresponding to key and retrieve the value from the bucket.

So how do we locate the corresponding bucket based on key? This is achieved through a hash function. The role of the hash function is to map a larger input space to a smaller output space. In a hash table, the input space is all keys, and the output space is all buckets (array indices). In other words, given a key, we can use the hash function to obtain the storage location of the key-value pair corresponding to that key in the array.

When inputting a key, the hash function's calculation process consists of the following two steps:

  1. Calculate the hash value through a hash algorithm hash().
  2. Take the modulo of the hash value by the number of buckets (array length) capacity to obtain the bucket (array index) index corresponding to that key.
index = hash(key) % capacity\n

Subsequently, we can use index to access the corresponding bucket in the hash table and retrieve the value.

Assuming the array length is capacity = 100 and the hash algorithm is hash(key) = key, the hash function becomes key % 100. Figure 6-2 shows the working principle of the hash function using key as student ID and value as name.

Figure 6-2   Working principle of hash function

The following code implements a simple hash table. Here, we encapsulate key and value into a class Pair to represent a key-value pair.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_hash_map.py
class Pair:\n    \"\"\"Key-value pair\"\"\"\n\n    def __init__(self, key: int, val: str):\n        self.key = key\n        self.val = val\n\nclass ArrayHashMap:\n    \"\"\"Hash table based on array implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        # Initialize array with 100 buckets\n        self.buckets: list[Pair | None] = [None] * 100\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"Hash function\"\"\"\n        index = key % 100\n        return index\n\n    def get(self, key: int) -> str | None:\n        \"\"\"Query operation\"\"\"\n        index: int = self.hash_func(key)\n        pair: Pair = self.buckets[index]\n        if pair is None:\n            return None\n        return pair.val\n\n    def put(self, key: int, val: str):\n        \"\"\"Add and update operation\"\"\"\n        pair = Pair(key, val)\n        index: int = self.hash_func(key)\n        self.buckets[index] = pair\n\n    def remove(self, key: int):\n        \"\"\"Remove operation\"\"\"\n        index: int = self.hash_func(key)\n        # Set to None to represent removal\n        self.buckets[index] = None\n\n    def entry_set(self) -> list[Pair]:\n        \"\"\"Get all key-value pairs\"\"\"\n        result: list[Pair] = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair)\n        return result\n\n    def key_set(self) -> list[int]:\n        \"\"\"Get all keys\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.key)\n        return result\n\n    def value_set(self) -> list[str]:\n        \"\"\"Get all values\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.val)\n        return result\n\n    def print(self):\n        \"\"\"Print hash table\"\"\"\n        for pair in self.buckets:\n            if pair is not None:\n                print(pair.key, \"->\", pair.val)\n
array_hash_map.cpp
/* Key-value pair */\nstruct Pair {\n  public:\n    int key;\n    string val;\n    Pair(int key, string val) {\n        this->key = key;\n        this->val = val;\n    }\n};\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n  private:\n    vector<Pair *> buckets;\n\n  public:\n    ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = vector<Pair *>(100);\n    }\n\n    ~ArrayHashMap() {\n        // Free memory\n        for (const auto &bucket : buckets) {\n            delete bucket;\n        }\n        buckets.clear();\n    }\n\n    /* Hash function */\n    int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\n    string get(int key) {\n        int index = hashFunc(key);\n        Pair *pair = buckets[index];\n        if (pair == nullptr)\n            return \"\";\n        return pair->val;\n    }\n\n    /* Add operation */\n    void put(int key, string val) {\n        Pair *pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* Remove operation */\n    void remove(int key) {\n        int index = hashFunc(key);\n        // Free memory and set to nullptr\n        delete buckets[index];\n        buckets[index] = nullptr;\n    }\n\n    /* Get all key-value pairs */\n    vector<Pair *> pairSet() {\n        vector<Pair *> pairSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                pairSet.push_back(pair);\n            }\n        }\n        return pairSet;\n    }\n\n    /* Get all keys */\n    vector<int> keySet() {\n        vector<int> keySet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                keySet.push_back(pair->key);\n            }\n        }\n        return keySet;\n    }\n\n    /* Get all values */\n    vector<string> valueSet() {\n        vector<string> valueSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                valueSet.push_back(pair->val);\n            }\n        }\n        return valueSet;\n    }\n\n    /* Print hash table */\n    void print() {\n        for (Pair *kv : pairSet()) {\n            cout << kv->key << \" -> \" << kv->val << endl;\n        }\n    }\n};\n
array_hash_map.java
/* Key-value pair */\nclass Pair {\n    public int key;\n    public String val;\n\n    public Pair(int key, String val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private List<Pair> buckets;\n\n    public ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = new ArrayList<>();\n        for (int i = 0; i < 100; i++) {\n            buckets.add(null);\n        }\n    }\n\n    /* Hash function */\n    private int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\n    public String get(int key) {\n        int index = hashFunc(key);\n        Pair pair = buckets.get(index);\n        if (pair == null)\n            return null;\n        return pair.val;\n    }\n\n    /* Add operation */\n    public void put(int key, String val) {\n        Pair pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets.set(index, pair);\n    }\n\n    /* Remove operation */\n    public void remove(int key) {\n        int index = hashFunc(key);\n        // Set to null to represent deletion\n        buckets.set(index, null);\n    }\n\n    /* Get all key-value pairs */\n    public List<Pair> pairSet() {\n        List<Pair> pairSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                pairSet.add(pair);\n        }\n        return pairSet;\n    }\n\n    /* Get all keys */\n    public List<Integer> keySet() {\n        List<Integer> keySet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                keySet.add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* Get all values */\n    public List<String> valueSet() {\n        List<String> valueSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                valueSet.add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* Print hash table */\n    public void print() {\n        for (Pair kv : pairSet()) {\n            System.out.println(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
array_hash_map.cs
/* Key-value pair int->string */\nclass Pair(int key, string val) {\n    public int key = key;\n    public string val = val;\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    List<Pair?> buckets;\n    public ArrayHashMap() {\n        // Initialize array with 100 buckets\n        buckets = [];\n        for (int i = 0; i < 100; i++) {\n            buckets.Add(null);\n        }\n    }\n\n    /* Hash function */\n    int HashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* Query operation */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        Pair? pair = buckets[index];\n        if (pair == null) return null;\n        return pair.val;\n    }\n\n    /* Add operation */\n    public void Put(int key, string val) {\n        Pair pair = new(key, val);\n        int index = HashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* Remove operation */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // Set to null to represent deletion\n        buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\n    public List<Pair> PairSet() {\n        List<Pair> pairSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                pairSet.Add(pair);\n        }\n        return pairSet;\n    }\n\n    /* Get all keys */\n    public List<int> KeySet() {\n        List<int> keySet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                keySet.Add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* Get all values */\n    public List<string> ValueSet() {\n        List<string> valueSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                valueSet.Add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* Print hash table */\n    public void Print() {\n        foreach (Pair kv in PairSet()) {\n            Console.WriteLine(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
array_hash_map.go
/* Key-value pair */\ntype pair struct {\n    key int\n    val string\n}\n\n/* Hash table based on array implementation */\ntype arrayHashMap struct {\n    buckets []*pair\n}\n\n/* Initialize hash table */\nfunc newArrayHashMap() *arrayHashMap {\n    // Initialize array with 100 buckets\n    buckets := make([]*pair, 100)\n    return &arrayHashMap{buckets: buckets}\n}\n\n/* Hash function */\nfunc (a *arrayHashMap) hashFunc(key int) int {\n    index := key % 100\n    return index\n}\n\n/* Query operation */\nfunc (a *arrayHashMap) get(key int) string {\n    index := a.hashFunc(key)\n    pair := a.buckets[index]\n    if pair == nil {\n        return \"Not Found\"\n    }\n    return pair.val\n}\n\n/* Add operation */\nfunc (a *arrayHashMap) put(key int, val string) {\n    pair := &pair{key: key, val: val}\n    index := a.hashFunc(key)\n    a.buckets[index] = pair\n}\n\n/* Remove operation */\nfunc (a *arrayHashMap) remove(key int) {\n    index := a.hashFunc(key)\n    // Set to nil to delete\n    a.buckets[index] = nil\n}\n\n/* Get all key pairs */\nfunc (a *arrayHashMap) pairSet() []*pair {\n    var pairs []*pair\n    for _, pair := range a.buckets {\n        if pair != nil {\n            pairs = append(pairs, pair)\n        }\n    }\n    return pairs\n}\n\n/* Get all keys */\nfunc (a *arrayHashMap) keySet() []int {\n    var keys []int\n    for _, pair := range a.buckets {\n        if pair != nil {\n            keys = append(keys, pair.key)\n        }\n    }\n    return keys\n}\n\n/* Get all values */\nfunc (a *arrayHashMap) valueSet() []string {\n    var values []string\n    for _, pair := range a.buckets {\n        if pair != nil {\n            values = append(values, pair.val)\n        }\n    }\n    return values\n}\n\n/* Print hash table */\nfunc (a *arrayHashMap) print() {\n    for _, pair := range a.buckets {\n        if pair != nil {\n            fmt.Println(pair.key, \"->\", pair.val)\n        }\n    }\n}\n
array_hash_map.swift
/* Key-value pair */\nclass Pair: Equatable {\n    public var key: Int\n    public var val: String\n\n    public init(key: Int, val: String) {\n        self.key = key\n        self.val = val\n    }\n\n    public static func == (lhs: Pair, rhs: Pair) -> Bool {\n        lhs.key == rhs.key && lhs.val == rhs.val\n    }\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private var buckets: [Pair?]\n\n    init() {\n        // Initialize array with 100 buckets\n        buckets = Array(repeating: nil, count: 100)\n    }\n\n    /* Hash function */\n    private func hashFunc(key: Int) -> Int {\n        let index = key % 100\n        return index\n    }\n\n    /* Query operation */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let pair = buckets[index]\n        return pair?.val\n    }\n\n    /* Add operation */\n    func put(key: Int, val: String) {\n        let pair = Pair(key: key, val: val)\n        let index = hashFunc(key: key)\n        buckets[index] = pair\n    }\n\n    /* Remove operation */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        // Set to nil to delete\n        buckets[index] = nil\n    }\n\n    /* Get all key-value pairs */\n    func pairSet() -> [Pair] {\n        buckets.compactMap { $0 }\n    }\n\n    /* Get all keys */\n    func keySet() -> [Int] {\n        buckets.compactMap { $0?.key }\n    }\n\n    /* Get all values */\n    func valueSet() -> [String] {\n        buckets.compactMap { $0?.val }\n    }\n\n    /* Print hash table */\n    func print() {\n        for pair in pairSet() {\n            Swift.print(\"\\(pair.key) -> \\(pair.val)\")\n        }\n    }\n}\n
array_hash_map.js
/* Key-value pair Number -> String */\nclass Pair {\n    constructor(key, val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    #buckets;\n    constructor() {\n        // Initialize array with 100 buckets\n        this.#buckets = new Array(100).fill(null);\n    }\n\n    /* Hash function */\n    #hashFunc(key) {\n        return key % 100;\n    }\n\n    /* Query operation */\n    get(key) {\n        let index = this.#hashFunc(key);\n        let pair = this.#buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* Add operation */\n    set(key, val) {\n        let index = this.#hashFunc(key);\n        this.#buckets[index] = new Pair(key, val);\n    }\n\n    /* Remove operation */\n    delete(key) {\n        let index = this.#hashFunc(key);\n        // Set to null to represent deletion\n        this.#buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\n    entries() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* Get all keys */\n    keys() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* Get all values */\n    values() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* Print hash table */\n    print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
array_hash_map.ts
/* Key-value pair Number -> String */\nclass Pair {\n    public key: number;\n    public val: string;\n\n    constructor(key: number, val: string) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    private readonly buckets: (Pair | null)[];\n\n    constructor() {\n        // Initialize array with 100 buckets\n        this.buckets = new Array(100).fill(null);\n    }\n\n    /* Hash function */\n    private hashFunc(key: number): number {\n        return key % 100;\n    }\n\n    /* Query operation */\n    public get(key: number): string | null {\n        let index = this.hashFunc(key);\n        let pair = this.buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* Add operation */\n    public set(key: number, val: string) {\n        let index = this.hashFunc(key);\n        this.buckets[index] = new Pair(key, val);\n    }\n\n    /* Remove operation */\n    public delete(key: number) {\n        let index = this.hashFunc(key);\n        // Set to null to represent deletion\n        this.buckets[index] = null;\n    }\n\n    /* Get all key-value pairs */\n    public entries(): (Pair | null)[] {\n        let arr: (Pair | null)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* Get all keys */\n    public keys(): (number | undefined)[] {\n        let arr: (number | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* Get all values */\n    public values(): (string | undefined)[] {\n        let arr: (string | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* Print hash table */\n    public print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
array_hash_map.dart
/* Key-value pair */\nclass Pair {\n  int key;\n  String val;\n  Pair(this.key, this.val);\n}\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n  late List<Pair?> _buckets;\n\n  ArrayHashMap() {\n    // Initialize array with 100 buckets\n    _buckets = List.filled(100, null);\n  }\n\n  /* Hash function */\n  int _hashFunc(int key) {\n    final int index = key % 100;\n    return index;\n  }\n\n  /* Query operation */\n  String? get(int key) {\n    final int index = _hashFunc(key);\n    final Pair? pair = _buckets[index];\n    if (pair == null) {\n      return null;\n    }\n    return pair.val;\n  }\n\n  /* Add operation */\n  void put(int key, String val) {\n    final Pair pair = Pair(key, val);\n    final int index = _hashFunc(key);\n    _buckets[index] = pair;\n  }\n\n  /* Remove operation */\n  void remove(int key) {\n    final int index = _hashFunc(key);\n    _buckets[index] = null;\n  }\n\n  /* Get all key-value pairs */\n  List<Pair> pairSet() {\n    List<Pair> pairSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        pairSet.add(pair);\n      }\n    }\n    return pairSet;\n  }\n\n  /* Get all keys */\n  List<int> keySet() {\n    List<int> keySet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        keySet.add(pair.key);\n      }\n    }\n    return keySet;\n  }\n\n  /* Get all values */\n  List<String> values() {\n    List<String> valueSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        valueSet.add(pair.val);\n      }\n    }\n    return valueSet;\n  }\n\n  /* Print hash table */\n  void printHashMap() {\n    for (final Pair kv in pairSet()) {\n      print(\"${kv.key} -> ${kv.val}\");\n    }\n  }\n}\n
array_hash_map.rs
/* Key-value pair */\n#[derive(Debug, Clone, PartialEq)]\npub struct Pair {\n    pub key: i32,\n    pub val: String,\n}\n\n/* Hash table based on array implementation */\npub struct ArrayHashMap {\n    buckets: Vec<Option<Pair>>,\n}\n\nimpl ArrayHashMap {\n    pub fn new() -> ArrayHashMap {\n        // Initialize array with 100 buckets\n        Self {\n            buckets: vec![None; 100],\n        }\n    }\n\n    /* Hash function */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % 100\n    }\n\n    /* Query operation */\n    pub fn get(&self, key: i32) -> Option<&String> {\n        let index = self.hash_func(key);\n        self.buckets[index].as_ref().map(|pair| &pair.val)\n    }\n\n    /* Add operation */\n    pub fn put(&mut self, key: i32, val: &str) {\n        let index = self.hash_func(key);\n        self.buckets[index] = Some(Pair {\n            key,\n            val: val.to_string(),\n        });\n    }\n\n    /* Remove operation */\n    pub fn remove(&mut self, key: i32) {\n        let index = self.hash_func(key);\n        // Set to None to represent removal\n        self.buckets[index] = None;\n    }\n\n    /* Get all key-value pairs */\n    pub fn entry_set(&self) -> Vec<&Pair> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref())\n            .collect()\n    }\n\n    /* Get all keys */\n    pub fn key_set(&self) -> Vec<&i32> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.key))\n            .collect()\n    }\n\n    /* Get all values */\n    pub fn value_set(&self) -> Vec<&String> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.val))\n            .collect()\n    }\n\n    /* Print hash table */\n    pub fn print(&self) {\n        for pair in self.entry_set() {\n            println!(\"{} -> {}\", pair.key, pair.val);\n        }\n    }\n}\n
array_hash_map.c
/* Key-value pair int->string */\ntypedef struct {\n    int key;\n    char *val;\n} Pair;\n\n/* Hash table based on array implementation */\ntypedef struct {\n    Pair *buckets[MAX_SIZE];\n} ArrayHashMap;\n\n/* Constructor */\nArrayHashMap *newArrayHashMap() {\n    ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap));\n    for (int i=0; i < MAX_SIZE; i++) {\n        hmap->buckets[i] = NULL;\n    }\n    return hmap;\n}\n\n/* Destructor */\nvoid delArrayHashMap(ArrayHashMap *hmap) {\n    for (int i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            free(hmap->buckets[i]->val);\n            free(hmap->buckets[i]);\n        }\n    }\n    free(hmap);\n}\n\n/* Add operation */\nvoid put(ArrayHashMap *hmap, const int key, const char *val) {\n    Pair *Pair = malloc(sizeof(Pair));\n    Pair->key = key;\n    Pair->val = malloc(strlen(val) + 1);\n    strcpy(Pair->val, val);\n\n    int index = hashFunc(key);\n    hmap->buckets[index] = Pair;\n}\n\n/* Remove operation */\nvoid removeItem(ArrayHashMap *hmap, const int key) {\n    int index = hashFunc(key);\n    free(hmap->buckets[index]->val);\n    free(hmap->buckets[index]);\n    hmap->buckets[index] = NULL;\n}\n\n/* Get all key-value pairs */\nvoid pairSet(ArrayHashMap *hmap, MapSet *set) {\n    Pair *entries;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    entries = malloc(sizeof(Pair) * total);\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            entries[index].key = hmap->buckets[i]->key;\n            entries[index].val = malloc(strlen(hmap->buckets[i]->val) + 1);\n            strcpy(entries[index].val, hmap->buckets[i]->val);\n            index++;\n        }\n    }\n    set->set = entries;\n    set->len = total;\n}\n\n/* Get all keys */\nvoid keySet(ArrayHashMap *hmap, MapSet *set) {\n    int *keys;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    keys = malloc(total * sizeof(int));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            keys[index] = hmap->buckets[i]->key;\n            index++;\n        }\n    }\n    set->set = keys;\n    set->len = total;\n}\n\n/* Get all values */\nvoid valueSet(ArrayHashMap *hmap, MapSet *set) {\n    char **vals;\n    int i = 0, index = 0;\n    int total = 0;\n    /* Count valid key-value pairs */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    vals = malloc(total * sizeof(char *));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            vals[index] = hmap->buckets[i]->val;\n            index++;\n        }\n    }\n    set->set = vals;\n    set->len = total;\n}\n\n/* Print hash table */\nvoid print(ArrayHashMap *hmap) {\n    int i;\n    MapSet set;\n    pairSet(hmap, &set);\n    Pair *entries = (Pair *)set.set;\n    for (i = 0; i < set.len; i++) {\n        printf(\"%d -> %s\\n\", entries[i].key, entries[i].val);\n    }\n    free(set.set);\n}\n
array_hash_map.kt
/* Key-value pair */\nclass Pair(\n    var key: Int,\n    var _val: String\n)\n\n/* Hash table based on array implementation */\nclass ArrayHashMap {\n    // Initialize array with 100 buckets\n    private val buckets = arrayOfNulls<Pair>(100)\n\n    /* Hash function */\n    fun hashFunc(key: Int): Int {\n        val index = key % 100\n        return index\n    }\n\n    /* Query operation */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val pair = buckets[index] ?: return null\n        return pair._val\n    }\n\n    /* Add operation */\n    fun put(key: Int, _val: String) {\n        val pair = Pair(key, _val)\n        val index = hashFunc(key)\n        buckets[index] = pair\n    }\n\n    /* Remove operation */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        // Set to null to represent deletion\n        buckets[index] = null\n    }\n\n    /* Get all key-value pairs */\n    fun pairSet(): MutableList<Pair> {\n        val pairSet = mutableListOf<Pair>()\n        for (pair in buckets) {\n            if (pair != null)\n                pairSet.add(pair)\n        }\n        return pairSet\n    }\n\n    /* Get all keys */\n    fun keySet(): MutableList<Int> {\n        val keySet = mutableListOf<Int>()\n        for (pair in buckets) {\n            if (pair != null)\n                keySet.add(pair.key)\n        }\n        return keySet\n    }\n\n    /* Get all values */\n    fun valueSet(): MutableList<String> {\n        val valueSet = mutableListOf<String>()\n        for (pair in buckets) {\n            if (pair != null)\n                valueSet.add(pair._val)\n        }\n        return valueSet\n    }\n\n    /* Print hash table */\n    fun print() {\n        for (kv in pairSet()) {\n            val key = kv.key\n            val _val = kv._val\n            println(\"$key -> $_val\")\n        }\n    }\n}\n
array_hash_map.rb
### Key-value pair ###\nclass Pair\n  attr_accessor :key, :val\n\n  def initialize(key, val)\n    @key = key\n    @val = val\n  end\nend\n\n### Hash map based on array ###\nclass ArrayHashMap\n  ### Constructor ###\n  def initialize\n    # Initialize array with 100 buckets\n    @buckets = Array.new(100)\n  end\n\n  ### Hash function ###\n  def hash_func(key)\n    index = key % 100\n  end\n\n  ### Query operation ###\n  def get(key)\n    index = hash_func(key)\n    pair = @buckets[index]\n\n    return if pair.nil?\n    pair.val\n  end\n\n  ### Add operation ###\n  def put(key, val)\n    pair = Pair.new(key, val)\n    index = hash_func(key)\n    @buckets[index] = pair\n  end\n\n  ### Delete operation ###\n  def remove(key)\n    index = hash_func(key)\n    # Set to nil to delete\n    @buckets[index] = nil\n  end\n\n  ### Get all key-value pairs ###\n  def entry_set\n    result = []\n    @buckets.each { |pair| result << pair unless pair.nil? }\n    result\n  end\n\n  ### Get all keys ###\n  def key_set\n    result = []\n    @buckets.each { |pair| result << pair.key unless pair.nil? }\n    result\n  end\n\n  ### Get all values ###\n  def value_set\n    result = []\n    @buckets.each { |pair| result << pair.val unless pair.nil? }\n    result\n  end\n\n  ### Print hash table ###\n  def print\n    @buckets.each { |pair| puts \"#{pair.key} -> #{pair.val}\" unless pair.nil? }\n  end\nend\n
","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/hash_map/#613-hash-collision-and-resizing","level":2,"title":"6.1.3   Hash Collision and Resizing","text":"

Fundamentally, the role of a hash function is to map the input space consisting of all keys to the output space consisting of all array indices, and the input space is often much larger than the output space. Therefore, theoretically there must be cases where \"multiple inputs correspond to the same output\".

For the hash function in the above example, when the input keys have the same last two digits, the hash function produces the same output. For example, when querying two students with IDs 12836 and 20336, we get:

12836 % 100 = 36\n20336 % 100 = 36\n

As shown in Figure 6-3, two student IDs point to the same name, which is obviously incorrect. We call this situation where multiple inputs correspond to the same output a hash collision.

Figure 6-3   Hash collision example

It's easy to see that the larger the hash table capacity \\(n\\), the lower the probability that multiple keys will be assigned to the same bucket, and the fewer collisions. Therefore, we can reduce hash collisions by expanding the hash table.

As shown in Figure 6-4, before expansion, the key-value pairs (136, A) and (236, D) collided, but after expansion, the collision disappears.

Figure 6-4   Hash table resizing

Similar to array expansion, hash table expansion requires migrating all key-value pairs from the original hash table to the new hash table, which is very time-consuming. Moreover, since the hash table capacity capacity changes, we need to recalculate the storage locations of all key-value pairs through the hash function, further increasing the computational overhead of the expansion process. For this reason, programming languages typically reserve a sufficiently large hash table capacity to prevent frequent expansion.

The load factor is an important concept for hash tables. It is defined as the number of elements in the hash table divided by the number of buckets, and is used to measure the severity of hash collisions. It is also commonly used as a trigger condition for hash table expansion. For example, in Java, when the load factor exceeds \\(0.75\\), the system will expand the hash table to \\(2\\) times its original size.

","path":["Chapter 6. Hashing","6.1   Hash Table"],"tags":[]},{"location":"chapter_hashing/summary/","level":1,"title":"6.4   Summary","text":"","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_hashing/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Given an input key, a hash table can retrieve the corresponding value in \\(O(1)\\) time, which is highly efficient.
  • Common hash table operations include querying, adding key-value pairs, deleting key-value pairs, and traversing the hash table.
  • The hash function maps a key to an array index, allowing access to the corresponding bucket and retrieval of the value.
  • Two different keys may end up with the same array index after hashing, leading to erroneous query results. This phenomenon is known as hash collision.
  • The larger the capacity of the hash table, the lower the probability of hash collisions. Therefore, hash table expansion can mitigate hash collisions. Similar to array expansion, hash table expansion is costly.
  • The load factor, defined as the number of elements divided by the number of buckets, reflects the severity of hash collisions and is often used as a condition to trigger hash table expansion.
  • Separate chaining addresses hash collisions by converting each element into a linked list, storing all colliding elements in the same linked list. However, excessively long linked lists can reduce query efficiency, which can be improved by converting the linked lists into red-black trees.
  • Open addressing handles hash collisions through multiple probing. Linear probing uses a fixed step size but cannot delete elements and is prone to clustering. Double hashing uses multiple hash functions for probing, which reduces clustering compared to linear probing but increases computational overhead.
  • Different programming languages adopt various hash table implementations. For example, Java's HashMap uses separate chaining, while Python's dict employs open addressing.
  • In hash tables, we desire hash algorithms with determinism, high efficiency, and uniform distribution. In cryptography, hash algorithms should also possess collision resistance and the avalanche effect.
  • Hash algorithms typically use large prime numbers as moduli to maximize the uniform distribution of hash values and reduce hash collisions.
  • Common hash algorithms include MD5, SHA-1, SHA-2, and SHA-3. MD5 is often used for file integrity checks, while SHA-2 is commonly used in secure applications and protocols.
  • Programming languages usually provide built-in hash algorithms for data types to calculate bucket indices in hash tables. Generally, only immutable objects are hashable.
","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_hashing/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: When does the time complexity of a hash table degrade to \\(O(n)\\)?

The time complexity of a hash table can degrade to \\(O(n)\\) when hash collisions are severe. When the hash function is well-designed, the capacity is set appropriately, and collisions are evenly distributed, the time complexity is \\(O(1)\\). We usually consider the time complexity to be \\(O(1)\\) when using built-in hash tables in programming languages.

Q: Why not use the hash function \\(f(x) = x\\)? This would eliminate collisions.

Under the hash function \\(f(x) = x\\), each element corresponds to a unique bucket index, which is equivalent to an array. However, the input space is usually much larger than the output space (array length), so the last step of a hash function is often to take the modulo of the array length. In other words, the goal of a hash table is to map a larger state space to a smaller one while providing \\(O(1)\\) query efficiency.

Q: Why can hash tables be more efficient than arrays, linked lists, or binary trees, even though hash tables are implemented using these structures?

Firstly, hash tables have higher time efficiency but lower space efficiency. A significant portion of memory in hash tables remains unused.

Secondly, hash tables are only more time-efficient in specific use cases. If a feature can be implemented with the same time complexity using an array or a linked list, it's usually faster than using a hash table. This is because the computation of the hash function incurs overhead, making the constant factor in the time complexity larger.

Lastly, the time complexity of hash tables can degrade. For example, in separate chaining, we perform search operations in a linked list or red-black tree, which still risks degrading to \\(O(n)\\) time.

Q: Does double hashing also have the flaw of not being able to delete elements directly? Can space marked as deleted be reused?

Double hashing is a form of open addressing, and all open addressing methods have the drawback of not being able to delete elements directly; they require marking elements as deleted. Marked spaces can be reused. When inserting new elements into the hash table, and the hash function points to a position marked as deleted, that position can be used by the new element. This maintains the probing sequence of the hash table while ensuring efficient use of space.

Q: Why do hash collisions occur during the search process in linear probing?

During the search process, the hash function points to the corresponding bucket and key-value pair. If the key doesn't match, it indicates a hash collision. Therefore, linear probing will search downward at a predetermined step size until the correct key-value pair is found or the search fails.

Q: Why can expanding a hash table alleviate hash collisions?

The last step of a hash function often involves taking the modulo of the array length \\(n\\), to keep the output within the array index range. When expanding, the array length \\(n\\) changes, and the indices corresponding to the keys may also change. Keys that were previously mapped to the same bucket might be distributed across multiple buckets after expansion, thereby mitigating hash collisions.

","path":["Chapter 6. Hashing","6.4   Summary"],"tags":[]},{"location":"chapter_heap/","level":1,"title":"Chapter 8.   Heap","text":"

Abstract

Heaps are like mountain peaks, layered and undulating, each with its unique form.

The peaks rise and fall at varying heights, yet the tallest peak always catches the eye first.

","path":["Chapter 8. Heap","Chapter 8.   Heap"],"tags":[]},{"location":"chapter_heap/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 8.1   Heap
  • 8.2   Building a Heap
  • 8.3   Top-K Problem
  • 8.4   Summary
","path":["Chapter 8. Heap","Chapter 8.   Heap"],"tags":[]},{"location":"chapter_heap/build_heap/","level":1,"title":"8.2   Heap Construction Operation","text":"

In some cases, we want to build a heap using all elements of a list, and this process is called \"heap construction operation.\"

","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#821-implementing-with-element-insertion","level":2,"title":"8.2.1   Implementing with Element Insertion","text":"

We first create an empty heap, then iterate through the list, performing the \"element insertion operation\" on each element in sequence. This means adding the element to the bottom of the heap and then performing \"bottom-to-top\" heapify on that element.

Each time an element is inserted into the heap, the heap's length increases by one. Since nodes are added to the binary tree sequentially from top to bottom, the heap is constructed \"from top to bottom.\"

Given \\(n\\) elements, each element's insertion operation takes \\(O(\\log{n})\\) time, so the time complexity of this heap construction method is \\(O(n \\log n)\\).

","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#822-implementing-through-heapify-traversal","level":2,"title":"8.2.2   Implementing Through Heapify Traversal","text":"

In fact, we can implement a more efficient heap construction method in two steps.

  1. Add all elements of the list as-is to the heap, at which point the heap property is not yet satisfied.
  2. Traverse the heap in reverse order (reverse of level-order traversal), performing \"top-to-bottom heapify\" on each non-leaf node in sequence.

After heapifying a node, the subtree rooted at that node becomes a valid sub-heap. Since we traverse in reverse order, the heap is constructed \"from bottom to top.\"

The reason for choosing reverse order traversal is that it ensures the subtree below the current node is already a valid sub-heap, making the heapification of the current node effective.

It's worth noting that since leaf nodes have no children, they are naturally valid sub-heaps and do not require heapification. As shown in the code below, the last non-leaf node is the parent of the last node; we start from it and traverse in reverse order to perform heapification:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def __init__(self, nums: list[int]):\n    \"\"\"Constructor, build heap based on input list\"\"\"\n    # Add list elements to heap as is\n    self.max_heap = nums\n    # Heapify all nodes except leaf nodes\n    for i in range(self.parent(self.size() - 1), -1, -1):\n        self.sift_down(i)\n
my_heap.cpp
/* Constructor, build heap based on input list */\nMaxHeap(vector<int> nums) {\n    // Add list elements to heap as is\n    maxHeap = nums;\n    // Heapify all nodes except leaf nodes\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
my_heap.java
/* Constructor, build heap based on input list */\nMaxHeap(List<Integer> nums) {\n    // Add list elements to heap as is\n    maxHeap = new ArrayList<>(nums);\n    // Heapify all nodes except leaf nodes\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
my_heap.cs
/* Constructor, build heap from input list */\nMaxHeap(IEnumerable<int> nums) {\n    // Add list elements to heap as is\n    maxHeap = new List<int>(nums);\n    // Heapify all nodes except leaf nodes\n    var size = Parent(this.Size() - 1);\n    for (int i = size; i >= 0; i--) {\n        SiftDown(i);\n    }\n}\n
my_heap.go
/* Constructor, build heap from slice */\nfunc newMaxHeap(nums []any) *maxHeap {\n    // Add list elements to heap as is\n    h := &maxHeap{data: nums}\n    for i := h.parent(len(h.data) - 1); i >= 0; i-- {\n        // Heapify all nodes except leaf nodes\n        h.siftDown(i)\n    }\n    return h\n}\n
my_heap.swift
/* Constructor, build heap based on input list */\ninit(nums: [Int]) {\n    // Add list elements to heap as is\n    maxHeap = nums\n    // Heapify all nodes except leaf nodes\n    for i in (0 ... parent(i: size() - 1)).reversed() {\n        siftDown(i: i)\n    }\n}\n
my_heap.js
/* Constructor, build empty heap or build heap from input list */\nconstructor(nums) {\n    // Add list elements to heap as is\n    this.#maxHeap = nums === undefined ? [] : [...nums];\n    // Heapify all nodes except leaf nodes\n    for (let i = this.#parent(this.size() - 1); i >= 0; i--) {\n        this.#siftDown(i);\n    }\n}\n
my_heap.ts
/* Constructor, build empty heap or build heap from input list */\nconstructor(nums?: number[]) {\n    // Add list elements to heap as is\n    this.maxHeap = nums === undefined ? [] : [...nums];\n    // Heapify all nodes except leaf nodes\n    for (let i = this.parent(this.size() - 1); i >= 0; i--) {\n        this.siftDown(i);\n    }\n}\n
my_heap.dart
/* Constructor, build heap based on input list */\nMaxHeap(List<int> nums) {\n  // Add list elements to heap as is\n  _maxHeap = nums;\n  // Heapify all nodes except leaf nodes\n  for (int i = _parent(size() - 1); i >= 0; i--) {\n    siftDown(i);\n  }\n}\n
my_heap.rs
/* Constructor, build heap based on input list */\nfn new(nums: Vec<i32>) -> Self {\n    // Add list elements to heap as is\n    let mut heap = MaxHeap { max_heap: nums };\n    // Heapify all nodes except leaf nodes\n    for i in (0..=Self::parent(heap.size() - 1)).rev() {\n        heap.sift_down(i);\n    }\n    heap\n}\n
my_heap.c
/* Constructor, build heap from slice */\nMaxHeap *newMaxHeap(int nums[], int size) {\n    // Push all elements to heap\n    MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap));\n    maxHeap->size = size;\n    memcpy(maxHeap->data, nums, size * sizeof(int));\n    for (int i = parent(maxHeap, size - 1); i >= 0; i--) {\n        // Heapify all nodes except leaf nodes\n        siftDown(maxHeap, i);\n    }\n    return maxHeap;\n}\n
my_heap.kt
/* Max heap */\nclass MaxHeap(nums: MutableList<Int>?) {\n    // Use list instead of array, no need to consider capacity expansion\n    private val maxHeap = mutableListOf<Int>()\n\n    /* Constructor, build heap based on input list */\n    init {\n        // Add list elements to heap as is\n        maxHeap.addAll(nums!!)\n        // Heapify all nodes except leaf nodes\n        for (i in parent(size() - 1) downTo 0) {\n            siftDown(i)\n        }\n    }\n\n    /* Get index of left child node */\n    private fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Get index of right child node */\n    private fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Get index of parent node */\n    private fun parent(i: Int): Int {\n        return (i - 1) / 2 // Floor division\n    }\n\n    /* Swap elements */\n    private fun swap(i: Int, j: Int) {\n        val temp = maxHeap[i]\n        maxHeap[i] = maxHeap[j]\n        maxHeap[j] = temp\n    }\n\n    /* Get heap size */\n    fun size(): Int {\n        return maxHeap.size\n    }\n\n    /* Check if heap is empty */\n    fun isEmpty(): Boolean {\n        /* Check if heap is empty */\n        return size() == 0\n    }\n\n    /* Access top element */\n    fun peek(): Int {\n        return maxHeap[0]\n    }\n\n    /* Element enters heap */\n    fun push(_val: Int) {\n        // Add node\n        maxHeap.add(_val)\n        // Heapify from bottom to top\n        siftUp(size() - 1)\n    }\n\n    /* Starting from node i, heapify from bottom to top */\n    private fun siftUp(it: Int) {\n        // Kotlin function parameters are immutable, so create temporary variable\n        var i = it\n        while (true) {\n            // Get parent node of node i\n            val p = parent(i)\n            // When \"crossing root node\" or \"node needs no repair\", end heapify\n            if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n            // Swap two nodes\n            swap(i, p)\n            // Loop upward heapify\n            i = p\n        }\n    }\n\n    /* Element exits heap */\n    fun pop(): Int {\n        // Handle empty case\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Delete node\n        swap(0, size() - 1)\n        // Remove node\n        val _val = maxHeap.removeAt(size() - 1)\n        // Return top element\n        siftDown(0)\n        // Return heap top element\n        return _val\n    }\n\n    /* Starting from node i, heapify from top to bottom */\n    private fun siftDown(it: Int) {\n        // Kotlin function parameters are immutable, so create temporary variable\n        var i = it\n        while (true) {\n            // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n            val l = left(i)\n            val r = right(i)\n            var ma = i\n            if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n            if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n            // Swap two nodes\n            if (ma == i) break\n            // Swap two nodes\n            swap(i, ma)\n            // Loop downwards heapification\n            i = ma\n        }\n    }\n\n    /* Driver Code */\n    fun print() {\n        val queue = PriorityQueue { a: Int, b: Int -> b - a }\n        queue.addAll(maxHeap)\n        printHeap(queue)\n    }\n}\n
my_heap.rb
### Constructor, build heap from input list ###\ndef initialize(nums)\n  # Add list elements to heap as is\n  @max_heap = nums\n  # Heapify all nodes except leaf nodes\n  parent(size - 1).downto(0) do |i|\n    sift_down(i)\n  end\nend\n
","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/build_heap/#823-complexity-analysis","level":2,"title":"8.2.3   Complexity Analysis","text":"

Next, let's attempt to derive the time complexity of this second heap construction method.

  • Assuming the complete binary tree has \\(n\\) nodes, then the number of leaf nodes is \\((n + 1) / 2\\), where \\(/\\) is floor division. Therefore, the number of nodes that need heapification is \\((n - 1) / 2\\).
  • In the top-to-bottom heapify process, each node is heapified at most to the leaf nodes, so the maximum number of iterations is the binary tree height \\(\\log n\\).

Multiplying these two together, we get a time complexity of \\(O(n \\log n)\\) for the heap construction process. However, this estimate is not accurate because it doesn't account for the property that binary trees have far more nodes at lower levels than at upper levels.

Let's perform a more accurate calculation. To reduce calculation difficulty, assume a \"perfect binary tree\" with \\(n\\) nodes and height \\(h\\); this assumption does not affect the correctness of the result.

Figure 8-5   Node count at each level of a perfect binary tree

As shown in Figure 8-5, the maximum number of iterations for a node's \"top-to-bottom heapify\" equals the distance from that node to the leaf nodes, which is precisely the \"node height.\" Therefore, we can sum the \"number of nodes \\(\\times\\) node height\" at each level to obtain the total number of heapify iterations for all nodes.

\\[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{(h-1)}\\times1 \\]

To simplify the above expression, we need to use sequence knowledge from high school. First, multiply \\(T(h)\\) by \\(2\\) to get:

\\[ \\begin{aligned} T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{h-1}\\times1 \\newline 2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \\dots + 2^{h}\\times1 \\newline \\end{aligned} \\]

Using the method of differences, subtract the first equation \\(T(h)\\) from the second equation \\(2 T(h)\\) to get:

\\[ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \\dots + 2^{h-1} + 2^h \\]

Observing the above expression, we find that \\(T(h)\\) is a geometric series, which can be calculated directly using the sum formula, yielding a time complexity of:

\\[ \\begin{aligned} T(h) & = 2 \\frac{1 - 2^h}{1 - 2} - h \\newline & = 2^{h+1} - h - 2 \\newline & = O(2^h) \\end{aligned} \\]

Furthermore, a perfect binary tree with height \\(h\\) has \\(n = 2^{h+1} - 1\\) nodes, so the complexity is \\(O(2^h) = O(n)\\). This derivation shows that the time complexity of building a heap from an input list is \\(O(n)\\), which is highly efficient.

","path":["Chapter 8. Heap","8.2   Heap Construction Operation"],"tags":[]},{"location":"chapter_heap/heap/","level":1,"title":"8.1   Heap","text":"

A heap is a complete binary tree that satisfies specific conditions and can be mainly categorized into two types, as shown in Figure 8-1.

  • min heap: The value of any node \\(\\leq\\) the values of its child nodes.
  • max heap: The value of any node \\(\\geq\\) the values of its child nodes.

Figure 8-1   Min heap and max heap

As a special case of a complete binary tree, heaps have the following characteristics.

  • The bottom layer nodes are filled from left to right, and nodes in other layers are fully filled.
  • We call the root node of the binary tree the \"heap top\" and the bottom-rightmost node the \"heap bottom.\"
  • For max heaps (min heaps), the value of the heap top element (root node) is the largest (smallest).
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#811-common-heap-operations","level":2,"title":"8.1.1   Common Heap Operations","text":"

It should be noted that many programming languages provide a priority queue, which is an abstract data structure defined as a queue with priority sorting.

In fact, heaps are typically used to implement priority queues, with max heaps corresponding to priority queues where elements are dequeued in descending order. From a usage perspective, we can regard \"priority queue\" and \"heap\" as equivalent data structures. Therefore, this book does not make a special distinction between the two and uniformly refers to them as \"heap.\"

Common heap operations are shown in Table 8-1, and method names need to be determined based on the programming language.

Table 8-1   Efficiency of Heap Operations

Method name Description Time complexity push() Insert an element into the heap \\(O(\\log n)\\) pop() Remove the heap top element \\(O(\\log n)\\) peek() Access the heap top element (max/min value for max/min heap) \\(O(1)\\) size() Get the number of elements in the heap \\(O(1)\\) isEmpty() Check if the heap is empty \\(O(1)\\)

In practical applications, we can directly use the heap class (or priority queue class) provided by programming languages.

Similar to \"ascending order\" and \"descending order\" in sorting algorithms, we can implement conversion between \"min heap\" and \"max heap\" by setting a flag or modifying the Comparator. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap.py
# Initialize a min heap\nmin_heap, flag = [], 1\n# Initialize a max heap\nmax_heap, flag = [], -1\n\n# Python's heapq module implements a min heap by default\n# Consider negating elements before pushing them to the heap, which inverts the size relationship and thus implements a max heap\n# In this example, flag = 1 corresponds to a min heap, flag = -1 corresponds to a max heap\n\n# Push elements into the heap\nheapq.heappush(max_heap, flag * 1)\nheapq.heappush(max_heap, flag * 3)\nheapq.heappush(max_heap, flag * 2)\nheapq.heappush(max_heap, flag * 5)\nheapq.heappush(max_heap, flag * 4)\n\n# Get the heap top element\npeek: int = flag * max_heap[0] # 5\n\n# Remove the heap top element\n# The removed elements will form a descending sequence\nval = flag * heapq.heappop(max_heap) # 5\nval = flag * heapq.heappop(max_heap) # 4\nval = flag * heapq.heappop(max_heap) # 3\nval = flag * heapq.heappop(max_heap) # 2\nval = flag * heapq.heappop(max_heap) # 1\n\n# Get the heap size\nsize: int = len(max_heap)\n\n# Check if the heap is empty\nis_empty: bool = not max_heap\n\n# Build a heap from an input list\nmin_heap: list[int] = [1, 3, 2, 5, 4]\nheapq.heapify(min_heap)\n
heap.cpp
/* Initialize a heap */\n// Initialize a min heap\npriority_queue<int, vector<int>, greater<int>> minHeap;\n// Initialize a max heap\npriority_queue<int, vector<int>, less<int>> maxHeap;\n\n/* Push elements into the heap */\nmaxHeap.push(1);\nmaxHeap.push(3);\nmaxHeap.push(2);\nmaxHeap.push(5);\nmaxHeap.push(4);\n\n/* Get the heap top element */\nint peek = maxHeap.top(); // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\nmaxHeap.pop(); // 5\nmaxHeap.pop(); // 4\nmaxHeap.pop(); // 3\nmaxHeap.pop(); // 2\nmaxHeap.pop(); // 1\n\n/* Get the heap size */\nint size = maxHeap.size();\n\n/* Check if the heap is empty */\nbool isEmpty = maxHeap.empty();\n\n/* Build a heap from an input list */\nvector<int> input{1, 3, 2, 5, 4};\npriority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());\n
heap.java
/* Initialize a heap */\n// Initialize a min heap\nQueue<Integer> minHeap = new PriorityQueue<>();\n// Initialize a max heap (use lambda expression to modify Comparator)\nQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);\n\n/* Push elements into the heap */\nmaxHeap.offer(1);\nmaxHeap.offer(3);\nmaxHeap.offer(2);\nmaxHeap.offer(5);\nmaxHeap.offer(4);\n\n/* Get the heap top element */\nint peek = maxHeap.peek(); // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.poll(); // 5\npeek = maxHeap.poll(); // 4\npeek = maxHeap.poll(); // 3\npeek = maxHeap.poll(); // 2\npeek = maxHeap.poll(); // 1\n\n/* Get the heap size */\nint size = maxHeap.size();\n\n/* Check if the heap is empty */\nboolean isEmpty = maxHeap.isEmpty();\n\n/* Build a heap from an input list */\nminHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));\n
heap.cs
/* Initialize a heap */\n// Initialize a min heap\nPriorityQueue<int, int> minHeap = new();\n// Initialize a max heap (use lambda expression to modify Comparer)\nPriorityQueue<int, int> maxHeap = new(Comparer<int>.Create((x, y) => y.CompareTo(x)));\n\n/* Push elements into the heap */\nmaxHeap.Enqueue(1, 1);\nmaxHeap.Enqueue(3, 3);\nmaxHeap.Enqueue(2, 2);\nmaxHeap.Enqueue(5, 5);\nmaxHeap.Enqueue(4, 4);\n\n/* Get the heap top element */\nint peek = maxHeap.Peek();//5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.Dequeue();  // 5\npeek = maxHeap.Dequeue();  // 4\npeek = maxHeap.Dequeue();  // 3\npeek = maxHeap.Dequeue();  // 2\npeek = maxHeap.Dequeue();  // 1\n\n/* Get the heap size */\nint size = maxHeap.Count;\n\n/* Check if the heap is empty */\nbool isEmpty = maxHeap.Count == 0;\n\n/* Build a heap from an input list */\nminHeap = new PriorityQueue<int, int>([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);\n
heap.go
// In Go, we can construct a max heap of integers by implementing heap.Interface\n// Implementing heap.Interface also requires implementing sort.Interface\ntype intHeap []any\n\n// Push implements the heap.Interface method for pushing an element into the heap\nfunc (h *intHeap) Push(x any) {\n    // Push and Pop use pointer receiver as parameters\n    // because they not only adjust the slice contents but also modify the slice length\n    *h = append(*h, x.(int))\n}\n\n// Pop implements the heap.Interface method for popping the heap top element\nfunc (h *intHeap) Pop() any {\n    // The element to be removed is stored at the end\n    last := (*h)[len(*h)-1]\n    *h = (*h)[:len(*h)-1]\n    return last\n}\n\n// Len is a sort.Interface method\nfunc (h *intHeap) Len() int {\n    return len(*h)\n}\n\n// Less is a sort.Interface method\nfunc (h *intHeap) Less(i, j int) bool {\n    // To implement a min heap, change this to a less-than sign\n    return (*h)[i].(int) > (*h)[j].(int)\n}\n\n// Swap is a sort.Interface method\nfunc (h *intHeap) Swap(i, j int) {\n    (*h)[i], (*h)[j] = (*h)[j], (*h)[i]\n}\n\n// Top gets the heap top element\nfunc (h *intHeap) Top() any {\n    return (*h)[0]\n}\n\n/* Driver Code */\nfunc TestHeap(t *testing.T) {\n    /* Initialize a heap */\n    // Initialize a max heap\n    maxHeap := &intHeap{}\n    heap.Init(maxHeap)\n    /* Push elements into the heap */\n    // Call heap.Interface methods to add elements\n    heap.Push(maxHeap, 1)\n    heap.Push(maxHeap, 3)\n    heap.Push(maxHeap, 2)\n    heap.Push(maxHeap, 4)\n    heap.Push(maxHeap, 5)\n\n    /* Get the heap top element */\n    top := maxHeap.Top()\n    fmt.Printf(\"Heap top element is %d\\n\", top)\n\n    /* Remove the heap top element */\n    // Call heap.Interface methods to remove elements\n    heap.Pop(maxHeap) // 5\n    heap.Pop(maxHeap) // 4\n    heap.Pop(maxHeap) // 3\n    heap.Pop(maxHeap) // 2\n    heap.Pop(maxHeap) // 1\n\n    /* Get the heap size */\n    size := len(*maxHeap)\n    fmt.Printf(\"Number of heap elements is %d\\n\", size)\n\n    /* Check if the heap is empty */\n    isEmpty := len(*maxHeap) == 0\n    fmt.Printf(\"Is the heap empty? %t\\n\", isEmpty)\n}\n
heap.swift
/* Initialize a heap */\n// Swift's Heap type supports both max heaps and min heaps, and requires importing swift-collections\nvar heap = Heap<Int>()\n\n/* Push elements into the heap */\nheap.insert(1)\nheap.insert(3)\nheap.insert(2)\nheap.insert(5)\nheap.insert(4)\n\n/* Get the heap top element */\nvar peek = heap.max()!\n\n/* Remove the heap top element */\npeek = heap.removeMax() // 5\npeek = heap.removeMax() // 4\npeek = heap.removeMax() // 3\npeek = heap.removeMax() // 2\npeek = heap.removeMax() // 1\n\n/* Get the heap size */\nlet size = heap.count\n\n/* Check if the heap is empty */\nlet isEmpty = heap.isEmpty\n\n/* Build a heap from an input list */\nlet heap2 = Heap([1, 3, 2, 5, 4])\n
heap.js
// JavaScript does not provide a built-in Heap class\n
heap.ts
// TypeScript does not provide a built-in Heap class\n
heap.dart
// Dart does not provide a built-in Heap class\n
heap.rs
use std::collections::BinaryHeap;\nuse std::cmp::Reverse;\n\n/* Initialize a heap */\n// Initialize a min heap\nlet mut min_heap = BinaryHeap::<Reverse<i32>>::new();\n// Initialize a max heap\nlet mut max_heap = BinaryHeap::new();\n\n/* Push elements into the heap */\nmax_heap.push(1);\nmax_heap.push(3);\nmax_heap.push(2);\nmax_heap.push(5);\nmax_heap.push(4);\n\n/* Get the heap top element */\nlet peek = max_heap.peek().unwrap();  // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\nlet peek = max_heap.pop().unwrap();   // 5\nlet peek = max_heap.pop().unwrap();   // 4\nlet peek = max_heap.pop().unwrap();   // 3\nlet peek = max_heap.pop().unwrap();   // 2\nlet peek = max_heap.pop().unwrap();   // 1\n\n/* Get the heap size */\nlet size = max_heap.len();\n\n/* Check if the heap is empty */\nlet is_empty = max_heap.is_empty();\n\n/* Build a heap from an input list */\nlet min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);\n
heap.c
// C does not provide a built-in Heap class\n
heap.kt
/* Initialize a heap */\n// Initialize a min heap\nvar minHeap = PriorityQueue<Int>()\n// Initialize a max heap (use lambda expression to modify Comparator)\nval maxHeap = PriorityQueue { a: Int, b: Int -> b - a }\n\n/* Push elements into the heap */\nmaxHeap.offer(1)\nmaxHeap.offer(3)\nmaxHeap.offer(2)\nmaxHeap.offer(5)\nmaxHeap.offer(4)\n\n/* Get the heap top element */\nvar peek = maxHeap.peek() // 5\n\n/* Remove the heap top element */\n// The removed elements will form a descending sequence\npeek = maxHeap.poll() // 5\npeek = maxHeap.poll() // 4\npeek = maxHeap.poll() // 3\npeek = maxHeap.poll() // 2\npeek = maxHeap.poll() // 1\n\n/* Get the heap size */\nval size = maxHeap.size\n\n/* Check if the heap is empty */\nval isEmpty = maxHeap.isEmpty()\n\n/* Build a heap from an input list */\nminHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))\n
heap.rb
# Ruby does not provide a built-in Heap class\n
Code Visualization

Full Screen >

","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#812-implementation-of-the-heap","level":2,"title":"8.1.2   Implementation of the Heap","text":"

The following implementation is of a max heap. To convert it to a min heap, simply invert all size logic comparisons (for example, replace \\(\\geq\\) with \\(\\leq\\)). Interested readers are encouraged to implement this on their own.

","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#1-heap-storage-and-representation","level":3,"title":"1.   Heap Storage and Representation","text":"

As mentioned in the \"Binary Tree\" chapter, complete binary trees are well-suited for array representation. Since heaps are a type of complete binary tree, we will use arrays to store heaps.

When representing a binary tree with an array, elements represent node values, and indexes represent node positions in the binary tree. Node pointers are implemented through index mapping formulas.

As shown in Figure 8-2, given an index \\(i\\), the index of its left child is \\(2i + 1\\), the index of its right child is \\(2i + 2\\), and the index of its parent is \\((i - 1) / 2\\) (floor division). When an index is out of bounds, it indicates a null node or that the node does not exist.

Figure 8-2   Representation and storage of heaps

We can encapsulate the index mapping formula into functions for convenient subsequent use:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def left(self, i: int) -> int:\n    \"\"\"Get index of left child node\"\"\"\n    return 2 * i + 1\n\ndef right(self, i: int) -> int:\n    \"\"\"Get index of right child node\"\"\"\n    return 2 * i + 2\n\ndef parent(self, i: int) -> int:\n    \"\"\"Get index of parent node\"\"\"\n    return (i - 1) // 2  # Floor division\n
my_heap.cpp
/* Get index of left child node */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
my_heap.java
/* Get index of left child node */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
my_heap.cs
/* Get index of left child node */\nint Left(int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint Right(int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint Parent(int i) {\n    return (i - 1) / 2; // Floor division\n}\n
my_heap.go
/* Get index of left child node */\nfunc (h *maxHeap) left(i int) int {\n    return 2*i + 1\n}\n\n/* Get index of right child node */\nfunc (h *maxHeap) right(i int) int {\n    return 2*i + 2\n}\n\n/* Get index of parent node */\nfunc (h *maxHeap) parent(i int) int {\n    // Floor division\n    return (i - 1) / 2\n}\n
my_heap.swift
/* Get index of left child node */\nfunc left(i: Int) -> Int {\n    2 * i + 1\n}\n\n/* Get index of right child node */\nfunc right(i: Int) -> Int {\n    2 * i + 2\n}\n\n/* Get index of parent node */\nfunc parent(i: Int) -> Int {\n    (i - 1) / 2 // Floor division\n}\n
my_heap.js
/* Get index of left child node */\n#left(i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\n#right(i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\n#parent(i) {\n    return Math.floor((i - 1) / 2); // Floor division\n}\n
my_heap.ts
/* Get index of left child node */\nleft(i: number): number {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nright(i: number): number {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nparent(i: number): number {\n    return Math.floor((i - 1) / 2); // Floor division\n}\n
my_heap.dart
/* Get index of left child node */\nint _left(int i) {\n  return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint _right(int i) {\n  return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint _parent(int i) {\n  return (i - 1) ~/ 2; // Floor division\n}\n
my_heap.rs
/* Get index of left child node */\nfn left(i: usize) -> usize {\n    2 * i + 1\n}\n\n/* Get index of right child node */\nfn right(i: usize) -> usize {\n    2 * i + 2\n}\n\n/* Get index of parent node */\nfn parent(i: usize) -> usize {\n    (i - 1) / 2 // Floor division\n}\n
my_heap.c
/* Get index of left child node */\nint left(MaxHeap *maxHeap, int i) {\n    return 2 * i + 1;\n}\n\n/* Get index of right child node */\nint right(MaxHeap *maxHeap, int i) {\n    return 2 * i + 2;\n}\n\n/* Get index of parent node */\nint parent(MaxHeap *maxHeap, int i) {\n    return (i - 1) / 2; // Round down\n}\n
my_heap.kt
/* Get index of left child node */\nfun left(i: Int): Int {\n    return 2 * i + 1\n}\n\n/* Get index of right child node */\nfun right(i: Int): Int {\n    return 2 * i + 2\n}\n\n/* Get index of parent node */\nfun parent(i: Int): Int {\n    return (i - 1) / 2 // Floor division\n}\n
my_heap.rb
### Get left child index ###\ndef left(i)\n  2 * i + 1\nend\n\n### Get right child index ###\ndef right(i)\n  2 * i + 2\nend\n\n### Get parent node index ###\ndef parent(i)\n  (i - 1) / 2     # Floor division\nend\n
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#2-accessing-the-heap-top-element","level":3,"title":"2.   Accessing the Heap Top Element","text":"

The heap top element is the root node of the binary tree, which is also the first element of the list:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def peek(self) -> int:\n    \"\"\"Access top element\"\"\"\n    return self.max_heap[0]\n
my_heap.cpp
/* Access top element */\nint peek() {\n    return maxHeap[0];\n}\n
my_heap.java
/* Access top element */\nint peek() {\n    return maxHeap.get(0);\n}\n
my_heap.cs
/* Access top element */\nint Peek() {\n    return maxHeap[0];\n}\n
my_heap.go
/* Access top element */\nfunc (h *maxHeap) peek() any {\n    return h.data[0]\n}\n
my_heap.swift
/* Access top element */\nfunc peek() -> Int {\n    maxHeap[0]\n}\n
my_heap.js
/* Access top element */\npeek() {\n    return this.#maxHeap[0];\n}\n
my_heap.ts
/* Access top element */\npeek(): number {\n    return this.maxHeap[0];\n}\n
my_heap.dart
/* Access top element */\nint peek() {\n  return _maxHeap[0];\n}\n
my_heap.rs
/* Access top element */\nfn peek(&self) -> Option<i32> {\n    self.max_heap.first().copied()\n}\n
my_heap.c
/* Access top element */\nint peek(MaxHeap *maxHeap) {\n    return maxHeap->data[0];\n}\n
my_heap.kt
/* Access top element */\nfun peek(): Int {\n    return maxHeap[0]\n}\n
my_heap.rb
### Access heap top element ###\ndef peek\n  @max_heap[0]\nend\n
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#3-inserting-an-element-into-the-heap","level":3,"title":"3.   Inserting an Element Into the Heap","text":"

Given an element val, we first add it to the bottom of the heap. After addition, since val may be larger than other elements in the heap, the heap's property may be violated. Therefore, it's necessary to repair the path from the inserted node to the root node. This operation is called heapify.

Starting from the inserted node, perform heapify from bottom to top. As shown in Figure 8-3, we compare the inserted node with its parent node, and if the inserted node is larger, swap them. Then continue this operation, repairing nodes in the heap from bottom to top until we pass the root node or encounter a node that does not need swapping.

<1><2><3><4><5><6><7><8><9>

Figure 8-3   Steps of inserting an element into the heap

Given a total of \\(n\\) nodes, the tree height is \\(O(\\log n)\\). Thus, the number of loop iterations in the heapify operation is at most \\(O(\\log n)\\), making the time complexity of the element insertion operation \\(O(\\log n)\\). The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def push(self, val: int):\n    \"\"\"Element enters heap\"\"\"\n    # Add node\n    self.max_heap.append(val)\n    # Heapify from bottom to top\n    self.sift_up(self.size() - 1)\n\ndef sift_up(self, i: int):\n    \"\"\"Starting from node i, heapify from bottom to top\"\"\"\n    while True:\n        # Get parent node of node i\n        p = self.parent(i)\n        # When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n            break\n        # Swap two nodes\n        self.swap(i, p)\n        # Loop upward heapify\n        i = p\n
my_heap.cpp
/* Element enters heap */\nvoid push(int val) {\n    // Add node\n    maxHeap.push_back(val);\n    // Heapify from bottom to top\n    siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Swap two nodes\n        swap(maxHeap[i], maxHeap[p]);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.java
/* Element enters heap */\nvoid push(int val) {\n    // Add node\n    maxHeap.add(val);\n    // Heapify from bottom to top\n    siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n            break;\n        // Swap two nodes\n        swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.cs
/* Element enters heap */\nvoid Push(int val) {\n    // Add node\n    maxHeap.Add(val);\n    // Heapify from bottom to top\n    SiftUp(Size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid SiftUp(int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = Parent(i);\n        // If 'past root node' or 'node needs no repair', end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // Swap two nodes\n        Swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.go
/* Element enters heap */\nfunc (h *maxHeap) push(val any) {\n    // Add node\n    h.data = append(h.data, val)\n    // Heapify from bottom to top\n    h.siftUp(len(h.data) - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfunc (h *maxHeap) siftUp(i int) {\n    for true {\n        // Get parent node of node i\n        p := h.parent(i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 || h.data[i].(int) <= h.data[p].(int) {\n            break\n        }\n        // Swap two nodes\n        h.swap(i, p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
my_heap.swift
/* Element enters heap */\nfunc push(val: Int) {\n    // Add node\n    maxHeap.append(val)\n    // Heapify from bottom to top\n    siftUp(i: size() - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfunc siftUp(i: Int) {\n    var i = i\n    while true {\n        // Get parent node of node i\n        let p = parent(i: i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if p < 0 || maxHeap[i] <= maxHeap[p] {\n            break\n        }\n        // Swap two nodes\n        swap(i: i, j: p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
my_heap.js
/* Element enters heap */\npush(val) {\n    // Add node\n    this.#maxHeap.push(val);\n    // Heapify from bottom to top\n    this.#siftUp(this.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\n#siftUp(i) {\n    while (true) {\n        // Get parent node of node i\n        const p = this.#parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break;\n        // Swap two nodes\n        this.#swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.ts
/* Element enters heap */\npush(val: number): void {\n    // Add node\n    this.maxHeap.push(val);\n    // Heapify from bottom to top\n    this.siftUp(this.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nsiftUp(i: number): void {\n    while (true) {\n        // Get parent node of node i\n        const p = this.parent(i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break;\n        // Swap two nodes\n        this.swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.dart
/* Element enters heap */\nvoid push(int val) {\n  // Add node\n  _maxHeap.add(val);\n  // Heapify from bottom to top\n  siftUp(size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(int i) {\n  while (true) {\n    // Get parent node of node i\n    int p = _parent(i);\n    // When \"crossing root node\" or \"node needs no repair\", end heapify\n    if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {\n      break;\n    }\n    // Swap two nodes\n    _swap(i, p);\n    // Loop upward heapify\n    i = p;\n  }\n}\n
my_heap.rs
/* Element enters heap */\nfn push(&mut self, val: i32) {\n    // Add node\n    self.max_heap.push(val);\n    // Heapify from bottom to top\n    self.sift_up(self.size() - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nfn sift_up(&mut self, mut i: usize) {\n    loop {\n        // Node i is already the heap root, end heapification\n        if i == 0 {\n            break;\n        }\n        // Get parent node of node i\n        let p = Self::parent(i);\n        // When \"node needs no repair\", end heapification\n        if self.max_heap[i] <= self.max_heap[p] {\n            break;\n        }\n        // Swap two nodes\n        self.swap(i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.c
/* Element enters heap */\nvoid push(MaxHeap *maxHeap, int val) {\n    // By default, should not add this many nodes\n    if (maxHeap->size == MAX_SIZE) {\n        printf(\"heap is full!\");\n        return;\n    }\n    // Add node\n    maxHeap->data[maxHeap->size] = val;\n    maxHeap->size++;\n\n    // Heapify from bottom to top\n    siftUp(maxHeap, maxHeap->size - 1);\n}\n\n/* Starting from node i, heapify from bottom to top */\nvoid siftUp(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Get parent node of node i\n        int p = parent(maxHeap, i);\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {\n            break;\n        }\n        // Swap two nodes\n        swap(maxHeap, i, p);\n        // Loop upward heapify\n        i = p;\n    }\n}\n
my_heap.kt
/* Element enters heap */\nfun push(_val: Int) {\n    // Add node\n    maxHeap.add(_val)\n    // Heapify from bottom to top\n    siftUp(size() - 1)\n}\n\n/* Starting from node i, heapify from bottom to top */\nfun siftUp(it: Int) {\n    // Kotlin function parameters are immutable, so create temporary variable\n    var i = it\n    while (true) {\n        // Get parent node of node i\n        val p = parent(i)\n        // When \"crossing root node\" or \"node needs no repair\", end heapify\n        if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n        // Swap two nodes\n        swap(i, p)\n        // Loop upward heapify\n        i = p\n    }\n}\n
my_heap.rb
### Push element to heap ###\ndef push(val)\n  # Add node\n  @max_heap << val\n  # Heapify from bottom to top\n  sift_up(size - 1)\nend\n\n### Heapify from node i, bottom to top ###\ndef sift_up(i)\n  loop do\n    # Get parent node of node i\n    p = parent(i)\n    # When \"crossing root node\" or \"node needs no repair\", end heapify\n    break if p < 0 || @max_heap[i] <= @max_heap[p]\n    # Swap two nodes\n    swap(i, p)\n    # Loop upward heapify\n    i = p\n  end\nend\n
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#4-removing-the-heap-top-element","level":3,"title":"4.   Removing the Heap Top Element","text":"

The heap top element is the root node of the binary tree, which is the first element of the list. If we directly remove the first element from the list, all node indexes in the binary tree would change, making subsequent repair with heapify difficult. To minimize changes in element indexes, we use the following steps.

  1. Swap the heap top element with the heap bottom element (swap the root node with the rightmost leaf node).
  2. After swapping, remove the heap bottom from the list (note that since we've swapped, we're actually removing the original heap top element).
  3. Starting from the root node, perform heapify from top to bottom.

As shown in Figure 8-4, the direction of \"top-to-bottom heapify\" is opposite to \"bottom-to-top heapify\". We compare the root node's value with its two children and swap it with the largest child. Then loop this operation until we pass a leaf node or encounter a node that doesn't need swapping.

<1><2><3><4><5><6><7><8><9><10>

Figure 8-4   Steps of removing the heap top element

Similar to the element insertion operation, the time complexity of the heap top element removal operation is also \\(O(\\log n)\\). The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def pop(self) -> int:\n    \"\"\"Element exits heap\"\"\"\n    # Handle empty case\n    if self.is_empty():\n        raise IndexError(\"Heap is empty\")\n    # Swap root node with rightmost leaf node (swap first element with last element)\n    self.swap(0, self.size() - 1)\n    # Delete node\n    val = self.max_heap.pop()\n    # Heapify from top to bottom\n    self.sift_down(0)\n    # Return top element\n    return val\n\ndef sift_down(self, i: int):\n    \"\"\"Starting from node i, heapify from top to bottom\"\"\"\n    while True:\n        # Find node with largest value among i, l, r, denoted as ma\n        l, r, ma = self.left(i), self.right(i), i\n        if l < self.size() and self.max_heap[l] > self.max_heap[ma]:\n            ma = l\n        if r < self.size() and self.max_heap[r] > self.max_heap[ma]:\n            ma = r\n        # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        if ma == i:\n            break\n        # Swap two nodes\n        self.swap(i, ma)\n        # Loop downward heapify\n        i = ma\n
my_heap.cpp
/* Element exits heap */\nvoid pop() {\n    // Handle empty case\n    if (isEmpty()) {\n        throw out_of_range(\"Heap is empty\");\n    }\n    // Delete node\n    swap(maxHeap[0], maxHeap[size() - 1]);\n    // Remove node\n    maxHeap.pop_back();\n    // Return top element\n    siftDown(0);\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // Swap two nodes\n        if (ma == i)\n            break;\n        swap(maxHeap[i], maxHeap[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.java
/* Element exits heap */\nint pop() {\n    // Handle empty case\n    if (isEmpty())\n        throw new IndexOutOfBoundsException();\n    // Delete node\n    swap(0, size() - 1);\n    // Remove node\n    int val = maxHeap.remove(size() - 1);\n    // Return top element\n    siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap.get(l) > maxHeap.get(ma))\n            ma = l;\n        if (r < size() && maxHeap.get(r) > maxHeap.get(ma))\n            ma = r;\n        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.cs
/* Element exits heap */\nint Pop() {\n    // Handle empty case\n    if (IsEmpty())\n        throw new IndexOutOfRangeException();\n    // Delete node\n    Swap(0, Size() - 1);\n    // Remove node\n    int val = maxHeap.Last();\n    maxHeap.RemoveAt(Size() - 1);\n    // Return top element\n    SiftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid SiftDown(int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = Left(i), r = Right(i), ma = i;\n        if (l < Size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < Size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // If 'node i is largest' or 'past leaf node', end heapify\n        if (ma == i) break;\n        // Swap two nodes\n        Swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.go
/* Element exits heap */\nfunc (h *maxHeap) pop() any {\n    // Handle empty case\n    if h.isEmpty() {\n        fmt.Println(\"error\")\n        return nil\n    }\n    // Delete node\n    h.swap(0, h.size()-1)\n    // Remove node\n    val := h.data[len(h.data)-1]\n    h.data = h.data[:len(h.data)-1]\n    // Return top element\n    h.siftDown(0)\n\n    // Return heap top element\n    return val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfunc (h *maxHeap) siftDown(i int) {\n    for true {\n        // Find node with maximum value among nodes i, l, r, denoted as max\n        l, r, max := h.left(i), h.right(i), i\n        if l < h.size() && h.data[l].(int) > h.data[max].(int) {\n            max = l\n        }\n        if r < h.size() && h.data[r].(int) > h.data[max].(int) {\n            max = r\n        }\n        // Swap two nodes\n        if max == i {\n            break\n        }\n        // Swap two nodes\n        h.swap(i, max)\n        // Loop downwards heapification\n        i = max\n    }\n}\n
my_heap.swift
/* Element exits heap */\nfunc pop() -> Int {\n    // Handle empty case\n    if isEmpty() {\n        fatalError(\"Heap is empty\")\n    }\n    // Delete node\n    swap(i: 0, j: size() - 1)\n    // Remove node\n    let val = maxHeap.remove(at: size() - 1)\n    // Return top element\n    siftDown(i: 0)\n    // Return heap top element\n    return val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfunc siftDown(i: Int) {\n    var i = i\n    while true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let l = left(i: i)\n        let r = right(i: i)\n        var ma = i\n        if l < size(), maxHeap[l] > maxHeap[ma] {\n            ma = l\n        }\n        if r < size(), maxHeap[r] > maxHeap[ma] {\n            ma = r\n        }\n        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        swap(i: i, j: ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n
my_heap.js
/* Element exits heap */\npop() {\n    // Handle empty case\n    if (this.isEmpty()) throw new Error('Heap is empty');\n    // Delete node\n    this.#swap(0, this.size() - 1);\n    // Remove node\n    const val = this.#maxHeap.pop();\n    // Return top element\n    this.#siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\n#siftDown(i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        const l = this.#left(i),\n            r = this.#right(i);\n        let ma = i;\n        if (l < this.size() && this.#maxHeap[l] > this.#maxHeap[ma]) ma = l;\n        if (r < this.size() && this.#maxHeap[r] > this.#maxHeap[ma]) ma = r;\n        // Swap two nodes\n        if (ma === i) break;\n        // Swap two nodes\n        this.#swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.ts
/* Element exits heap */\npop(): number {\n    // Handle empty case\n    if (this.isEmpty()) throw new RangeError('Heap is empty.');\n    // Delete node\n    this.swap(0, this.size() - 1);\n    // Remove node\n    const val = this.maxHeap.pop();\n    // Return top element\n    this.siftDown(0);\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nsiftDown(i: number): void {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        const l = this.left(i),\n            r = this.right(i);\n        let ma = i;\n        if (l < this.size() && this.maxHeap[l] > this.maxHeap[ma]) ma = l;\n        if (r < this.size() && this.maxHeap[r] > this.maxHeap[ma]) ma = r;\n        // Swap two nodes\n        if (ma === i) break;\n        // Swap two nodes\n        this.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.dart
/* Element exits heap */\nint pop() {\n  // Handle empty case\n  if (isEmpty()) throw Exception('Heap is empty');\n  // Delete node\n  _swap(0, size() - 1);\n  // Remove node\n  int val = _maxHeap.removeLast();\n  // Return top element\n  siftDown(0);\n  // Return heap top element\n  return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(int i) {\n  while (true) {\n    // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n    int l = _left(i);\n    int r = _right(i);\n    int ma = i;\n    if (l < size() && _maxHeap[l] > _maxHeap[ma]) ma = l;\n    if (r < size() && _maxHeap[r] > _maxHeap[ma]) ma = r;\n    // Swap two nodes\n    if (ma == i) break;\n    // Swap two nodes\n    _swap(i, ma);\n    // Loop downwards heapification\n    i = ma;\n  }\n}\n
my_heap.rs
/* Element exits heap */\nfn pop(&mut self) -> i32 {\n    // Handle empty case\n    if self.is_empty() {\n        panic!(\"index out of bounds\");\n    }\n    // Delete node\n    self.swap(0, self.size() - 1);\n    // Remove node\n    let val = self.max_heap.pop().unwrap();\n    // Return top element\n    self.sift_down(0);\n    // Return heap top element\n    val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfn sift_down(&mut self, mut i: usize) {\n    loop {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let (l, r, mut ma) = (Self::left(i), Self::right(i), i);\n        if l < self.size() && self.max_heap[l] > self.max_heap[ma] {\n            ma = l;\n        }\n        if r < self.size() && self.max_heap[r] > self.max_heap[ma] {\n            ma = r;\n        }\n        // Swap two nodes\n        if ma == i {\n            break;\n        }\n        // Swap two nodes\n        self.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n
my_heap.c
/* Element exits heap */\nint pop(MaxHeap *maxHeap) {\n    // Handle empty case\n    if (isEmpty(maxHeap)) {\n        printf(\"heap is empty!\");\n        return INT_MAX;\n    }\n    // Delete node\n    swap(maxHeap, 0, size(maxHeap) - 1);\n    // Remove node\n    int val = maxHeap->data[maxHeap->size - 1];\n    maxHeap->size--;\n    // Return top element\n    siftDown(maxHeap, 0);\n\n    // Return heap top element\n    return val;\n}\n\n/* Starting from node i, heapify from top to bottom */\nvoid siftDown(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // Find node with maximum value among nodes i, l, r, denoted as max\n        int l = left(maxHeap, i);\n        int r = right(maxHeap, i);\n        int max = i;\n        if (l < size(maxHeap) && maxHeap->data[l] > maxHeap->data[max]) {\n            max = l;\n        }\n        if (r < size(maxHeap) && maxHeap->data[r] > maxHeap->data[max]) {\n            max = r;\n        }\n        // Swap two nodes\n        if (max == i) {\n            break;\n        }\n        // Swap two nodes\n        swap(maxHeap, i, max);\n        // Loop downwards heapification\n        i = max;\n    }\n}\n
my_heap.kt
/* Element exits heap */\nfun pop(): Int {\n    // Handle empty case\n    if (isEmpty()) throw IndexOutOfBoundsException()\n    // Delete node\n    swap(0, size() - 1)\n    // Remove node\n    val _val = maxHeap.removeAt(size() - 1)\n    // Return top element\n    siftDown(0)\n    // Return heap top element\n    return _val\n}\n\n/* Starting from node i, heapify from top to bottom */\nfun siftDown(it: Int) {\n    // Kotlin function parameters are immutable, so create temporary variable\n    var i = it\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        val l = left(i)\n        val r = right(i)\n        var ma = i\n        if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n        if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n        // Swap two nodes\n        if (ma == i) break\n        // Swap two nodes\n        swap(i, ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n
my_heap.rb
### Pop element from heap ###\ndef pop\n  # Handle empty case\n  raise IndexError, \"Heap is empty\" if is_empty?\n  # Delete node\n  swap(0, size - 1)\n  # Remove node\n  val = @max_heap.pop\n  # Return top element\n  sift_down(0)\n  # Return heap top element\n  val\nend\n\n### Heapify from node i, top to bottom ###\ndef sift_down(i)\n  loop do\n    # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n    l, r, ma = left(i), right(i), i\n    ma = l if l < size && @max_heap[l] > @max_heap[ma]\n    ma = r if r < size && @max_heap[r] > @max_heap[ma]\n\n    # Swap two nodes\n    break if ma == i\n\n    # Swap two nodes\n    swap(i, ma)\n    # Loop downwards heapification\n    i = ma\n  end\nend\n
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/heap/#813-common-applications-of-heaps","level":2,"title":"8.1.3   Common Applications of Heaps","text":"
  • Priority queue: Heaps are typically the preferred data structure for implementing priority queues, with both enqueue and dequeue operations having a time complexity of \\(O(\\log n)\\), and the heap construction operation having \\(O(n)\\), all of which are highly efficient.
  • Heap sort: Given a set of data, we can build a heap with them and then continuously perform element removal operations to obtain sorted data. However, we usually use a more elegant approach to implement heap sort, as detailed in the \"Heap Sort\" chapter.
  • Getting the largest \\(k\\) elements: This is a classic algorithm problem and also a typical application, such as selecting the top 10 trending news for Weibo hot search, selecting the top 10 best-selling products, etc.
","path":["Chapter 8. Heap","8.1   Heap"],"tags":[]},{"location":"chapter_heap/summary/","level":1,"title":"8.4   Summary","text":"","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • A heap is a complete binary tree that can be categorized as a max heap or min heap based on its property. The heap top element of a max heap (min heap) is the largest (smallest).
  • A priority queue is defined as a queue with priority sorting, typically implemented using heaps.
  • Common heap operations and their corresponding time complexities include: element insertion \\(O(\\log n)\\), heap top element removal \\(O(\\log n)\\), and accessing the heap top element \\(O(1)\\).
  • Complete binary trees are well-suited for array representation, so we typically use arrays to store heaps.
  • Heapify operations are used to maintain the heap property and are employed in both element insertion and removal operations.
  • The time complexity of building a heap with \\(n\\) input elements can be optimized to \\(O(n)\\), which is highly efficient.
  • Top-k is a classic algorithm problem that can be efficiently solved using the heap data structure, with a time complexity of \\(O(n \\log k)\\).
","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Are the \"heap\" in data structures and the \"heap\" in memory management the same concept?

The two are not the same concept; they just happen to share the name \"heap.\" The heap in computer system memory is part of dynamic memory allocation, where programs can use it to store data during runtime. Programs can request a certain amount of heap memory to store complex structures such as objects and arrays. When this data is no longer needed, the program needs to release this memory to prevent memory leaks. Compared to stack memory, heap memory management and usage require more caution, as improper use can lead to issues such as memory leaks and dangling pointers.

","path":["Chapter 8. Heap","8.4   Summary"],"tags":[]},{"location":"chapter_heap/top_k/","level":1,"title":"8.3   Top-K Problem","text":"

Question

Given an unordered array nums of length \\(n\\), return the largest \\(k\\) elements in the array.

For this problem, we'll first introduce two solutions with relatively straightforward approaches, then introduce a more efficient heap-based solution.

","path":["Chapter 8. Heap","8.3   Top-K Problem"],"tags":[]},{"location":"chapter_heap/top_k/#831-method-1-iterative-selection","level":2,"title":"8.3.1   Method 1: Iterative Selection","text":"

We can perform \\(k\\) rounds of traversal as shown in Figure 8-6, extracting the \\(1^{st}\\), \\(2^{nd}\\), \\(\\dots\\), \\(k^{th}\\) largest elements in each round, with a time complexity of \\(O(nk)\\).

This method is only suitable when \\(k \\ll n\\), because when \\(k\\) is close to \\(n\\), the time complexity approaches \\(O(n^2)\\), which is very time-consuming.

Figure 8-6   Traversing to find the largest k elements

Tip

When \\(k = n\\), we can obtain a complete sorted sequence, which is equivalent to the \"selection sort\" algorithm.

","path":["Chapter 8. Heap","8.3   Top-K Problem"],"tags":[]},{"location":"chapter_heap/top_k/#832-method-2-sorting","level":2,"title":"8.3.2   Method 2: Sorting","text":"

As shown in Figure 8-7, we can first sort the array nums, then return the rightmost \\(k\\) elements, with a time complexity of \\(O(n \\log n)\\).

Clearly, this method \"overachieves\" the task, as we only need to find the largest \\(k\\) elements, without needing to sort the other elements.

Figure 8-7   Sorting to find the largest k elements

","path":["Chapter 8. Heap","8.3   Top-K Problem"],"tags":[]},{"location":"chapter_heap/top_k/#833-method-3-heap","level":2,"title":"8.3.3   Method 3: Heap","text":"

We can solve the Top-k problem more efficiently using heaps, with the process shown in Figure 8-8.

  1. Initialize a min heap, where the heap top element is the smallest.
  2. First, insert the first \\(k\\) elements of the array into the heap in sequence.
  3. Starting from the \\((k + 1)^{th}\\) element, if the current element is greater than the heap top element, remove the heap top element and insert the current element into the heap.
  4. After traversal is complete, the heap contains the largest \\(k\\) elements.
<1><2><3><4><5><6><7><8><9>

Figure 8-8   Finding the largest k elements using a heap

Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby top_k.py
def top_k_heap(nums: list[int], k: int) -> list[int]:\n    \"\"\"Find the largest k elements in array based on heap\"\"\"\n    # Initialize min heap\n    heap = []\n    # Enter the first k elements of array into heap\n    for i in range(k):\n        heapq.heappush(heap, nums[i])\n    # Starting from the (k+1)th element, maintain heap length as k\n    for i in range(k, len(nums)):\n        # If current element is greater than top element, top element exits heap, current element enters heap\n        if nums[i] > heap[0]:\n            heapq.heappop(heap)\n            heapq.heappush(heap, nums[i])\n    return heap\n
top_k.cpp
/* Find the largest k elements in array based on heap */\npriority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {\n    // Python's heapq module implements min heap by default\n    priority_queue<int, vector<int>, greater<int>> heap;\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.push(nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.size(); i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.top()) {\n            heap.pop();\n            heap.push(nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.java
/* Find the largest k elements in array based on heap */\nQueue<Integer> topKHeap(int[] nums, int k) {\n    // Python's heapq module implements min heap by default\n    Queue<Integer> heap = new PriorityQueue<Integer>();\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.offer(nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.peek()) {\n            heap.poll();\n            heap.offer(nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.cs
/* Find the largest k elements in array based on heap */\nPriorityQueue<int, int> TopKHeap(int[] nums, int k) {\n    // Python's heapq module implements min heap by default\n    PriorityQueue<int, int> heap = new();\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        heap.Enqueue(nums[i], nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < nums.Length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.Peek()) {\n            heap.Dequeue();\n            heap.Enqueue(nums[i], nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.go
/* Find the largest k elements in array based on heap */\nfunc topKHeap(nums []int, k int) *minHeap {\n    // Python's heapq module implements min heap by default\n    h := &minHeap{}\n    heap.Init(h)\n    // Enter the first k elements of array into heap\n    for i := 0; i < k; i++ {\n        heap.Push(h, nums[i])\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for i := k; i < len(nums); i++ {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if nums[i] > h.Top().(int) {\n            heap.Pop(h)\n            heap.Push(h, nums[i])\n        }\n    }\n    return h\n}\n
top_k.swift
/* Find the largest k elements in array based on heap */\nfunc topKHeap(nums: [Int], k: Int) -> [Int] {\n    // Initialize min heap and build heap with first k elements\n    var heap = Heap(nums.prefix(k))\n    // Starting from the (k+1)th element, maintain heap length as k\n    for i in nums.indices.dropFirst(k) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if nums[i] > heap.min()! {\n            _ = heap.removeMin()\n            heap.insert(nums[i])\n        }\n    }\n    return heap.unordered\n}\n
top_k.js
/* Element enters heap */\nfunction pushMinHeap(maxHeap, val) {\n    // Negate element\n    maxHeap.push(-val);\n}\n\n/* Element exits heap */\nfunction popMinHeap(maxHeap) {\n    // Negate element\n    return -maxHeap.pop();\n}\n\n/* Access top element */\nfunction peekMinHeap(maxHeap) {\n    // Negate element\n    return -maxHeap.peek();\n}\n\n/* Extract elements from heap */\nfunction getMinHeap(maxHeap) {\n    // Negate element\n    return maxHeap.getMaxHeap().map((num) => -num);\n}\n\n/* Find the largest k elements in array based on heap */\nfunction topKHeap(nums, k) {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    const maxHeap = new MaxHeap([]);\n    // Enter the first k elements of array into heap\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (let i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Return elements in heap\n    return getMinHeap(maxHeap);\n}\n
top_k.ts
/* Element enters heap */\nfunction pushMinHeap(maxHeap: MaxHeap, val: number): void {\n    // Negate element\n    maxHeap.push(-val);\n}\n\n/* Element exits heap */\nfunction popMinHeap(maxHeap: MaxHeap): number {\n    // Negate element\n    return -maxHeap.pop();\n}\n\n/* Access top element */\nfunction peekMinHeap(maxHeap: MaxHeap): number {\n    // Negate element\n    return -maxHeap.peek();\n}\n\n/* Extract elements from heap */\nfunction getMinHeap(maxHeap: MaxHeap): number[] {\n    // Negate element\n    return maxHeap.getMaxHeap().map((num: number) => -num);\n}\n\n/* Find the largest k elements in array based on heap */\nfunction topKHeap(nums: number[], k: number): number[] {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    const maxHeap = new MaxHeap([]);\n    // Enter the first k elements of array into heap\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (let i = k; i < nums.length; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // Return elements in heap\n    return getMinHeap(maxHeap);\n}\n
top_k.dart
/* Find the largest k elements in array based on heap */\nMinHeap topKHeap(List<int> nums, int k) {\n  // Initialize min heap, push first k elements of array to heap\n  MinHeap heap = MinHeap(nums.sublist(0, k));\n  // Starting from the (k+1)th element, maintain heap length as k\n  for (int i = k; i < nums.length; i++) {\n    // If current element is greater than top element, top element exits heap, current element enters heap\n    if (nums[i] > heap.peek()) {\n      heap.pop();\n      heap.push(nums[i]);\n    }\n  }\n  return heap;\n}\n
top_k.rs
/* Find the largest k elements in array based on heap */\nfn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {\n    // BinaryHeap is a max heap, use Reverse to negate elements to implement min heap\n    let mut heap = BinaryHeap::<Reverse<i32>>::new();\n    // Enter the first k elements of array into heap\n    for &num in nums.iter().take(k) {\n        heap.push(Reverse(num));\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for &num in nums.iter().skip(k) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if num > heap.peek().unwrap().0 {\n            heap.pop();\n            heap.push(Reverse(num));\n        }\n    }\n    heap\n}\n
top_k.c
/* Element enters heap */\nvoid pushMinHeap(MaxHeap *maxHeap, int val) {\n    // Negate element\n    push(maxHeap, -val);\n}\n\n/* Element exits heap */\nint popMinHeap(MaxHeap *maxHeap) {\n    // Negate element\n    return -pop(maxHeap);\n}\n\n/* Access top element */\nint peekMinHeap(MaxHeap *maxHeap) {\n    // Negate element\n    return -peek(maxHeap);\n}\n\n/* Extract elements from heap */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Negate all heap elements and store in res array\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n/* Extract elements from heap */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // Negate all heap elements and store in res array\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n// Function to find k largest elements in array using heap\nint *topKHeap(int *nums, int sizeNums, int k) {\n    // Python's heapq module implements min heap by default\n    // Note: We negate all heap elements to simulate min heap using max heap\n    int *empty = (int *)malloc(0);\n    MaxHeap *maxHeap = newMaxHeap(empty, 0);\n    // Enter the first k elements of array into heap\n    for (int i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (int i = k; i < sizeNums; i++) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    int *res = getMinHeap(maxHeap);\n    // Free memory\n    delMaxHeap(maxHeap);\n    return res;\n}\n
top_k.kt
/* Find the largest k elements in array based on heap */\nfun topKHeap(nums: IntArray, k: Int): Queue<Int> {\n    // Python's heapq module implements min heap by default\n    val heap = PriorityQueue<Int>()\n    // Enter the first k elements of array into heap\n    for (i in 0..<k) {\n        heap.offer(nums[i])\n    }\n    // Starting from the (k+1)th element, maintain heap length as k\n    for (i in k..<nums.size) {\n        // If current element is greater than top element, top element exits heap, current element enters heap\n        if (nums[i] > heap.peek()) {\n            heap.poll()\n            heap.offer(nums[i])\n        }\n    }\n    return heap\n}\n
top_k.rb
### Find largest k elements in array using heap ###\ndef top_k_heap(nums, k)\n  # Python's heapq module implements min heap by default\n  # Note: We negate all heap elements to simulate min heap using max heap\n  max_heap = MaxHeap.new([])\n\n  # Enter the first k elements of array into heap\n  for i in 0...k\n    push_min_heap(max_heap, nums[i])\n  end\n\n  # Starting from the (k+1)th element, maintain heap length as k\n  for i in k...nums.length\n    # If current element is greater than top element, top element exits heap, current element enters heap\n    if nums[i] > peek_min_heap(max_heap)\n      pop_min_heap(max_heap)\n      push_min_heap(max_heap, nums[i])\n    end\n  end\n\n  get_min_heap(max_heap)\nend\n

A total of \\(n\\) rounds of heap insertions and removals are performed, with the heap's maximum length being \\(k\\), so the time complexity is \\(O(n \\log k)\\). This method is very efficient; when \\(k\\) is small, the time complexity approaches \\(O(n)\\); when \\(k\\) is large, the time complexity does not exceed \\(O(n \\log n)\\).

Additionally, this method is suitable for dynamic data stream scenarios. By continuously adding data, we can maintain the elements in the heap, thus achieving dynamic updates of the largest \\(k\\) elements.

","path":["Chapter 8. Heap","8.3   Top-K Problem"],"tags":[]},{"location":"chapter_hello_algo/","level":1,"title":"Before Starting","text":"

A few years ago, I shared the \"Sword for Offer\" problem solutions on LeetCode, receiving encouragement and support from many readers. During interactions with readers, the most frequently asked question I encountered was \"how to get started with algorithms.\" Gradually, I developed a keen interest in this question.

Diving straight into problem-solving seems to be the most popular approach—it's simple, direct, and effective. However, problem-solving is like playing Minesweeper: those with strong self-learning abilities can successfully defuse the mines one by one, while those with insufficient foundations may end up bruised and battered, retreating step by step in frustration. Reading through textbooks is also a common practice, but for job seekers, graduation theses, resume submissions, and preparations for written tests and interviews have already consumed most of their energy, making working through thick books an arduous challenge.

If you're facing similar struggles, then it's fortunate that this book has \"found\" you. This book is my answer to this question—even if it may not be the optimal solution, it is at least a positive attempt. While this book alone won't directly land you a job offer, it will guide you to explore the \"knowledge map\" of data structures and algorithms, help you understand the shapes, sizes, and distributions of different \"mines,\" and enable you to master various \"mine-clearing methods.\" With these skills, I believe you can tackle problems and read technical literature more confidently, gradually building a complete knowledge system.

I deeply agree with Professor Feynman's words: \"Knowledge isn't free. You have to pay attention.\" In this sense, this book is not entirely \"free.\" In order to live up to the precious \"attention\" you invest in this book, I will do my utmost and devote my greatest \"attention\" to completing this work.

I'm acutely aware of my limited knowledge and shallow expertise. Although the content of this book has been refined over a period of time, there are certainly still many errors, and I sincerely welcome critiques and corrections from teachers and fellow students.

Hello, Algorithms!

The advent of computers has brought tremendous changes to the world. With their high-speed computing capabilities and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics in video games, the intelligent decision-making in autonomous driving, AlphaGo's brilliant Go matches, or ChatGPT's natural interactions, these applications are all exquisite interpretations of algorithms on computers.

In fact, before the advent of computers, algorithms and data structures already existed in every corner of the world. Early algorithms were relatively simple, such as ancient counting methods and tool-making procedures. As civilization progressed, algorithms gradually became more refined and complex. From the ingenious craftsmanship of master artisans, to industrial products that liberate productive forces, to the scientific laws governing the operation of the universe, behind almost every ordinary or astonishing thing lies ingenious algorithmic thinking.

Similarly, data structures are everywhere: from large-scale social networks to small subway systems, many systems can be modeled as \"graphs\"; from a nation to a family, the primary organizational forms of society exhibit characteristics of \"trees\"; winter clothing is like a \"stack,\" where the first item put on is the last to be taken off; a badminton tube is like a \"queue,\" with items inserted at one end and retrieved from the other; a dictionary is like a \"hash table,\" enabling quick lookup of target entries.

This book aims to help readers understand the core concepts of algorithms and data structures through clear and accessible animated illustrations and runnable code examples, and to implement them through programming. Building on this foundation, the book endeavors to reveal the vivid manifestations of algorithms in the complex world and showcase the beauty of algorithms. I hope this book can be of help to you!

","path":["Before Starting"],"tags":[]},{"location":"chapter_introduction/","level":1,"title":"Chapter 1.   Encounter with Algorithms","text":"

Abstract

A young girl dances gracefully, intertwined with data, her skirt flowing with the melody of algorithms.

She invites you to dance with her. Follow her steps closely and enter the world of algorithms, full of logic and beauty.

","path":["Chapter 1. Encounter With Algorithms","Chapter 1.   Encounter with Algorithms"],"tags":[]},{"location":"chapter_introduction/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 1.1   Algorithms Are Everywhere
  • 1.2   What Is an Algorithm
  • 1.3   Summary
","path":["Chapter 1. Encounter With Algorithms","Chapter 1.   Encounter with Algorithms"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   Algorithms Are Everywhere","text":"

When we hear the term \"algorithm,\" we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives.

Before we start discussing about algorithms officially, there's an interesting fact worth sharing: you've learned many algorithms unconsciously and are used to applying them in your daily life. Here, I will give a few specific examples to prove this point.

Example 1: Looking Up a Dictionary. In an English dictionary, words are listed alphabetically. Assuming we're searching for a word that starts with the letter \\(r\\), this is typically done in the following way:

  1. Open the dictionary to about halfway and check the first vocabulary of the page, let's say the letter starts with \\(m\\).
  2. Since \\(r\\) comes after \\(m\\) in the alphabet, the first half can be ignored and the search space is narrowed down to the second half.
  3. Repeat steps 1. and 2. until you find the page where the word starts with \\(r\\).
<1><2><3><4><5>

Figure 1-1   Process of looking up a dictionary

Looking up a dictionary, an essential skill for elementary school students is actually the famous \"Binary Search\" algorithm. From a data structure perspective, we can consider the dictionary as a sorted \"array\"; from an algorithmic perspective, the series of actions taken to look up a word in the dictionary can be viewed as the algorithm \"Binary Search.\"

Example 2: Organizing Card Deck. When playing cards, we need to arrange the cards in our hands in ascending order, as shown in the following process.

  1. Divide the playing cards into \"ordered\" and \"unordered\" sections, assuming initially the leftmost card is already in order.
  2. Take out a card from the unordered section and insert it into the correct position in the ordered section; after this, the leftmost two cards are in order.
  3. Repeat step 2 until all cards are in order.

Figure 1-2   Process of sorting a deck of cards

The above method of organizing playing cards is practically the \"Insertion Sort\" algorithm, which is very efficient for small datasets. Many programming languages' sorting functions include the insertion sort.

Example 3: Making Change. Assume making a purchase of \\(69\\) at a supermarket. If you give the cashier \\(100\\), they will need to provide you with \\(31\\) in change. This process can be clearly understood as illustrated in Figure 1-3.

  1. The options are currencies valued below \\(31\\), including \\(1\\), \\(5\\), \\(10\\), and \\(20\\).
  2. Take out the largest \\(20\\) from the options, leaving \\(31 - 20 = 11\\).
  3. Take out the largest \\(10\\) from the remaining options, leaving \\(11 - 10 = 1\\).
  4. Take out the largest \\(1\\) from the remaining options, leaving \\(1 - 1 = 0\\).
  5. Complete change-making, the solution is \\(20 + 10 + 1 = 31\\).

Figure 1-3   Process of making change

In the steps described, we choose the best option at each stage by utilizing the largest denomination available, which leads to an effective change-making strategy. From a data structures and algorithms perspective, this approach is known as a \"Greedy\" algorithm.

From cooking a meal to interstellar travel, almost all problem-solving involves algorithms. The advent of computers allows us to store data structures in memory and write code to call the CPU and GPU to execute algorithms. In this way, we can transfer real-life problems to computers and solve various complex issues in a more efficient way.

Tip

If you are still confused about concepts like data structures, algorithms, arrays, and binary searches, I encourage you to keep reading. This book will gently guide you into the realm of understanding data structures and algorithms.

","path":["Chapter 1. Encounter With Algorithms","1.1   Algorithms Are Everywhere"],"tags":[]},{"location":"chapter_introduction/summary/","level":1,"title":"1.3   Summary","text":"","path":["Chapter 1. Encounter With Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Algorithms are ubiquitous in daily life and are not distant, esoteric knowledge. In fact, we have already learned many algorithms unconsciously and use them to solve problems big and small in life.
  • The principle of looking up a dictionary is consistent with the binary search algorithm. Binary search embodies the important algorithmic idea of divide and conquer.
  • The process of organizing playing cards is very similar to the insertion sort algorithm. Insertion sort is suitable for sorting small datasets.
  • The steps of making change are essentially a greedy algorithm, where the best choice is made at each step based on the current situation.
  • An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time, while a data structure is the way computers organize and store data.
  • Data structures and algorithms are closely connected. Data structures are the foundation of algorithms, and algorithms breathe life into data structures.
  • We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the shape and connection method of the blocks represent the data structure, and the steps to assemble the blocks correspond to the algorithm.
","path":["Chapter 1. Encounter With Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: As a programmer, I have never used algorithms to solve problems in my daily work. Common algorithms are already encapsulated by programming languages and can be used directly. Does this mean that the problems in our work have not yet reached the level where algorithms are needed?

If we compare specific work skills to \"techniques\" in martial arts, then fundamental subjects should be more like \"internal skills\".

I believe the significance of learning algorithms (and other fundamental subjects) is not to implement them from scratch at work, but rather to be able to make professional reactions and judgments when solving problems based on the knowledge learned, thereby improving the overall quality of work. Here is a simple example. Every programming language has a built-in sorting function:

  • If we have not studied data structures and algorithms, we might simply feed any given data to this sorting function. It runs smoothly with good performance, and there doesn't seem to be any problem.
  • But if we have studied algorithms, we would know that the time complexity of the built-in sorting function is \\(O(n \\log n)\\). However, if the given data consists of integers with a fixed number of digits (such as student IDs), we can use the more efficient \"radix sort\", reducing the time complexity to \\(O(nk)\\), where \\(k\\) is the number of digits. When the data volume is very large, the saved running time can create significant value (reduced costs, improved experience, etc.).

In the field of engineering, a large number of problems are difficult to reach optimal solutions, and many problems are only solved \"approximately\". The difficulty of a problem depends on one hand on the nature of the problem itself, and on the other hand on the knowledge reserve of the person observing the problem. The more complete a person's knowledge and the more experience they have, the deeper their analysis of the problem will be, and the more elegantly the problem can be solved.

","path":["Chapter 1. Encounter With Algorithms","1.3   Summary"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/","level":1,"title":"1.2   What Is an Algorithm","text":"","path":["Chapter 1. Encounter With Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#121-algorithm-definition","level":2,"title":"1.2.1   Algorithm Definition","text":"

An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time. It has the following characteristics.

  • The problem is well-defined, with clear input and output definitions.
  • It is feasible and can be completed within a finite number of steps, time, and memory space.
  • Each step has a definite meaning, and under the same input and operating conditions, the output is always the same.
","path":["Chapter 1. Encounter With Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#122-data-structure-definition","level":2,"title":"1.2.2   Data Structure Definition","text":"

A data structure is a way of organizing and storing data, covering the data content, relationships between data, and methods for data operations. It has the following design objectives.

  • Occupy as little space as possible to save computer memory.
  • Data operations should be as fast as possible, covering data access, addition, deletion, update, etc.
  • Provide a concise data representation and logical information so that algorithms can run efficiently.

Data structure design is a process full of trade-offs. If we want to achieve improvements in one aspect, we often need to make compromises in another aspect. Here are two examples.

  • Compared to arrays, linked lists are more convenient for data addition and deletion operations but sacrifice data access speed.
  • Compared to linked lists, graphs provide richer logical information but require larger memory space.
","path":["Chapter 1. Encounter With Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123-the-relationship-between-data-structures-and-algorithms","level":2,"title":"1.2.3   The Relationship Between Data Structures and Algorithms","text":"

As shown in Figure 1-4, data structures and algorithms are highly related and tightly coupled, specifically manifested in the following three aspects.

  • Data structures are the foundation of algorithms. Data structures provide algorithms with structured storage of data and methods for operating on data.
  • Algorithms breathe life into data structures. Data structures themselves only store data information; combined with algorithms, they can solve specific problems.
  • Algorithms can usually be implemented based on different data structures, but execution efficiency may vary greatly. Choosing the appropriate data structure is key.

Figure 1-4   The relationship between data structures and algorithms

Data structures and algorithms are like assembling building blocks as shown in Figure 1-5. A set of building blocks, in addition to containing many parts, also comes with detailed assembly instructions. By following the instructions step by step, we can assemble an exquisite building block model.

Figure 1-5   Assembling blocks

The detailed correspondence between the two is shown in Table 1-1.

Table 1-1   Comparing data structures and algorithms to assembling building blocks

Data structures and algorithms Assembling building blocks Input data Unassembled building blocks Data structure Organization form of building blocks, including shape, size, connection method, etc. Algorithm A series of operational steps to assemble the blocks into the target form Output data Building block model

It is worth noting that data structures and algorithms are independent of programming languages. For this reason, this book is able to provide implementations based on multiple programming languages.

Conventional abbreviation

In actual discussions, we usually abbreviate \"data structures and algorithms\" as \"algorithms\". For example, the well-known LeetCode algorithm problems actually examine knowledge of both data structures and algorithms.

","path":["Chapter 1. Encounter With Algorithms","1.2   What Is an Algorithm"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"Chapter 0.   Preface","text":"

Abstract

Algorithms are like a beautiful symphony, each line of code flows like a melody.

May this book gently resonate in your mind, leaving a unique and profound melody.

","path":["Chapter 0. Preface","Chapter 0.   Preface"],"tags":[]},{"location":"chapter_preface/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 0.1   About This Book
  • 0.2   How to Use This Book
  • 0.3   Summary
","path":["Chapter 0. Preface","Chapter 0.   Preface"],"tags":[]},{"location":"chapter_preface/about_the_book/","level":1,"title":"0.1   About This Book","text":"

This project aims to create an open-source, free, beginner-friendly introductory tutorial on data structures and algorithms.

  • The entire book uses animated illustrations, with clear and easy-to-understand content and a smooth learning curve, guiding beginners to explore the knowledge map of data structures and algorithms.
  • The source code can be run with one click, helping readers improve their programming skills through practice and understand how algorithms work and the underlying implementation of data structures.
  • We encourage readers to learn from each other, and everyone is welcome to ask questions and share insights in the comments section, making progress together through discussion and exchange.
","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#011-target-audience","level":2,"title":"0.1.1   Target Audience","text":"

If you are an algorithm beginner who has never been exposed to algorithms, or if you already have some problem-solving experience and have a vague understanding of data structures and algorithms, oscillating between knowing and not knowing, then this book is tailor-made for you!

If you have already accumulated a certain amount of problem-solving experience and are familiar with most question types, this book can help you review and organize your algorithm knowledge system, and the repository's source code can be used as a \"problem-solving toolkit\" or \"algorithm dictionary.\"

If you are an algorithm \"expert,\" we look forward to receiving your valuable suggestions, or participating in creation together.

Prerequisites

You need to have at least a programming foundation in any language, and be able to read and write simple code.

","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#012-content-structure","level":2,"title":"0.1.2   Content Structure","text":"

The main content of this book is shown in Figure 0-1.

  • Complexity analysis: Evaluation dimensions and methods for data structures and algorithms. Methods for calculating time complexity and space complexity, common types, examples, etc.
  • Data structures: Classification methods for basic data types and data structures. The definition, advantages and disadvantages, common operations, common types, typical applications, implementation methods, etc. of data structures such as arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs.
  • Algorithms: The definition, advantages and disadvantages, efficiency, application scenarios, problem-solving steps, and example problems of algorithms such as searching, sorting, divide and conquer, backtracking, dynamic programming, and greedy algorithms.

Figure 0-1   Main content of this book

","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/about_the_book/#013-acknowledgements","level":2,"title":"0.1.3   Acknowledgements","text":"

This book has been continuously improved through the joint efforts of many contributors in the open-source community. Thanks to every contributor who invested time and effort, they are (in the order automatically generated by GitHub): krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, IsChristina, xBLACKICEx, guowei-gong, Cathay-Chen, pengchzn, QiLOL, magentaqin, hello-ikun, JoseHung, qualifier1024, thomasq0, sunshinesDL, L-Super, Guanngxu, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, Shyam-Chen, theNefelibatas, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, MolDuM, Nigh, Dr-XYZ, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, beatrix-chan, DullSword, xjr7670, jiaxianhua, qq909244296, iStig, boloboloda, hts0000, gledfish, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, llql1211, fbigm, echo1937, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, GaochaoZhu, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Allen-Scai, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, zhongfq, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Keynman, KeiichiKasai, and KawaiiAsh.

The code review work for this book was completed by coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (in alphabetical order). Thanks to them for the time and effort they put in, it is they who ensure the standardization and unity of code in various languages.

The Traditional Chinese version of this book was reviewed by Shyam-Chen and Dr-XYZ, the English version was reviewed by yuelinxin, K3v123, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn, thomasq0 and magentaqin, and the Japanese edition was reviewed by eltociear. It is because of their continuous contributions that this book can serve a wider readership, and we thank them.

The ePub ebook generation tool for this book was developed by zhongfq. We thank him for his contribution, which provides readers with a more flexible way to read.

During the creation of this book, I received help from many people.

  • Thanks to my mentor at the company, Dr. Li Xi, who encouraged me to \"take action quickly\" during a conversation, strengthening my determination to write this book;
  • Thanks to my girlfriend Bubble as the first reader of this book, who provided many valuable suggestions from the perspective of an algorithm beginner, making this book more suitable for novices to read;
  • Thanks to Tengbao, Qibao, and Feibao for coming up with a creative name for this book, evoking everyone's fond memories of writing their first line of code \"Hello World!\";
  • Thanks to Xiaoquan for providing professional help in intellectual property rights, which played an important role in the improvement of this open-source book;
  • Thanks to Sutong for designing the beautiful cover and logo for this book, and for patiently making revisions multiple times driven by my obsessive-compulsive disorder;
  • Thanks to @squidfunk for the typesetting suggestions, as well as for developing the open-source documentation theme Material-for-MkDocs.

During the writing process, I read many textbooks and articles on data structures and algorithms. These works provided excellent examples for this book and ensured the accuracy and quality of the book's content. I would like to thank all the teachers and predecessors for their outstanding contributions!

This book advocates a learning method that combines hands and brain, and in this regard I was deeply inspired by Dive into Deep Learning. I highly recommend this excellent work to all readers.

Heartfelt thanks to my parents, it is your support and encouragement that has given me the opportunity to do this interesting thing.

","path":["Chapter 0. Preface","0.1   About This Book"],"tags":[]},{"location":"chapter_preface/suggestions/","level":1,"title":"0.2   How to Use This Book","text":"

Tip

For the best reading experience, it is recommended that you read through this section.

","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#021-writing-style-conventions","level":2,"title":"0.2.1   Writing Style Conventions","text":"
  • Titles marked with * are optional sections with relatively difficult content. If you have limited time, you can skip them first.
  • Technical terms will be in bold (in paper and PDF versions) or underlined (in web versions), such as array. It is recommended to memorize them for reading literature.
  • Key content and summary statements will be bolded, and such text deserves special attention.
  • Words and phrases with specific meanings will be marked with \"quotation marks\" to avoid ambiguity.
  • When it comes to nouns that are inconsistent between programming languages, this book uses Python as the standard, for example, using None to represent \"null\".
  • This book partially abandons the comment conventions of programming languages in favor of more compact content layout. Comments are mainly divided into three types: title comments, content comments, and multi-line comments.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
\"\"\"Title comment, used to label functions, classes, test cases, etc.\"\"\"\n\n# Content comment, used to explain code in detail\n\n\"\"\"\nMulti-line\ncomment\n\"\"\"\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n// Multi-line\n// comment\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
/* Title comment, used to label functions, classes, test cases, etc. */\n\n// Content comment, used to explain code in detail\n\n/**\n * Multi-line\n * comment\n */\n
### Title comment, used to label functions, classes, test cases, etc. ###\n\n# Content comment, used to explain code in detail\n\n# Multi-line\n# comment\n
","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#022-learning-efficiently-with-animated-illustrations","level":2,"title":"0.2.2   Learning Efficiently with Animated Illustrations","text":"

Compared to text, videos and images have higher information density and structural organization, making them easier to understand. In this book, key and difficult knowledge will mainly be presented in the form of animated illustrations, with text serving as explanation and supplement.

If you find that a section of content provides animated illustrations as shown in Figure 0-2 while reading this book, please focus on the illustrations first, with text as a supplement, and combine the two to understand the content.

Figure 0-2   Example of animated illustrations

","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#023-deepening-understanding-through-code-practice","level":2,"title":"0.2.3   Deepening Understanding Through Code Practice","text":"

The accompanying code for this book is hosted in the GitHub repository. As shown in Figure 0-3, the source code comes with test cases and can be run with one click.

If time permits, it is recommended that you type out the code yourself. If you have limited study time, please at least read through and run all the code.

Compared to reading code, the process of writing code often brings more rewards. Learning by doing is the real learning.

Figure 0-3   Example of running code

The preliminary work for running code is mainly divided into three steps.

Step 1: Install the local programming environment. Please follow the tutorial shown in the appendix for installation. If already installed, you can skip this step.

Step 2: Clone or download the code repository. Visit the GitHub repository. If you have already installed Git, you can clone this repository with the following command:

git clone https://github.com/krahets/hello-algo.git\n

Of course, you can also click the \"Download ZIP\" button at the location shown in Figure 0-4 to directly download the code compressed package, and then extract it locally.

Figure 0-4   Clone repository and download code

Step 3: Run the source code. As shown in Figure 0-5, for code blocks with file names at the top, we can find the corresponding source code files in the codes folder of the repository. The source code files can be run with one click, which will help you save unnecessary debugging time and allow you to focus on learning content.

Figure 0-5   Code blocks and corresponding source code files

In addition to running code locally, the web version also supports visual running of Python code (implemented based on pythontutor). As shown in Figure 0-6, you can click \"Visual Run\" below the code block to expand the view and observe the execution process of the algorithm code; you can also click \"Full Screen View\" for a better viewing experience.

Figure 0-6   Visual running of Python code

","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#024-growing-together-through-questions-and-discussions","level":2,"title":"0.2.4   Growing Together Through Questions and Discussions","text":"

When reading this book, please do not easily skip knowledge points that you have not learned well. Feel free to ask your questions in the comments section, and my friends and I will do our best to answer you, and generally reply within two days.

As shown in Figure 0-7, the web version has a comments section at the bottom of each chapter. I hope you will pay more attention to the content of the comments section. On the one hand, you can learn about the problems that everyone encounters, thus checking for omissions and stimulating deeper thinking. On the other hand, I hope you can generously answer other friends' questions, share your insights, and help others progress.

Figure 0-7   Example of comments section

","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/suggestions/#025-algorithm-learning-roadmap","level":2,"title":"0.2.5   Algorithm Learning Roadmap","text":"

From an overall perspective, we can divide the process of learning data structures and algorithms into three stages.

  1. Stage 1: Algorithm introduction. We need to familiarize ourselves with the characteristics and usage of various data structures, and learn the principles, processes, uses, and efficiency of different algorithms.
  2. Stage 2: Practice algorithm problems. It is recommended to start with popular problems, and accumulate at least 100 problems first, to familiarize yourself with mainstream algorithm problems. When first practicing problems, \"knowledge forgetting\" may be a challenge, but rest assured, this is very normal. We can review problems according to the \"Ebbinghaus forgetting curve\", and usually after 3-5 rounds of repetition, we can firmly remember them. For recommended problem lists and practice plans, please see this GitHub repository.
  3. Stage 3: Building a knowledge system. In terms of learning, we can read algorithm column articles, problem-solving frameworks, and algorithm textbooks to continuously enrich our knowledge system. In terms of practicing problems, we can try advanced problem-solving strategies, such as categorization by topic, one problem multiple solutions, one solution multiple problems, etc. Related problem-solving insights can be found in various communities.

As shown in Figure 0-8, the content of this book mainly covers \"Stage 1\", aiming to help you more efficiently carry out Stage 2 and Stage 3 learning.

Figure 0-8   Algorithm learning roadmap

","path":["Chapter 0. Preface","0.2   How to Use This Book"],"tags":[]},{"location":"chapter_preface/summary/","level":1,"title":"0.3   Summary","text":"","path":["Chapter 0. Preface","0.3   Summary"],"tags":[]},{"location":"chapter_preface/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • The main audience of this book is algorithm beginners. If you already have a certain foundation, this book can help you systematically review algorithm knowledge, and the source code in the book can also be used as a \"problem-solving toolkit.\"
  • The content of the book mainly includes three parts: complexity analysis, data structures, and algorithms, covering most topics in this field.
  • For algorithm novices, reading an introductory book during the initial learning stage is crucial, as it can help you avoid many detours.
  • The animated illustrations in the book are usually used to introduce key and difficult knowledge. When reading this book, you should pay more attention to these contents.
  • Practice is the best way to learn programming. It is strongly recommended to run the source code and type the code yourself.
  • The web version of this book has a comments section for each chapter, where you are welcome to share your questions and insights at any time.
","path":["Chapter 0. Preface","0.3   Summary"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"References","text":"

[1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition).

[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

[3] Robert Sedgewick, et al. Algorithms (4th Edition).

[4] Yan Weimin. Data Structures (C Language Version).

[5] Deng Junhui. Data Structures (C++ Language Version, Third Edition).

[6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition).

[7] Cheng Jie. Conversational Data Structures.

[8] Wang Zheng. The Beauty of Data Structures and Algorithms.

[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

[10] Aston Zhang, et al. Dive into Deep Learning.

","path":["References"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"Chapter 10.   Searching","text":"

Abstract

Searching is an adventure into the unknown, where we may need to traverse every corner of the mysterious space, or we may be able to quickly lock onto the target.

In this journey of discovery, each exploration may yield an unexpected answer.

","path":["Chapter 10. Searching","Chapter 10.   Searching"],"tags":[]},{"location":"chapter_searching/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 10.1   Binary Search
  • 10.2   Binary Search Insertion
  • 10.3   Binary Search Edge Cases
  • 10.4   Hash Optimization Strategy
  • 10.5   Search Algorithms Revisited
  • 10.6   Summary
","path":["Chapter 10. Searching","Chapter 10.   Searching"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   Binary Search","text":"

Binary search is an efficient searching algorithm based on the divide-and-conquer strategy. It leverages the orderliness of data to reduce the search range by half in each round until the target element is found or the search interval becomes empty.

Question

Given an array nums of length \\(n\\) with elements arranged in ascending order and no duplicates, search for and return the index of element target in the array. If the array does not contain the element, return \\(-1\\). An example is shown in Figure 10-1.

Figure 10-1   Binary search example data

As shown in Figure 10-2, we first initialize pointers \\(i = 0\\) and \\(j = n - 1\\), pointing to the first and last elements of the array respectively, representing the search interval \\([0, n - 1]\\). Note that square brackets denote a closed interval, which includes the boundary values themselves.

Next, perform the following two steps in a loop:

  1. Calculate the midpoint index \\(m = \\lfloor {(i + j) / 2} \\rfloor\\), where \\(\\lfloor \\: \\rfloor\\) denotes the floor operation.
  2. Compare nums[m] and target, which results in three cases:
    1. When nums[m] < target, it indicates that target is in the interval \\([m + 1, j]\\), so execute \\(i = m + 1\\).
    2. When nums[m] > target, it indicates that target is in the interval \\([i, m - 1]\\), so execute \\(j = m - 1\\).
    3. When nums[m] = target, it indicates that target has been found, so return index \\(m\\).

If the array does not contain the target element, the search interval will eventually shrink to empty. In this case, return \\(-1\\).

<1><2><3><4><5><6><7>

Figure 10-2   Binary search process

It's worth noting that since both \\(i\\) and \\(j\\) are of int type, \\(i + j\\) may exceed the range of the int type. To avoid large number overflow, we typically use the formula \\(m = \\lfloor {i + (j - i) / 2} \\rfloor\\) to calculate the midpoint.

The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
def binary_search(nums: list[int], target: int) -> int:\n    \"\"\"Binary search (closed interval)\"\"\"\n    # Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    i, j = 0, len(nums) - 1\n    # Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j:\n        # In theory, Python numbers can be infinitely large (depending on memory size), no need to consider large number overflow\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # This means target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # This means target is in the interval [i, m-1]\n        else:\n            return m  # Found the target element, return its index\n    return -1  # Target element not found, return -1\n
binary_search.cpp
/* Binary search (closed interval on both sides) */\nint binarySearch(vector<int> &nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.size() - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.java
/* Binary search (closed interval on both sides) */\nint binarySearch(int[] nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.cs
/* Binary search (closed interval on both sides) */\nint BinarySearch(int[] nums, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = nums.Length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2;   // Calculate the midpoint index m\n        if (nums[m] < target)      // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else                       // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.go
/* Binary search (closed interval on both sides) */\nfunc binarySearch(nums []int, target int) int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    i, j := 0, len(nums)-1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    for i <= j {\n        m := i + (j-i)/2      // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m-1]\n            j = m - 1\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.swift
/* Binary search (closed interval on both sides) */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m-1]\n            j = m - 1\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.js
/* Binary search (closed interval on both sides) */\nfunction binarySearch(nums, target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let i = 0,\n        j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        // Calculate midpoint index m, use parseInt() to round down\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target)\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else return m; // Found the target element, return its index\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.ts
/* Binary search (closed interval on both sides) */\nfunction binarySearch(nums: number[], target: number): number {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let i = 0,\n        j = nums.length - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        // Calculate the midpoint index m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    return -1; // Target element not found, return -1\n}\n
binary_search.dart
/* Binary search (closed interval on both sides) */\nint binarySearch(List<int> nums, int target) {\n  // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n  int i = 0, j = nums.length - 1;\n  // Loop, exit when the search interval is empty (empty when i > j)\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      // This means target is in the interval [m+1, j]\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // This means target is in the interval [i, m-1]\n      j = m - 1;\n    } else {\n      // Found the target element, return its index\n      return m;\n    }\n  }\n  // Target element not found, return -1\n  return -1;\n}\n
binary_search.rs
/* Binary search (closed interval on both sides) */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    let mut i = 0;\n    let mut j = nums.len() as i32 - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            // This means target is in the interval [m+1, j]\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // This means target is in the interval [i, m-1]\n            j = m - 1;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.c
/* Binary search (closed interval on both sides) */\nint binarySearch(int *nums, int len, int target) {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    int i = 0, j = len - 1;\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j]\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.kt
/* Binary search (closed interval on both sides) */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n    var i = 0\n    var j = nums.size - 1\n    // Loop, exit when the search interval is empty (empty when i > j)\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j]\n            i = m + 1\n        else if (nums[m] > target) // This means target is in the interval [i, m-1]\n            j = m - 1\n        else  // Found the target element, return its index\n            return m\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.rb
### Binary search (closed interval) ###\ndef binary_search(nums, target)\n  # Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array\n  i, j = 0, nums.length - 1\n\n  # Loop, exit when the search interval is empty (empty when i > j)\n  while i <= j\n    # In theory, Ruby numbers can be infinitely large (limited by memory), no need to consider overflow\n    m = (i + j) / 2   # Calculate the midpoint index m\n\n    if nums[m] < target\n      i = m + 1 # This means target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # This means target is in the interval [i, m-1]\n    else\n      return m  # Found the target element, return its index\n    end\n  end\n\n  -1  # Target element not found, return -1\nend\n

Time complexity is \\(O(\\log n)\\): In the binary loop, the interval is reduced by half each round, so the number of loops is \\(\\log_2 n\\).

Space complexity is \\(O(1)\\): Pointers \\(i\\) and \\(j\\) use constant-size space.

","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search/#1011-interval-representation-methods","level":2,"title":"10.1.1   Interval Representation Methods","text":"

In addition to the closed interval mentioned above, another common interval representation is the \"left-closed right-open\" interval, defined as \\([0, n)\\), meaning the left boundary includes itself while the right boundary does not. Under this representation, the interval \\([i, j)\\) is empty when \\(i = j\\).

We can implement a binary search algorithm with the same functionality based on this representation:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
def binary_search_lcro(nums: list[int], target: int) -> int:\n    \"\"\"Binary search (left-closed right-open interval)\"\"\"\n    # Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    i, j = 0, len(nums)\n    # Loop, exit when the search interval is empty (empty when i = j)\n    while i < j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # This means target is in the interval [m+1, j)\n        elif nums[m] > target:\n            j = m  # This means target is in the interval [i, m)\n        else:\n            return m  # Found the target element, return its index\n    return -1  # Target element not found, return -1\n
binary_search.cpp
/* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(vector<int> &nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.size();\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.java
/* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(int[] nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.cs
/* Binary search (left-closed right-open interval) */\nint BinarySearchLCRO(int[] nums, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = nums.Length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2;   // Calculate the midpoint index m\n        if (nums[m] < target)      // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else                       // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.go
/* Binary search (left-closed right-open interval) */\nfunc binarySearchLCRO(nums []int, target int) int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    i, j := 0, len(nums)\n    // Loop, exit when the search interval is empty (empty when i = j)\n    for i < j {\n        m := i + (j-i)/2      // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m)\n            j = m\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.swift
/* Binary search (left-closed right-open interval) */\nfunc binarySearchLCRO(nums: [Int], target: Int) -> Int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    var i = nums.startIndex\n    var j = nums.endIndex\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while i < j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target { // This means target is in the interval [m+1, j)\n            i = m + 1\n        } else if nums[m] > target { // This means target is in the interval [i, m)\n            j = m\n        } else { // Found the target element, return its index\n            return m\n        }\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.js
/* Binary search (left-closed right-open interval) */\nfunction binarySearchLCRO(nums, target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let i = 0,\n        j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        // Calculate midpoint index m, use parseInt() to round down\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target)\n            // This means target is in the interval [i, m)\n            j = m;\n        // Found the target element, return its index\n        else return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.ts
/* Binary search (left-closed right-open interval) */\nfunction binarySearchLCRO(nums: number[], target: number): number {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let i = 0,\n        j = nums.length;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        // Calculate the midpoint index m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // This means target is in the interval [i, m)\n            j = m;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    return -1; // Target element not found, return -1\n}\n
binary_search.dart
/* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(List<int> nums, int target) {\n  // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n  int i = 0, j = nums.length;\n  // Loop, exit when the search interval is empty (empty when i = j)\n  while (i < j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      // This means target is in the interval [m+1, j)\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // This means target is in the interval [i, m)\n      j = m;\n    } else {\n      // Found the target element, return its index\n      return m;\n    }\n  }\n  // Target element not found, return -1\n  return -1;\n}\n
binary_search.rs
/* Binary search (left-closed right-open interval) */\nfn binary_search_lcro(nums: &[i32], target: i32) -> i32 {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    let mut i = 0;\n    let mut j = nums.len() as i32;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while i < j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            // This means target is in the interval [m+1, j)\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // This means target is in the interval [i, m)\n            j = m;\n        } else {\n            // Found the target element, return its index\n            return m;\n        }\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.c
/* Binary search (left-closed right-open interval) */\nint binarySearchLCRO(int *nums, int len, int target) {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    int i = 0, j = len;\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target)    // This means target is in the interval [m+1, j)\n            i = m + 1;\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m;\n        else // Found the target element, return its index\n            return m;\n    }\n    // Target element not found, return -1\n    return -1;\n}\n
binary_search.kt
/* Binary search (left-closed right-open interval) */\nfun binarySearchLCRO(nums: IntArray, target: Int): Int {\n    // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n    var i = 0\n    var j = nums.size\n    // Loop, exit when the search interval is empty (empty when i = j)\n    while (i < j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) // This means target is in the interval [m+1, j)\n            i = m + 1\n        else if (nums[m] > target) // This means target is in the interval [i, m)\n            j = m\n        else  // Found the target element, return its index\n            return m\n    }\n    // Target element not found, return -1\n    return -1\n}\n
binary_search.rb
### Binary search (left-closed right-open interval) ###\ndef binary_search_lcro(nums, target)\n  # Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1\n  i, j = 0, nums.length\n\n  # Loop, exit when the search interval is empty (empty when i = j)\n  while i < j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # This means target is in the interval [m+1, j)\n    elsif nums[m] > target\n      j = m - 1 # This means target is in the interval [i, m)\n    else\n      return m  # Found the target element, return its index\n    end\n  end\n\n  -1  # Target element not found, return -1\nend\n

As shown in Figure 10-3, under the two interval representations, the initialization, loop condition, and interval narrowing operations of the binary search algorithm are all different.

Since both the left and right boundaries in the \"closed interval\" representation are defined as closed, the operations to narrow the interval through pointers \\(i\\) and \\(j\\) are also symmetric. This makes it less error-prone, so the \"closed interval\" approach is generally recommended.

Figure 10-3   Two interval definitions

","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search/#1012-advantages-and-limitations","level":2,"title":"10.1.2   Advantages and Limitations","text":"

Binary search performs well in both time and space aspects.

  • Binary search has high time efficiency. With large data volumes, the logarithmic time complexity has significant advantages. For example, when the data size \\(n = 2^{20}\\), linear search requires \\(2^{20} = 1048576\\) loop rounds, while binary search only needs \\(\\log_2 2^{20} = 20\\) rounds.
  • Binary search requires no extra space. Compared to searching algorithms that require additional space (such as hash-based search), binary search is more space-efficient.

However, binary search is not suitable for all situations, mainly for the following reasons:

  • Binary search is only applicable to sorted data. If the input data is unsorted, sorting specifically to use binary search would be counterproductive, as sorting algorithms typically have a time complexity of \\(O(n \\log n)\\), which is higher than both linear search and binary search. For scenarios with frequent element insertions, maintaining array orderliness requires inserting elements at specific positions with a time complexity of \\(O(n)\\), which is also very expensive.
  • Binary search is only applicable to arrays. Binary search requires jump-style (non-contiguous) element access, and jump-style access has low efficiency in linked lists, making it unsuitable for linked lists or data structures based on linked list implementations.
  • For small data volumes, linear search performs better. In linear search, each round requires only 1 comparison operation; while in binary search, it requires 1 addition, 1 division, 1-3 comparison operations, and 1 addition (subtraction), totaling 4-6 unit operations. Therefore, when the data volume \\(n\\) is small, linear search is actually faster than binary search.
","path":["Chapter 10. Searching","10.1   Binary Search"],"tags":[]},{"location":"chapter_searching/binary_search_edge/","level":1,"title":"10.3   Binary Search Edge Cases","text":"","path":["Chapter 10. Searching","10.3   Binary Search Edge Cases"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1031-finding-the-left-boundary","level":2,"title":"10.3.1   Finding the Left Boundary","text":"

Question

Given a sorted array nums of length \\(n\\) that may contain duplicate elements, return the index of the leftmost element target in the array. If the array does not contain the element, return \\(-1\\).

Recall the method for finding the insertion point with binary search. After the search completes, \\(i\\) points to the leftmost target, so finding the insertion point is essentially finding the index of the leftmost target.

Consider implementing the left boundary search using the insertion point finding function. Note that the array may not contain target, which could result in the following two cases:

  • The insertion point index \\(i\\) is out of bounds.
  • The element nums[i] is not equal to target.

When either of these situations occurs, simply return \\(-1\\). The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
def binary_search_left_edge(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for the leftmost target\"\"\"\n    # Equivalent to finding the insertion point of target\n    i = binary_search_insertion(nums, target)\n    # Target not found, return -1\n    if i == len(nums) or nums[i] != target:\n        return -1\n    # Found target, return index i\n    return i\n
binary_search_edge.cpp
/* Binary search for the leftmost target */\nint binarySearchLeftEdge(vector<int> &nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.size() || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.java
/* Binary search for the leftmost target */\nint binarySearchLeftEdge(int[] nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binary_search_insertion.binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.length || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.cs
/* Binary search for the leftmost target */\nint BinarySearchLeftEdge(int[] nums, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i == nums.Length || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.go
/* Binary search for the leftmost target */\nfunc binarySearchLeftEdge(nums []int, target int) int {\n    // Equivalent to finding the insertion point of target\n    i := binarySearchInsertion(nums, target)\n    // Target not found, return -1\n    if i == len(nums) || nums[i] != target {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
binary_search_edge.swift
/* Binary search for the leftmost target */\nfunc binarySearchLeftEdge(nums: [Int], target: Int) -> Int {\n    // Equivalent to finding the insertion point of target\n    let i = binarySearchInsertion(nums: nums, target: target)\n    // Target not found, return -1\n    if i == nums.endIndex || nums[i] != target {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
binary_search_edge.js
/* Binary search for the leftmost target */\nfunction binarySearchLeftEdge(nums, target) {\n    // Equivalent to finding the insertion point of target\n    const i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.ts
/* Binary search for the leftmost target */\nfunction binarySearchLeftEdge(nums: Array<number>, target: number): number {\n    // Equivalent to finding the insertion point of target\n    const i = binarySearchInsertion(nums, target);\n    // Target not found, return -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.dart
/* Binary search for the leftmost target */\nint binarySearchLeftEdge(List<int> nums, int target) {\n  // Equivalent to finding the insertion point of target\n  int i = binarySearchInsertion(nums, target);\n  // Target not found, return -1\n  if (i == nums.length || nums[i] != target) {\n    return -1;\n  }\n  // Found target, return index i\n  return i;\n}\n
binary_search_edge.rs
/* Binary search for the leftmost target */\nfn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {\n    // Equivalent to finding the insertion point of target\n    let i = binary_search_insertion(nums, target);\n    // Target not found, return -1\n    if i == nums.len() as i32 || nums[i as usize] != target {\n        return -1;\n    }\n    // Found target, return index i\n    i\n}\n
binary_search_edge.c
/* Binary search for the leftmost target */\nint binarySearchLeftEdge(int *nums, int numSize, int target) {\n    // Equivalent to finding the insertion point of target\n    int i = binarySearchInsertion(nums, numSize, target);\n    // Target not found, return -1\n    if (i == numSize || nums[i] != target) {\n        return -1;\n    }\n    // Found target, return index i\n    return i;\n}\n
binary_search_edge.kt
/* Binary search for the leftmost target */\nfun binarySearchLeftEdge(nums: IntArray, target: Int): Int {\n    // Equivalent to finding the insertion point of target\n    val i = binarySearchInsertion(nums, target)\n    // Target not found, return -1\n    if (i == nums.size || nums[i] != target) {\n        return -1\n    }\n    // Found target, return index i\n    return i\n}\n
binary_search_edge.rb
### Binary search leftmost target ###\ndef binary_search_left_edge(nums, target)\n  # Equivalent to finding the insertion point of target\n  i = binary_search_insertion(nums, target)\n\n  # Target not found, return -1\n  return -1 if i == nums.length || nums[i] != target\n\n  i # Found target, return index i\nend\n
","path":["Chapter 10. Searching","10.3   Binary Search Edge Cases"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1032-finding-the-right-boundary","level":2,"title":"10.3.2   Finding the Right Boundary","text":"

So how do we find the rightmost target? The most direct approach is to modify the code and replace the pointer shrinking operation in the nums[m] == target case. The code is omitted here; interested readers can implement it themselves.

Below we introduce two more clever methods.

","path":["Chapter 10. Searching","10.3   Binary Search Edge Cases"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1-reusing-left-boundary-search","level":3,"title":"1.   Reusing Left Boundary Search","text":"

In fact, we can use the function for finding the leftmost element to find the rightmost element. The specific method is: Convert finding the rightmost target into finding the leftmost target + 1.

As shown in Figure 10-7, after the search completes, pointer \\(i\\) points to the leftmost target + 1 (if it exists), while \\(j\\) points to the rightmost target, so we can simply return \\(j\\).

Figure 10-7   Converting right boundary search to left boundary search

Note that the returned insertion point is \\(i\\), so we need to subtract \\(1\\) from it to obtain \\(j\\):

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
def binary_search_right_edge(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for the rightmost target\"\"\"\n    # Convert to finding the leftmost target + 1\n    i = binary_search_insertion(nums, target + 1)\n    # j points to the rightmost target, i points to the first element greater than target\n    j = i - 1\n    # Target not found, return -1\n    if j == -1 or nums[j] != target:\n        return -1\n    # Found target, return index j\n    return j\n
binary_search_edge.cpp
/* Binary search for the rightmost target */\nint binarySearchRightEdge(vector<int> &nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.java
/* Binary search for the rightmost target */\nint binarySearchRightEdge(int[] nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.cs
/* Binary search for the rightmost target */\nint BinarySearchRightEdge(int[] nums, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.go
/* Binary search for the rightmost target */\nfunc binarySearchRightEdge(nums []int, target int) int {\n    // Convert to finding the leftmost target + 1\n    i := binarySearchInsertion(nums, target+1)\n    // j points to the rightmost target, i points to the first element greater than target\n    j := i - 1\n    // Target not found, return -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
binary_search_edge.swift
/* Binary search for the rightmost target */\nfunc binarySearchRightEdge(nums: [Int], target: Int) -> Int {\n    // Convert to finding the leftmost target + 1\n    let i = binarySearchInsertion(nums: nums, target: target + 1)\n    // j points to the rightmost target, i points to the first element greater than target\n    let j = i - 1\n    // Target not found, return -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
binary_search_edge.js
/* Binary search for the rightmost target */\nfunction binarySearchRightEdge(nums, target) {\n    // Convert to finding the leftmost target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    const j = i - 1;\n    // Target not found, return -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.ts
/* Binary search for the rightmost target */\nfunction binarySearchRightEdge(nums: Array<number>, target: number): number {\n    // Convert to finding the leftmost target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    const j = i - 1;\n    // Target not found, return -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.dart
/* Binary search for the rightmost target */\nint binarySearchRightEdge(List<int> nums, int target) {\n  // Convert to finding the leftmost target + 1\n  int i = binarySearchInsertion(nums, target + 1);\n  // j points to the rightmost target, i points to the first element greater than target\n  int j = i - 1;\n  // Target not found, return -1\n  if (j == -1 || nums[j] != target) {\n    return -1;\n  }\n  // Found target, return index j\n  return j;\n}\n
binary_search_edge.rs
/* Binary search for the rightmost target */\nfn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {\n    // Convert to finding the leftmost target + 1\n    let i = binary_search_insertion(nums, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    let j = i - 1;\n    // Target not found, return -1\n    if j == -1 || nums[j as usize] != target {\n        return -1;\n    }\n    // Found target, return index j\n    j\n}\n
binary_search_edge.c
/* Binary search for the rightmost target */\nint binarySearchRightEdge(int *nums, int numSize, int target) {\n    // Convert to finding the leftmost target + 1\n    int i = binarySearchInsertion(nums, numSize, target + 1);\n    // j points to the rightmost target, i points to the first element greater than target\n    int j = i - 1;\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // Found target, return index j\n    return j;\n}\n
binary_search_edge.kt
/* Binary search for the rightmost target */\nfun binarySearchRightEdge(nums: IntArray, target: Int): Int {\n    // Convert to finding the leftmost target + 1\n    val i = binarySearchInsertion(nums, target + 1)\n    // j points to the rightmost target, i points to the first element greater than target\n    val j = i - 1\n    // Target not found, return -1\n    if (j == -1 || nums[j] != target) {\n        return -1\n    }\n    // Found target, return index j\n    return j\n}\n
binary_search_edge.rb
### Binary search rightmost target ###\ndef binary_search_right_edge(nums, target)\n  # Convert to finding the leftmost target + 1\n  i = binary_search_insertion(nums, target + 1)\n\n  # j points to the rightmost target, i points to the first element greater than target\n  j = i - 1\n\n  # Target not found, return -1\n  return -1 if j == -1 || nums[j] != target\n\n  j # Found target, return index j\nend\n
","path":["Chapter 10. Searching","10.3   Binary Search Edge Cases"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#2-converting-to-element-search","level":3,"title":"2.   Converting to Element Search","text":"

We know that when the array does not contain target, \\(i\\) and \\(j\\) will eventually point to the first elements greater than and less than target, respectively.

Therefore, as shown in Figure 10-8, we can construct an element that does not exist in the array to find the left and right boundaries.

  • Finding the leftmost target: Can be converted to finding target - 0.5 and returning pointer \\(i\\).
  • Finding the rightmost target: Can be converted to finding target + 0.5 and returning pointer \\(j\\).

Figure 10-8   Converting boundary search to element search

The code is omitted here, but the following two points are worth noting:

  • Since the given array does not contain decimals, we don't need to worry about how to handle equal cases.
  • Because this method introduces decimals, the variable target in the function needs to be changed to a floating-point type (Python does not require this change).
","path":["Chapter 10. Searching","10.3   Binary Search Edge Cases"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/","level":1,"title":"10.2   Binary Search Insertion Point","text":"

Binary search can not only be used to search for target elements but also to solve many variant problems, such as searching for the insertion position of a target element.

","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1021-case-without-duplicate-elements","level":2,"title":"10.2.1   Case Without Duplicate Elements","text":"

Question

Given a sorted array nums of length \\(n\\) and an element target, where the array contains no duplicate elements. Insert target into the array nums while maintaining its sorted order. If the array already contains the element target, insert it to its left. Return the index of target in the array after insertion. An example is shown in Figure 10-4.

Figure 10-4   Binary search insertion point example data

If we want to reuse the binary search code from the previous section, we need to answer the following two questions.

Question 1: When the array contains target, is the insertion point index the same as that element's index?

The problem requires inserting target to the left of equal elements, which means the newly inserted target replaces the position of the original target. In other words, when the array contains target, the insertion point index is the index of that target.

Question 2: When the array does not contain target, what is the insertion point index?

Further consider the binary search process: When nums[m] < target, \\(i\\) moves, which means pointer \\(i\\) is approaching elements greater than or equal to target. Similarly, pointer \\(j\\) is always approaching elements less than or equal to target.

Therefore, when the binary search ends, we must have: \\(i\\) points to the first element greater than target, and \\(j\\) points to the first element less than target. It's easy to see that when the array does not contain target, the insertion index is \\(i\\). The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
def binary_search_insertion_simple(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for insertion point (no duplicate elements)\"\"\"\n    i, j = 0, len(nums) - 1  # Initialize closed interval [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target is in the interval [i, m-1]\n        else:\n            return m  # Found target, return insertion point m\n    # Target not found, return insertion point i\n    return i\n
binary_search_insertion.cpp
/* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.java
/* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.cs
/* Binary search for insertion point (no duplicate elements) */\nint BinarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.go
/* Binary search for insertion point (no duplicate elements) */\nfunc binarySearchInsertionSimple(nums []int, target int) int {\n    // Initialize closed interval [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Calculate the midpoint index m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target is in the interval [i, m-1]\n            j = m - 1\n        } else {\n            // Found target, return insertion point m\n            return m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
binary_search_insertion.swift
/* Binary search for insertion point (no duplicate elements) */\nfunc binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [0, n-1]\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            return m // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
binary_search_insertion.js
/* Binary search for insertion point (no duplicate elements) */\nfunction binarySearchInsertionSimple(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.ts
/* Binary search for insertion point (no duplicate elements) */\nfunction binarySearchInsertionSimple(\n    nums: Array<number>,\n    target: number\n): number {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.dart
/* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      i = m + 1; // target is in the interval [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target is in the interval [i, m-1]\n    } else {\n      return m; // Found target, return insertion point m\n    }\n  }\n  // Target not found, return insertion point i\n  return i;\n}\n
binary_search_insertion.rs
/* Binary search for insertion point (no duplicate elements) */\nfn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m;\n        }\n    }\n    // Target not found, return insertion point i\n    i\n}\n
binary_search_insertion.c
/* Binary search for insertion point (no duplicate elements) */\nint binarySearchInsertionSimple(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            return m; // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i;\n}\n
binary_search_insertion.kt
/* Binary search for insertion point (no duplicate elements) */\nfun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            return m // Found target, return insertion point m\n        }\n    }\n    // Target not found, return insertion point i\n    return i\n}\n
binary_search_insertion.rb
### Binary search insertion point (no duplicates) ###\ndef binary_search_insertion_simple(nums, target)\n  # Initialize closed interval [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target is in the interval [i, m-1]\n    else\n      return m  # Found target, return insertion point m\n    end\n  end\n\n  i # Target not found, return insertion point i\nend\n
","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1022-case-with-duplicate-elements","level":2,"title":"10.2.2   Case with Duplicate Elements","text":"

Question

Based on the previous problem, assume the array may contain duplicate elements, with everything else remaining the same.

Suppose there are multiple target elements in the array. Ordinary binary search can only return the index of one target, and cannot determine how many target elements are to the left and right of that element.

The problem requires inserting the target element at the leftmost position, so we need to find the index of the leftmost target in the array. Initially, consider implementing this through the steps shown in Figure 10-5:

  1. Perform binary search to obtain the index of any target, denoted as \\(k\\).
  2. Starting from index \\(k\\), perform linear traversal to the left, and return when the leftmost target is found.

Figure 10-5   Linear search for insertion point of duplicate elements

Although this method works, it includes linear search, resulting in a time complexity of \\(O(n)\\). When the array contains many duplicate target elements, this method is very inefficient.

Now consider extending the binary search code. As shown in Figure 10-6, the overall process remains unchanged: calculate the midpoint index \\(m\\) in each round, then compare target with nums[m], divided into the following cases:

  • When nums[m] < target or nums[m] > target, it means target has not been found yet, so use the ordinary binary search interval narrowing operation to make pointers \\(i\\) and \\(j\\) approach target.
  • When nums[m] == target, it means elements less than target are in the interval \\([i, m - 1]\\), so use \\(j = m - 1\\) to narrow the interval, thereby making pointer \\(j\\) approach elements less than target.

After the loop completes, \\(i\\) points to the leftmost target, and \\(j\\) points to the first element less than target, so index \\(i\\) is the insertion point.

<1><2><3><4><5><6><7><8>

Figure 10-6   Steps for binary search insertion point of duplicate elements

Observe the following code: the operations for branches nums[m] > target and nums[m] == target are the same, so the two can be merged.

Even so, we can still keep the conditional branches expanded, as the logic is clearer and more readable.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
def binary_search_insertion(nums: list[int], target: int) -> int:\n    \"\"\"Binary search for insertion point (with duplicate elements)\"\"\"\n    i, j = 0, len(nums) - 1  # Initialize closed interval [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # Calculate midpoint index m\n        if nums[m] < target:\n            i = m + 1  # target is in the interval [m+1, j]\n        elif nums[m] > target:\n            j = m - 1  # target is in the interval [i, m-1]\n        else:\n            j = m - 1  # The first element less than target is in the interval [i, m-1]\n    # Return insertion point i\n    return i\n
binary_search_insertion.cpp
/* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.java
/* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.cs
/* Binary search for insertion point (with duplicate elements) */\nint BinarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.go
/* Binary search for insertion point (with duplicate elements) */\nfunc binarySearchInsertion(nums []int, target int) int {\n    // Initialize closed interval [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // Calculate the midpoint index m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target is in the interval [m+1, j]\n            i = m + 1\n        } else if nums[m] > target {\n            // target is in the interval [i, m-1]\n            j = m - 1\n        } else {\n            // The first element less than target is in the interval [i, m-1]\n            j = m - 1\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
binary_search_insertion.swift
/* Binary search for insertion point (with duplicate elements) */\nfunc binarySearchInsertion(nums: [Int], target: Int) -> Int {\n    // Initialize closed interval [0, n-1]\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // Calculate the midpoint index m\n        if nums[m] < target {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if nums[m] > target {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            j = m - 1 // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
binary_search_insertion.js
/* Binary search for insertion point (with duplicate elements) */\nfunction binarySearchInsertion(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.ts
/* Binary search for insertion point (with duplicate elements) */\nfunction binarySearchInsertion(nums: Array<number>, target: number): number {\n    let i = 0,\n        j = nums.length - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.dart
/* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // Calculate the midpoint index m\n    if (nums[m] < target) {\n      i = m + 1; // target is in the interval [m+1, j]\n    } else if (nums[m] > target) {\n      j = m - 1; // target is in the interval [i, m-1]\n    } else {\n      j = m - 1; // The first element less than target is in the interval [i, m-1]\n    }\n  }\n  // Return insertion point i\n  return i;\n}\n
binary_search_insertion.rs
/* Binary search for insertion point (with duplicate elements) */\npub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // Calculate the midpoint index m\n        if nums[m as usize] < target {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if nums[m as usize] > target {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    i\n}\n
binary_search_insertion.c
/* Binary search for insertion point (with duplicate elements) */\nint binarySearchInsertion(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1; // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1; // target is in the interval [i, m-1]\n        } else {\n            j = m - 1; // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i;\n}\n
binary_search_insertion.kt
/* Binary search for insertion point (with duplicate elements) */\nfun binarySearchInsertion(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // Initialize closed interval [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // Calculate the midpoint index m\n        if (nums[m] < target) {\n            i = m + 1 // target is in the interval [m+1, j]\n        } else if (nums[m] > target) {\n            j = m - 1 // target is in the interval [i, m-1]\n        } else {\n            j = m - 1 // The first element less than target is in the interval [i, m-1]\n        }\n    }\n    // Return insertion point i\n    return i\n}\n
binary_search_insertion.rb
### Binary search insertion point (with duplicates) ###\ndef binary_search_insertion(nums, target)\n  # Initialize closed interval [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # Calculate the midpoint index m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target is in the interval [m+1, j]\n    elsif nums[m] > target\n      j = m - 1 # target is in the interval [i, m-1]\n    else\n      j = m - 1 # The first element less than target is in the interval [i, m-1]\n    end\n  end\n\n  i # Return insertion point i\nend\n

Tip

The code in this section all uses the \"closed interval\" approach. Interested readers can implement the \"left-closed right-open\" approach themselves.

Overall, binary search is simply about setting search targets for pointers \\(i\\) and \\(j\\) separately. The target could be a specific element (such as target) or a range of elements (such as elements less than target).

Through continuous binary iterations, both pointers \\(i\\) and \\(j\\) gradually approach their preset targets. Ultimately, they either successfully find the answer or stop after crossing the boundaries.

","path":["Chapter 10. Searching","10.2   Binary Search Insertion Point"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/","level":1,"title":"10.4   Hash Optimization Strategy","text":"

In algorithm problems, we often reduce the time complexity of algorithms by replacing linear search with hash-based search. Let's use an algorithm problem to deepen our understanding.

Question

Given an integer array nums and a target element target, search for two elements in the array whose \"sum\" equals target, and return their array indices. Any solution will do.

","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041-linear-search-trading-time-for-space","level":2,"title":"10.4.1   Linear Search: Trading Time for Space","text":"

Consider directly traversing all possible combinations. As shown in Figure 10-9, we open a two-layer loop and judge in each round whether the sum of two integers equals target. If so, return their indices.

Figure 10-9   Linear search solution for two sum

The code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n    \"\"\"Method 1: Brute force enumeration\"\"\"\n    # Two nested loops, time complexity is O(n^2)\n    for i in range(len(nums) - 1):\n        for j in range(i + 1, len(nums)):\n            if nums[i] + nums[j] == target:\n                return [i, j]\n    return []\n
two_sum.cpp
/* Method 1: Brute force enumeration */\nvector<int> twoSumBruteForce(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Two nested loops, time complexity is O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return {i, j};\n        }\n    }\n    return {};\n}\n
two_sum.java
/* Method 1: Brute force enumeration */\nint[] twoSumBruteForce(int[] nums, int target) {\n    int size = nums.length;\n    // Two nested loops, time complexity is O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return new int[] { i, j };\n        }\n    }\n    return new int[0];\n}\n
two_sum.cs
/* Method 1: Brute force enumeration */\nint[] TwoSumBruteForce(int[] nums, int target) {\n    int size = nums.Length;\n    // Two nested loops, time complexity is O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return [i, j];\n        }\n    }\n    return [];\n}\n
two_sum.go
/* Method 1: Brute force enumeration */\nfunc twoSumBruteForce(nums []int, target int) []int {\n    size := len(nums)\n    // Two nested loops, time complexity is O(n^2)\n    for i := 0; i < size-1; i++ {\n        for j := i + 1; j < size; j++ {\n            if nums[i]+nums[j] == target {\n                return []int{i, j}\n            }\n        }\n    }\n    return nil\n}\n
two_sum.swift
/* Method 1: Brute force enumeration */\nfunc twoSumBruteForce(nums: [Int], target: Int) -> [Int] {\n    // Two nested loops, time complexity is O(n^2)\n    for i in nums.indices.dropLast() {\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[i] + nums[j] == target {\n                return [i, j]\n            }\n        }\n    }\n    return [0]\n}\n
two_sum.js
/* Method 1: Brute force enumeration */\nfunction twoSumBruteForce(nums, target) {\n    const n = nums.length;\n    // Two nested loops, time complexity is O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
two_sum.ts
/* Method 1: Brute force enumeration */\nfunction twoSumBruteForce(nums: number[], target: number): number[] {\n    const n = nums.length;\n    // Two nested loops, time complexity is O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
two_sum.dart
/* Method 1: Brute force enumeration */\nList<int> twoSumBruteForce(List<int> nums, int target) {\n  int size = nums.length;\n  // Two nested loops, time complexity is O(n^2)\n  for (var i = 0; i < size - 1; i++) {\n    for (var j = i + 1; j < size; j++) {\n      if (nums[i] + nums[j] == target) return [i, j];\n    }\n  }\n  return [0];\n}\n
two_sum.rs
/* Method 1: Brute force enumeration */\npub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    let size = nums.len();\n    // Two nested loops, time complexity is O(n^2)\n    for i in 0..size - 1 {\n        for j in i + 1..size {\n            if nums[i] + nums[j] == target {\n                return Some(vec![i as i32, j as i32]);\n            }\n        }\n    }\n    None\n}\n
two_sum.c
/* Method 1: Brute force enumeration */\nint *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {\n    for (int i = 0; i < numsSize; ++i) {\n        for (int j = i + 1; j < numsSize; ++j) {\n            if (nums[i] + nums[j] == target) {\n                int *res = malloc(sizeof(int) * 2);\n                res[0] = i, res[1] = j;\n                *returnSize = 2;\n                return res;\n            }\n        }\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
two_sum.kt
/* Method 1: Brute force enumeration */\nfun twoSumBruteForce(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Two nested loops, time complexity is O(n^2)\n    for (i in 0..<size - 1) {\n        for (j in i + 1..<size) {\n            if (nums[i] + nums[j] == target) return intArrayOf(i, j)\n        }\n    }\n    return IntArray(0)\n}\n
two_sum.rb
### Method 1: Brute force enumeration ###\ndef two_sum_brute_force(nums, target)\n  # Two nested loops, time complexity is O(n^2)\n  for i in 0...(nums.length - 1)\n    for j in (i + 1)...nums.length\n      return [i, j] if nums[i] + nums[j] == target\n    end\n  end\n\n  []\nend\n

This method has a time complexity of \\(O(n^2)\\) and a space complexity of \\(O(1)\\), which is very time-consuming with large data volumes.

","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1042-hash-based-search-trading-space-for-time","level":2,"title":"10.4.2   Hash-Based Search: Trading Space for Time","text":"

Consider using a hash table where key-value pairs are array elements and element indices respectively. Loop through the array, performing the steps shown in Figure 10-10 in each round:

  1. Check if the number target - nums[i] is in the hash table. If so, directly return the indices of these two elements.
  2. Add the key-value pair nums[i] and index i to the hash table.
<1><2><3>

Figure 10-10   Hash table solution for two sum

The implementation code is shown below, requiring only a single loop:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
def two_sum_hash_table(nums: list[int], target: int) -> list[int]:\n    \"\"\"Method 2: Auxiliary hash table\"\"\"\n    # Auxiliary hash table, space complexity is O(n)\n    dic = {}\n    # Single loop, time complexity is O(n)\n    for i in range(len(nums)):\n        if target - nums[i] in dic:\n            return [dic[target - nums[i]], i]\n        dic[nums[i]] = i\n    return []\n
two_sum.cpp
/* Method 2: Auxiliary hash table */\nvector<int> twoSumHashTable(vector<int> &nums, int target) {\n    int size = nums.size();\n    // Auxiliary hash table, space complexity is O(n)\n    unordered_map<int, int> dic;\n    // Single loop, time complexity is O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.find(target - nums[i]) != dic.end()) {\n            return {dic[target - nums[i]], i};\n        }\n        dic.emplace(nums[i], i);\n    }\n    return {};\n}\n
two_sum.java
/* Method 2: Auxiliary hash table */\nint[] twoSumHashTable(int[] nums, int target) {\n    int size = nums.length;\n    // Auxiliary hash table, space complexity is O(n)\n    Map<Integer, Integer> dic = new HashMap<>();\n    // Single loop, time complexity is O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.containsKey(target - nums[i])) {\n            return new int[] { dic.get(target - nums[i]), i };\n        }\n        dic.put(nums[i], i);\n    }\n    return new int[0];\n}\n
two_sum.cs
/* Method 2: Auxiliary hash table */\nint[] TwoSumHashTable(int[] nums, int target) {\n    int size = nums.Length;\n    // Auxiliary hash table, space complexity is O(n)\n    Dictionary<int, int> dic = [];\n    // Single loop, time complexity is O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.ContainsKey(target - nums[i])) {\n            return [dic[target - nums[i]], i];\n        }\n        dic.Add(nums[i], i);\n    }\n    return [];\n}\n
two_sum.go
/* Method 2: Auxiliary hash table */\nfunc twoSumHashTable(nums []int, target int) []int {\n    // Auxiliary hash table, space complexity is O(n)\n    hashTable := map[int]int{}\n    // Single loop, time complexity is O(n)\n    for idx, val := range nums {\n        if preIdx, ok := hashTable[target-val]; ok {\n            return []int{preIdx, idx}\n        }\n        hashTable[val] = idx\n    }\n    return nil\n}\n
two_sum.swift
/* Method 2: Auxiliary hash table */\nfunc twoSumHashTable(nums: [Int], target: Int) -> [Int] {\n    // Auxiliary hash table, space complexity is O(n)\n    var dic: [Int: Int] = [:]\n    // Single loop, time complexity is O(n)\n    for i in nums.indices {\n        if let j = dic[target - nums[i]] {\n            return [j, i]\n        }\n        dic[nums[i]] = i\n    }\n    return [0]\n}\n
two_sum.js
/* Method 2: Auxiliary hash table */\nfunction twoSumHashTable(nums, target) {\n    // Auxiliary hash table, space complexity is O(n)\n    let m = {};\n    // Single loop, time complexity is O(n)\n    for (let i = 0; i < nums.length; i++) {\n        if (m[target - nums[i]] !== undefined) {\n            return [m[target - nums[i]], i];\n        } else {\n            m[nums[i]] = i;\n        }\n    }\n    return [];\n}\n
two_sum.ts
/* Method 2: Auxiliary hash table */\nfunction twoSumHashTable(nums: number[], target: number): number[] {\n    // Auxiliary hash table, space complexity is O(n)\n    let m: Map<number, number> = new Map();\n    // Single loop, time complexity is O(n)\n    for (let i = 0; i < nums.length; i++) {\n        let index = m.get(target - nums[i]);\n        if (index !== undefined) {\n            return [index, i];\n        } else {\n            m.set(nums[i], i);\n        }\n    }\n    return [];\n}\n
two_sum.dart
/* Method 2: Auxiliary hash table */\nList<int> twoSumHashTable(List<int> nums, int target) {\n  int size = nums.length;\n  // Auxiliary hash table, space complexity is O(n)\n  Map<int, int> dic = HashMap();\n  // Single loop, time complexity is O(n)\n  for (var i = 0; i < size; i++) {\n    if (dic.containsKey(target - nums[i])) {\n      return [dic[target - nums[i]]!, i];\n    }\n    dic.putIfAbsent(nums[i], () => i);\n  }\n  return [0];\n}\n
two_sum.rs
/* Method 2: Auxiliary hash table */\npub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    // Auxiliary hash table, space complexity is O(n)\n    let mut dic = HashMap::new();\n    // Single loop, time complexity is O(n)\n    for (i, num) in nums.iter().enumerate() {\n        match dic.get(&(target - num)) {\n            Some(v) => return Some(vec![*v as i32, i as i32]),\n            None => dic.insert(num, i as i32),\n        };\n    }\n    None\n}\n
two_sum.c
/* Hash table */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // Implemented using uthash.h\n} HashTable;\n\n/* Hash table lookup */\nHashTable *find(HashTable *h, int key) {\n    HashTable *tmp;\n    HASH_FIND_INT(h, &key, tmp);\n    return tmp;\n}\n\n/* Hash table element insertion */\nvoid insert(HashTable **h, int key, int val) {\n    HashTable *t = find(*h, key);\n    if (t == NULL) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = key, tmp->val = val;\n        HASH_ADD_INT(*h, key, tmp);\n    } else {\n        t->val = val;\n    }\n}\n\n/* Method 2: Auxiliary hash table */\nint *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {\n    HashTable *hashtable = NULL;\n    for (int i = 0; i < numsSize; i++) {\n        HashTable *t = find(hashtable, target - nums[i]);\n        if (t != NULL) {\n            int *res = malloc(sizeof(int) * 2);\n            res[0] = t->val, res[1] = i;\n            *returnSize = 2;\n            return res;\n        }\n        insert(&hashtable, nums[i], i);\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
two_sum.kt
/* Method 2: Auxiliary hash table */\nfun twoSumHashTable(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // Auxiliary hash table, space complexity is O(n)\n    val dic = HashMap<Int, Int>()\n    // Single loop, time complexity is O(n)\n    for (i in 0..<size) {\n        if (dic.containsKey(target - nums[i])) {\n            return intArrayOf(dic[target - nums[i]]!!, i)\n        }\n        dic[nums[i]] = i\n    }\n    return IntArray(0)\n}\n
two_sum.rb
### Method 2: Auxiliary hash table ###\ndef two_sum_hash_table(nums, target)\n  # Auxiliary hash table, space complexity is O(n)\n  dic = {}\n  # Single loop, time complexity is O(n)\n  for i in 0...nums.length\n    return [dic[target - nums[i]], i] if dic.has_key?(target - nums[i])\n\n    dic[nums[i]] = i\n  end\n\n  []\nend\n

This method reduces the time complexity from \\(O(n^2)\\) to \\(O(n)\\) through hash-based search, greatly improving runtime efficiency.

Since an additional hash table needs to be maintained, the space complexity is \\(O(n)\\). Nevertheless, this method achieves a more balanced overall time-space efficiency, making it the optimal solution for this problem.

","path":["Chapter 10. Searching","10.4   Hash Optimization Strategy"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/","level":1,"title":"10.5   Searching Algorithms Revisited","text":"

Searching algorithms are used to search for one or a group of elements that meet specific conditions in data structures (such as arrays, linked lists, trees, or graphs).

Searching algorithms can be divided into the following two categories based on their implementation approach:

  • Locating target elements by traversing the data structure, such as traversing arrays, linked lists, trees, and graphs.
  • Achieving efficient element search by utilizing data organization structure or prior information contained in the data, such as binary search, hash-based search, and binary search tree search.

It's not hard to see that these topics have all been covered in previous chapters, so searching algorithms are not unfamiliar to us. In this section, we will approach from a more systematic perspective and re-examine searching algorithms.

","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051-brute-force-search","level":2,"title":"10.5.1   Brute-Force Search","text":"

Brute-force search locates target elements by traversing each element of the data structure.

  • \"Linear search\" is applicable to linear data structures such as arrays and linked lists. It starts from one end of the data structure and accesses elements one by one until the target element is found or the other end is reached without finding the target element.
  • \"Breadth-first search\" and \"depth-first search\" are two traversal strategies for graphs and trees. Breadth-first search starts from the initial node and searches layer by layer, visiting nodes from near to far. Depth-first search starts from the initial node, follows a path to the end, then backtracks and tries other paths until the entire data structure is traversed.

The advantage of brute-force search is that it is simple and has good generality, requiring no data preprocessing or additional data structures.

However, the time complexity of such algorithms is \\(O(n)\\), where \\(n\\) is the number of elements, so performance is poor when dealing with large amounts of data.

","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1052-adaptive-search","level":2,"title":"10.5.2   Adaptive Search","text":"

Adaptive search utilizes the unique properties of data (such as orderliness) to optimize the search process, thereby locating target elements more efficiently.

  • \"Binary search\" uses the orderliness of data to achieve efficient searching, applicable only to arrays.
  • \"Hash-based search\" uses hash tables to establish key-value pair mappings between search data and target data, thereby achieving query operations.
  • \"Tree search\" in specific tree structures (such as binary search trees), quickly eliminates nodes based on comparing node values to locate target elements.

The advantage of such algorithms is high efficiency, with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\).

However, using these algorithms often requires data preprocessing. For example, binary search requires pre-sorting the array, while hash-based search and tree search both require additional data structures, and maintaining these data structures also requires extra time and space overhead.

Tip

Adaptive search algorithms are often called lookup algorithms, mainly used to quickly retrieve target elements in specific data structures.

","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1053-search-method-selection","level":2,"title":"10.5.3   Search Method Selection","text":"

Given a dataset of size \\(n\\), we can use linear search, binary search, tree search, hash-based search, and other methods to search for the target element. The working principles of each method are shown in Figure 10-11.

Figure 10-11   Multiple search strategies

The operational efficiency and characteristics of the above methods are as follows:

Table 10-1   Comparison of search algorithm efficiency

Linear search Binary search Tree search Hash-based search Search element \\(O(n)\\) \\(O(\\log n)\\) \\(O(\\log n)\\) \\(O(1)\\) Insert element \\(O(1)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Delete element \\(O(n)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) Extra space \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\) Data preprocessing / Sorting \\(O(n \\log n)\\) Tree building \\(O(n \\log n)\\) Hash table building \\(O(n)\\) Data ordered Unordered Ordered Ordered Unordered

The choice of search algorithm also depends on data volume, search performance requirements, data query and update frequency, etc.

Linear search

  • Good generality, requiring no data preprocessing operations. If we only need to query the data once, the data preprocessing time for the other three methods would be longer than linear search.
  • Suitable for small data volumes, where time complexity has less impact on efficiency.
  • Suitable for scenarios with high data update frequency, as this method does not require any additional data maintenance.

Binary search

  • Suitable for large data volumes with stable efficiency performance, worst-case time complexity of \\(O(\\log n)\\).
  • Data volume cannot be too large, as storing arrays requires contiguous memory space.
  • Not suitable for scenarios with frequent data insertion and deletion, as maintaining a sorted array has high overhead.

Hash-based search

  • Suitable for scenarios with high query performance requirements, with an average time complexity of \\(O(1)\\).
  • Not suitable for scenarios requiring ordered data or range searches, as hash tables cannot maintain data orderliness.
  • High dependence on hash functions and hash collision handling strategies, with significant risk of performance degradation.
  • Not suitable for excessively large data volumes, as hash tables require extra space to minimize collisions and thus provide good query performance.

Tree search

  • Suitable for massive data, as tree nodes are stored dispersedly in memory.
  • Suitable for scenarios requiring maintained ordered data or range searches.
  • During continuous node insertion and deletion, binary search trees may become skewed, degrading time complexity to \\(O(n)\\).
  • If using AVL trees or red-black trees, all operations can run stably at \\(O(\\log n)\\) efficiency, but operations to maintain tree balance add extra overhead.
","path":["Chapter 10. Searching","10.5   Searching Algorithms Revisited"],"tags":[]},{"location":"chapter_searching/summary/","level":1,"title":"10.6   Summary","text":"","path":["Chapter 10. Searching","10.6   Summary"],"tags":[]},{"location":"chapter_searching/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Binary search relies on data orderliness and progressively reduces the search interval by half through loops. It requires input data to be sorted and is only applicable to arrays or data structures based on array implementations.
  • Brute-force search locates data by traversing the data structure. Linear search is applicable to arrays and linked lists, while breadth-first search and depth-first search are applicable to graphs and trees. Such algorithms have good generality and require no data preprocessing, but have a relatively high time complexity of \\(O(n)\\).
  • Hash-based search, tree search, and binary search are efficient search methods that can quickly locate target elements in specific data structures. Such algorithms are highly efficient with time complexity reaching \\(O(\\log n)\\) or even \\(O(1)\\), but typically require additional data structures.
  • In practice, we need to analyze factors such as data scale, search performance requirements, and data query and update frequency to choose the appropriate search method.
  • Linear search is suitable for small-scale or frequently updated data; binary search is suitable for large-scale, sorted data; hash-based search is suitable for data with high query efficiency requirements and no need for range queries; tree search is suitable for large-scale dynamic data that needs to maintain order and support range queries.
  • Replacing linear search with hash-based search is a commonly used strategy to optimize runtime, reducing time complexity from \\(O(n)\\) to \\(O(1)\\).
","path":["Chapter 10. Searching","10.6   Summary"],"tags":[]},{"location":"chapter_sorting/","level":1,"title":"Chapter 11.   Sorting","text":"

Abstract

Sorting is like a magic key that transforms chaos into order, enabling us to understand and process data more efficiently.

Whether it's simple ascending order or complex categorized arrangements, sorting demonstrates the harmonious beauty of data.

","path":["Chapter 11. Sorting","Chapter 11.   Sorting"],"tags":[]},{"location":"chapter_sorting/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 11.1   Sorting Algorithms
  • 11.2   Selection Sort
  • 11.3   Bubble Sort
  • 11.4   Insertion Sort
  • 11.5   Quick Sort
  • 11.6   Merge Sort
  • 11.7   Heap Sort
  • 11.8   Bucket Sort
  • 11.9   Counting Sort
  • 11.10   Radix Sort
  • 11.11   Summary
","path":["Chapter 11. Sorting","Chapter 11.   Sorting"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   Bubble Sort","text":"

Bubble sort (bubble sort) achieves sorting by continuously comparing and swapping adjacent elements. This process is like bubbles rising from the bottom to the top, hence the name bubble sort.

As shown in Figure 11-4, the bubbling process can be simulated using element swap operations: starting from the leftmost end of the array and traversing to the right, compare the size of adjacent elements, and if \"left element > right element\", swap them. After completing the traversal, the largest element will be moved to the rightmost end of the array.

<1><2><3><4><5><6><7>

Figure 11-4   Simulating bubble using element swap operation

","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1131-algorithm-flow","level":2,"title":"11.3.1   Algorithm Flow","text":"

Assume the array has length \\(n\\). The steps of bubble sort are shown in Figure 11-5.

  1. First, perform \"bubbling\" on \\(n\\) elements, swapping the largest element of the array to its correct position.
  2. Next, perform \"bubbling\" on the remaining \\(n - 1\\) elements, swapping the second largest element to its correct position.
  3. And so on. After \\(n - 1\\) rounds of \"bubbling\", the largest \\(n - 1\\) elements have all been swapped to their correct positions.
  4. The only remaining element must be the smallest element, requiring no sorting, so the array sorting is complete.

Figure 11-5   Bubble sort flow

Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
def bubble_sort(nums: list[int]):\n    \"\"\"Bubble sort\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [0, i]\n    for i in range(n - 1, 0, -1):\n        # Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n
bubble_sort.cpp
/* Bubble sort */\nvoid bubbleSort(vector<int> &nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                // Using std::swap() function here\n                swap(nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
bubble_sort.java
/* Bubble sort */\nvoid bubbleSort(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.cs
/* Bubble sort */\nvoid BubbleSort(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
bubble_sort.go
/* Bubble sort */\nfunc bubbleSort(nums []int) {\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n            }\n        }\n    }\n}\n
bubble_sort.swift
/* Bubble sort */\nfunc bubbleSort(nums: inout [Int]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swapAt(j, j + 1)\n            }\n        }\n    }\n}\n
bubble_sort.js
/* Bubble sort */\nfunction bubbleSort(nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.ts
/* Bubble sort */\nfunction bubbleSort(nums: number[]): void {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.dart
/* Bubble sort */\nvoid bubbleSort(List<int> nums) {\n  // Outer loop: unsorted range is [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n      }\n    }\n  }\n}\n
bubble_sort.rs
/* Bubble sort */\nfn bubble_sort(nums: &mut [i32]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swap(j, j + 1);\n            }\n        }\n    }\n}\n
bubble_sort.c
/* Bubble sort */\nvoid bubbleSort(int nums[], int size) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n            }\n        }\n    }\n}\n
bubble_sort.kt
/* Bubble sort */\nfun bubbleSort(nums: IntArray) {\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n            }\n        }\n    }\n}\n
bubble_sort.rb
### Bubble sort ###\ndef bubble_sort(nums)\n  n = nums.length\n  # Outer loop: unsorted range is [0, i]\n  for i in (n - 1).downto(1)\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n
","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1132-efficiency-optimization","level":2,"title":"11.3.2   Efficiency Optimization","text":"

We notice that if no swap operations are performed during a certain round of \"bubbling\", it means the array has already completed sorting and can directly return the result. Therefore, we can add a flag flag to monitor this situation and return immediately once it occurs.

After optimization, the worst-case time complexity and average time complexity of bubble sort remain \\(O(n^2)\\); but when the input array is completely ordered, the best-case time complexity can reach \\(O(n)\\).

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
def bubble_sort_with_flag(nums: list[int]):\n    \"\"\"Bubble sort (flag optimization)\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [0, i]\n    for i in range(n - 1, 0, -1):\n        flag = False  # Initialize flag\n        # Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # Swap nums[j] and nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n                flag = True  # Record element swap\n        if not flag:\n            break  # No elements were swapped in this round of \"bubbling\", exit directly\n
bubble_sort.cpp
/* Bubble sort (flag optimization)*/\nvoid bubbleSortWithFlag(vector<int> &nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        bool flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                // Using std::swap() function here\n                swap(nums[j], nums[j + 1]);\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag)\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.java
/* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        boolean flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag)\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.cs
/* Bubble sort (flag optimization) */\nvoid BubbleSortWithFlag(int[] nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        bool flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                flag = true;  // Record element swap\n            }\n        }\n        if (!flag) break;     // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.go
/* Bubble sort (flag optimization) */\nfunc bubbleSortWithFlag(nums []int) {\n    // Outer loop: unsorted range is [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        flag := false // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // Swap nums[j] and nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n                flag = true // Record element swap\n            }\n        }\n        if flag == false { // No elements were swapped in this round of \"bubbling\", exit directly\n            break\n        }\n    }\n}\n
bubble_sort.swift
/* Bubble sort (flag optimization) */\nfunc bubbleSortWithFlag(nums: inout [Int]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        var flag = false // Initialize flag\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swapAt(j, j + 1)\n                flag = true // Record element swap\n            }\n        }\n        if !flag { // No elements were swapped in this round of \"bubbling\", exit directly\n            break\n        }\n    }\n}\n
bubble_sort.js
/* Bubble sort (flag optimization) */\nfunction bubbleSortWithFlag(nums) {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.ts
/* Bubble sort (flag optimization) */\nfunction bubbleSortWithFlag(nums: number[]): void {\n    // Outer loop: unsorted range is [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // Record element swap\n            }\n        }\n        if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.dart
/* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(List<int> nums) {\n  // Outer loop: unsorted range is [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    bool flag = false; // Initialize flag\n    // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // Swap nums[j] and nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        flag = true; // Record element swap\n      }\n    }\n    if (!flag) break; // No elements were swapped in this round of \"bubbling\", exit directly\n  }\n}\n
bubble_sort.rs
/* Bubble sort (flag optimization) */\nfn bubble_sort_with_flag(nums: &mut [i32]) {\n    // Outer loop: unsorted range is [0, i]\n    for i in (1..nums.len()).rev() {\n        let mut flag = false; // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // Swap nums[j] and nums[j + 1]\n                nums.swap(j, j + 1);\n                flag = true; // Record element swap\n            }\n        }\n        if !flag {\n            break; // No elements were swapped in this round of \"bubbling\", exit directly\n        };\n    }\n}\n
bubble_sort.c
/* Bubble sort (flag optimization) */\nvoid bubbleSortWithFlag(int nums[], int size) {\n    // Outer loop: unsorted range is [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        bool flag = false;\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n                flag = true;\n            }\n        }\n        if (!flag)\n            break;\n    }\n}\n
bubble_sort.kt
/* Bubble sort (flag optimization) */\nfun bubbleSortWithFlag(nums: IntArray) {\n    // Outer loop: unsorted range is [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        var flag = false // Initialize flag\n        // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // Swap nums[j] and nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                flag = true // Record element swap\n            }\n        }\n        if (!flag) break // No elements were swapped in this round of \"bubbling\", exit directly\n    }\n}\n
bubble_sort.rb
### Bubble sort (flag optimization) ###\ndef bubble_sort_with_flag(nums)\n  n = nums.length\n  # Outer loop: unsorted range is [0, i]\n  for i in (n - 1).downto(1)\n    flag = false # Initialize flag\n\n    # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # Swap nums[j] and nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n        flag = true # Record element swap\n      end\n    end\n\n    break unless flag # No elements were swapped in this round of \"bubbling\", exit directly\n  end\nend\n
","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1133-algorithm-characteristics","level":2,"title":"11.3.3   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n^2)\\), adaptive sorting: The array lengths traversed in each round of \"bubbling\" are \\(n - 1\\), \\(n - 2\\), \\(\\dots\\), \\(2\\), \\(1\\), totaling \\((n - 1) n / 2\\). After introducing the flag optimization, the best-case time complexity can reach \\(O(n)\\).
  • Space complexity of \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
  • Stable sorting: Since equal elements are not swapped during \"bubbling\".
","path":["Chapter 11. Sorting","11.3   Bubble Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/","level":1,"title":"11.8   Bucket Sort","text":"

The several sorting algorithms mentioned earlier all belong to \"comparison-based sorting algorithms\", which achieve sorting by comparing the size of elements. The time complexity of such sorting algorithms cannot exceed \\(O(n \\log n)\\). Next, we will explore several \"non-comparison sorting algorithms\", whose time complexity can reach linear order.

Bucket sort (bucket sort) is a typical application of the divide-and-conquer strategy. It works by setting up buckets with size order, each bucket corresponding to a data range, evenly distributing data to each bucket; then, sorting within each bucket separately; finally, merging all data in the order of the buckets.

","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1181-algorithm-flow","level":2,"title":"11.8.1   Algorithm Flow","text":"

Consider an array of length \\(n\\), whose elements are floating-point numbers in the range \\([0, 1)\\). The flow of bucket sort is shown in Figure 11-13.

  1. Initialize \\(k\\) buckets and distribute the \\(n\\) elements into the \\(k\\) buckets.
  2. Sort each bucket separately (here we use the built-in sorting function of the programming language).
  3. Merge the results in order from smallest to largest bucket.

Figure 11-13   Bucket sort algorithm flow

The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bucket_sort.py
def bucket_sort(nums: list[float]):\n    \"\"\"Bucket sort\"\"\"\n    # Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    k = len(nums) // 2\n    buckets = [[] for _ in range(k)]\n    # 1. Distribute array elements into various buckets\n    for num in nums:\n        # Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        i = int(num * k)\n        # Add num to bucket i\n        buckets[i].append(num)\n    # 2. Sort each bucket\n    for bucket in buckets:\n        # Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort()\n    # 3. Traverse buckets to merge results\n    i = 0\n    for bucket in buckets:\n        for num in bucket:\n            nums[i] = num\n            i += 1\n
bucket_sort.cpp
/* Bucket sort */\nvoid bucketSort(vector<float> &nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    int k = nums.size() / 2;\n    vector<vector<float>> buckets(k);\n    // 1. Distribute array elements into various buckets\n    for (float num : nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = num * k;\n        // Add num to bucket bucket_idx\n        buckets[i].push_back(num);\n    }\n    // 2. Sort each bucket\n    for (vector<float> &bucket : buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        sort(bucket.begin(), bucket.end());\n    }\n    // 3. Traverse buckets to merge results\n    int i = 0;\n    for (vector<float> &bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.java
/* Bucket sort */\nvoid bucketSort(float[] nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    int k = nums.length / 2;\n    List<List<Float>> buckets = new ArrayList<>();\n    for (int i = 0; i < k; i++) {\n        buckets.add(new ArrayList<>());\n    }\n    // 1. Distribute array elements into various buckets\n    for (float num : nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = (int) (num * k);\n        // Add num to bucket i\n        buckets.get(i).add(num);\n    }\n    // 2. Sort each bucket\n    for (List<Float> bucket : buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        Collections.sort(bucket);\n    }\n    // 3. Traverse buckets to merge results\n    int i = 0;\n    for (List<Float> bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.cs
/* Bucket sort */\nvoid BucketSort(float[] nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    int k = nums.Length / 2;\n    List<List<float>> buckets = [];\n    for (int i = 0; i < k; i++) {\n        buckets.Add([]);\n    }\n    // 1. Distribute array elements into various buckets\n    foreach (float num in nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        int i = (int)(num * k);\n        // Add num to bucket i\n        buckets[i].Add(num);\n    }\n    // 2. Sort each bucket\n    foreach (List<float> bucket in buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.Sort();\n    }\n    // 3. Traverse buckets to merge results\n    int j = 0;\n    foreach (List<float> bucket in buckets) {\n        foreach (float num in bucket) {\n            nums[j++] = num;\n        }\n    }\n}\n
bucket_sort.go
/* Bucket sort */\nfunc bucketSort(nums []float64) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    k := len(nums) / 2\n    buckets := make([][]float64, k)\n    for i := 0; i < k; i++ {\n        buckets[i] = make([]float64, 0)\n    }\n    // 1. Distribute array elements into various buckets\n    for _, num := range nums {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        i := int(num * float64(k))\n        // Add num to bucket i\n        buckets[i] = append(buckets[i], num)\n    }\n    // 2. Sort each bucket\n    for i := 0; i < k; i++ {\n        // Use built-in slice sorting function, can also be replaced with other sorting algorithms\n        sort.Float64s(buckets[i])\n    }\n    // 3. Traverse buckets to merge results\n    i := 0\n    for _, bucket := range buckets {\n        for _, num := range bucket {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
bucket_sort.swift
/* Bucket sort */\nfunc bucketSort(nums: inout [Double]) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    let k = nums.count / 2\n    var buckets = (0 ..< k).map { _ in [Double]() }\n    // 1. Distribute array elements into various buckets\n    for num in nums {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        let i = Int(num * Double(k))\n        // Add num to bucket i\n        buckets[i].append(num)\n    }\n    // 2. Sort each bucket\n    for i in buckets.indices {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        buckets[i].sort()\n    }\n    // 3. Traverse buckets to merge results\n    var i = nums.startIndex\n    for bucket in buckets {\n        for num in bucket {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
bucket_sort.js
/* Bucket sort */\nfunction bucketSort(nums) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    const k = nums.length / 2;\n    const buckets = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Distribute array elements into various buckets\n    for (const num of nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        const i = Math.floor(num * k);\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for (const bucket of buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Traverse buckets to merge results\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.ts
/* Bucket sort */\nfunction bucketSort(nums: number[]): void {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    const k = nums.length / 2;\n    const buckets: number[][] = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. Distribute array elements into various buckets\n    for (const num of nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        const i = Math.floor(num * k);\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for (const bucket of buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. Traverse buckets to merge results\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.dart
/* Bucket sort */\nvoid bucketSort(List<double> nums) {\n  // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n  int k = nums.length ~/ 2;\n  List<List<double>> buckets = List.generate(k, (index) => []);\n\n  // 1. Distribute array elements into various buckets\n  for (double _num in nums) {\n    // Input data range is [0, 1), use _num * k to map to index range [0, k-1]\n    int i = (_num * k).toInt();\n    // Add _num to bucket bucket_idx\n    buckets[i].add(_num);\n  }\n  // 2. Sort each bucket\n  for (List<double> bucket in buckets) {\n    bucket.sort();\n  }\n  // 3. Traverse buckets to merge results\n  int i = 0;\n  for (List<double> bucket in buckets) {\n    for (double _num in bucket) {\n      nums[i++] = _num;\n    }\n  }\n}\n
bucket_sort.rs
/* Bucket sort */\nfn bucket_sort(nums: &mut [f64]) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    let k = nums.len() / 2;\n    let mut buckets = vec![vec![]; k];\n    // 1. Distribute array elements into various buckets\n    for &num in nums.iter() {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        let i = (num * k as f64) as usize;\n        // Add num to bucket i\n        buckets[i].push(num);\n    }\n    // 2. Sort each bucket\n    for bucket in &mut buckets {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());\n    }\n    // 3. Traverse buckets to merge results\n    let mut i = 0;\n    for bucket in buckets.iter() {\n        for &num in bucket.iter() {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
bucket_sort.c
/* Bucket sort */\nvoid bucketSort(float nums[], int n) {\n    int k = n / 2;                                 // Initialize k = n/2 buckets\n    int *sizes = malloc(k * sizeof(int));          // Record each bucket's size\n    float **buckets = malloc(k * sizeof(float *)); // Array of dynamic arrays (buckets)\n    // Pre-allocate sufficient space for each bucket\n    for (int i = 0; i < k; ++i) {\n        buckets[i] = (float *)malloc(n * sizeof(float));\n        sizes[i] = 0;\n    }\n    // 1. Distribute array elements into various buckets\n    for (int i = 0; i < n; ++i) {\n        int idx = (int)(nums[i] * k);\n        buckets[idx][sizes[idx]++] = nums[i];\n    }\n    // 2. Sort each bucket\n    for (int i = 0; i < k; ++i) {\n        qsort(buckets[i], sizes[i], sizeof(float), compare);\n    }\n    // 3. Merge sorted buckets\n    int idx = 0;\n    for (int i = 0; i < k; ++i) {\n        for (int j = 0; j < sizes[i]; ++j) {\n            nums[idx++] = buckets[i][j];\n        }\n        // Free memory\n        free(buckets[i]);\n    }\n}\n
bucket_sort.kt
/* Bucket sort */\nfun bucketSort(nums: FloatArray) {\n    // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n    val k = nums.size / 2\n    val buckets = mutableListOf<MutableList<Float>>()\n    for (i in 0..<k) {\n        buckets.add(mutableListOf())\n    }\n    // 1. Distribute array elements into various buckets\n    for (num in nums) {\n        // Input data range is [0, 1), use num * k to map to index range [0, k-1]\n        val i = (num * k).toInt()\n        // Add num to bucket i\n        buckets[i].add(num)\n    }\n    // 2. Sort each bucket\n    for (bucket in buckets) {\n        // Use built-in sorting function, can also replace with other sorting algorithms\n        bucket.sort()\n    }\n    // 3. Traverse buckets to merge results\n    var i = 0\n    for (bucket in buckets) {\n        for (num in bucket) {\n            nums[i++] = num\n        }\n    }\n}\n
bucket_sort.rb
### Bucket sort ###\ndef bucket_sort(nums)\n  # Initialize k = n/2 buckets, expected to allocate 2 elements per bucket\n  k = nums.length / 2\n  buckets = Array.new(k) { [] }\n\n  # 1. Distribute array elements into various buckets\n  nums.each do |num|\n    # Input data range is [0, 1), use num * k to map to index range [0, k-1]\n    i = (num * k).to_i\n    # Add num to bucket i\n    buckets[i] << num\n  end\n\n  # 2. Sort each bucket\n  buckets.each do |bucket|\n    # Use built-in sorting function, can also replace with other sorting algorithms\n    bucket.sort!\n  end\n\n  # 3. Traverse buckets to merge results\n  i = 0\n  buckets.each do |bucket|\n    bucket.each do |num|\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1182-algorithm-characteristics","level":2,"title":"11.8.2   Algorithm Characteristics","text":"

Bucket sort is suitable for processing very large data volumes. For example, if the input data contains 1 million elements and system memory cannot load all the data at once, the data can be divided into 1000 buckets, each bucket sorted separately, and then the results merged.

  • Time complexity of \\(O(n + k)\\): Assuming the elements are evenly distributed among the buckets, then the number of elements in each bucket is \\(\\frac{n}{k}\\). Assuming sorting a single bucket uses \\(O(\\frac{n}{k} \\log\\frac{n}{k})\\) time, then sorting all buckets uses \\(O(n \\log\\frac{n}{k})\\) time. When the number of buckets \\(k\\) is relatively large, the time complexity approaches \\(O(n)\\). Merging results requires traversing all buckets and elements, taking \\(O(n + k)\\) time. In the worst case, all data is distributed into one bucket, and sorting that bucket uses \\(O(n^2)\\) time.
  • Space complexity of \\(O(n + k)\\), non-in-place sorting: Additional space is required for \\(k\\) buckets and a total of \\(n\\) elements.
  • Whether bucket sort is stable depends on whether the algorithm for sorting elements within buckets is stable.
","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1183-how-to-achieve-even-distribution","level":2,"title":"11.8.3   How to Achieve Even Distribution","text":"

Theoretically, bucket sort can achieve \\(O(n)\\) time complexity. The key is to evenly distribute elements to each bucket, because real data is often not evenly distributed. For example, if we want to evenly distribute all products on Taobao into 10 buckets by price range, there may be very many products below 100 yuan and very few above 1000 yuan. If the price intervals are evenly divided into 10, the difference in the number of products in each bucket will be very large.

To achieve even distribution, we can first set an approximate dividing line to roughly divide the data into 3 buckets. After distribution is complete, continue dividing buckets with more products into 3 buckets until the number of elements in all buckets is roughly equal.

As shown in Figure 11-14, this method essentially creates a recursion tree, with the goal of making the values of leaf nodes as even as possible. Of course, it is not necessary to divide the data into 3 buckets every round; the specific division method can be flexibly chosen according to data characteristics.

Figure 11-14   Recursively dividing buckets

If we know the probability distribution of product prices in advance, we can set the price dividing line for each bucket based on the data probability distribution. It is worth noting that the data distribution does not necessarily need to be specifically calculated, but can also be approximated using a certain probability model based on data characteristics.

As shown in Figure 11-15, we assume that product prices follow a normal distribution, which allows us to reasonably set price intervals to evenly distribute products to each bucket.

Figure 11-15   Dividing buckets based on probability distribution

","path":["Chapter 11. Sorting","11.8   Bucket Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/","level":1,"title":"11.9   Counting Sort","text":"

Counting sort (counting sort) achieves sorting by counting the number of elements, typically applied to integer arrays.

","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1191-simple-implementation","level":2,"title":"11.9.1   Simple Implementation","text":"

Let's start with a simple example. Given an array nums of length \\(n\\), where the elements are all \"non-negative integers\", the overall flow of counting sort is shown in Figure 11-16.

  1. Traverse the array to find the largest number, denoted as \\(m\\), and then create an auxiliary array counter of length \\(m + 1\\).
  2. Use counter to count the number of occurrences of each number in nums, where counter[num] corresponds to the number of occurrences of the number num. The counting method is simple: just traverse nums (let the current number be num), and increase counter[num] by \\(1\\) in each round.
  3. Since each index of counter is naturally ordered, this is equivalent to all numbers being sorted. Next, we traverse counter and fill in nums in ascending order based on the number of occurrences of each number.

Figure 11-16   Counting sort flow

The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
def counting_sort_naive(nums: list[int]):\n    \"\"\"Counting sort\"\"\"\n    # Simple implementation, cannot be used for sorting objects\n    # 1. Count the maximum element m in the array\n    m = 0\n    for num in nums:\n        m = max(m, num)\n    # 2. Count the occurrence of each number\n    # counter[num] represents the occurrence of num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Traverse counter, filling each element back into the original array nums\n    i = 0\n    for num in range(m + 1):\n        for _ in range(counter[num]):\n            nums[i] = num\n            i += 1\n
counting_sort.cpp
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(vector<int> &nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.java
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.cs
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid CountingSortNaive(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.go
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunc countingSortNaive(nums []int) {\n    // 1. Count the maximum element m in the array\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    for i, num := 0, 0; num < m+1; num++ {\n        for j := 0; j < counter[num]; j++ {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
counting_sort.swift
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunc countingSortNaive(nums: inout [Int]) {\n    // 1. Count the maximum element m in the array\n    let m = nums.max()!\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    var i = 0\n    for num in 0 ..< m + 1 {\n        for _ in 0 ..< counter[num] {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
counting_sort.js
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunction countingSortNaive(nums) {\n    // 1. Count the maximum element m in the array\n    let m = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.ts
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfunction countingSortNaive(nums: number[]): void {\n    // 1. Count the maximum element m in the array\n    let m: number = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.dart
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(List<int> nums) {\n  // 1. Count the maximum element m in the array\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Count the occurrence of each number\n  // counter[_num] represents occurrence count of _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Traverse counter, filling each element back into the original array nums\n  int i = 0;\n  for (int _num = 0; _num < m + 1; _num++) {\n    for (int j = 0; j < counter[_num]; j++, i++) {\n      nums[i] = _num;\n    }\n  }\n}\n
counting_sort.rs
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfn counting_sort_naive(nums: &mut [i32]) {\n    // 1. Count the maximum element m in the array\n    let m = *nums.iter().max().unwrap();\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    let mut counter = vec![0; m as usize + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    let mut i = 0;\n    for num in 0..m + 1 {\n        for _ in 0..counter[num as usize] {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
counting_sort.c
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nvoid countingSortNaive(int nums[], int size) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int *counter = calloc(m + 1, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n    // 4. Free memory\n    free(counter);\n}\n
counting_sort.kt
/* Counting sort */\n// Simple implementation, cannot be used for sorting objects\nfun countingSortNaive(nums: IntArray) {\n    // 1. Count the maximum element m in the array\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Traverse counter, filling each element back into the original array nums\n    var i = 0\n    for (num in 0..<m + 1) {\n        var j = 0\n        while (j < counter[num]) {\n            nums[i] = num\n            j++\n            i++\n        }\n    }\n}\n
counting_sort.rb
### Counting sort ###\ndef counting_sort_naive(nums)\n  # Simple implementation, cannot be used for sorting objects\n  # 1. Count the maximum element m in the array\n  m = 0\n  nums.each { |num| m = [m, num].max }\n  # 2. Count the occurrence of each number\n  # counter[num] represents the occurrence of num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Traverse counter, filling each element back into the original array nums\n  i = 0\n  for num in 0...(m + 1)\n    (0...counter[num]).each do\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n

Connection between counting sort and bucket sort

From the perspective of bucket sort, we can regard each index of the counting array counter in counting sort as a bucket, and the process of counting quantities as distributing each element to the corresponding bucket. Essentially, counting sort is a special case of bucket sort for integer data.

","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1192-complete-implementation","level":2,"title":"11.9.2   Complete Implementation","text":"

Observant readers may have noticed that if the input data is objects, step 3. above becomes invalid. Suppose the input data is product objects, and we want to sort the products by price (a member variable of the class), but the above algorithm can only give the sorting result of prices.

So how can we obtain the sorting result of the original data? We first calculate the \"prefix sum\" of counter. As the name suggests, the prefix sum at index i, prefix[i], equals the sum of the first i elements of the array:

\\[ \\text{prefix}[i] = \\sum_{j=0}^i \\text{counter[j]} \\]

The prefix sum has a clear meaning: prefix[num] - 1 represents the index of the last occurrence of element num in the result array res. This information is very critical because it tells us where each element should appear in the result array. Next, we traverse each element num of the original array nums in reverse order, performing the following two steps in each iteration.

  1. Fill num into the array res at index prefix[num] - 1.
  2. Decrease the prefix sum prefix[num] by \\(1\\) to get the index for the next placement of num.

After the traversal is complete, the array res contains the sorted result, and finally res is used to overwrite the original array nums. The complete counting sort flow is shown in Figure 11-17.

<1><2><3><4><5><6><7><8>

Figure 11-17   Counting sort steps

The implementation code of counting sort is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
def counting_sort(nums: list[int]):\n    \"\"\"Counting sort\"\"\"\n    # Complete implementation, can sort objects and is a stable sort\n    # 1. Count the maximum element m in the array\n    m = max(nums)\n    # 2. Count the occurrence of each number\n    # counter[num] represents the occurrence of num\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    # counter[num]-1 is the last index where num appears in res\n    for i in range(m):\n        counter[i + 1] += counter[i]\n    # 4. Traverse nums in reverse order, placing each element into the result array res\n    # Initialize the array res to record results\n    n = len(nums)\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        num = nums[i]\n        res[counter[num] - 1] = num  # Place num at the corresponding index\n        counter[num] -= 1  # Decrement the prefix sum by 1, getting the next index to place num\n    # Use result array res to overwrite the original array nums\n    for i in range(n):\n        nums[i] = res[i]\n
counting_sort.cpp
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(vector<int> &nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    int n = nums.size();\n    vector<int> res(n);\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--;              // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    nums = res;\n}\n
counting_sort.java
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    int n = nums.length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.cs
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid CountingSort(int[] nums) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    int n = nums.Length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.go
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunc countingSort(nums []int) {\n    // 1. Count the maximum element m in the array\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i := 0; i < m; i++ {\n        counter[i+1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    n := len(nums)\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        num := nums[i]\n        // Place num at the corresponding index\n        res[counter[num]-1] = num\n        // Decrement the prefix sum by 1, getting the next index to place num\n        counter[num]--\n    }\n    // Use result array res to overwrite the original array nums\n    copy(nums, res)\n}\n
counting_sort.swift
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunc countingSort(nums: inout [Int]) {\n    // 1. Count the maximum element m in the array\n    let m = nums.max()!\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i in 0 ..< m {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let num = nums[i]\n        res[counter[num] - 1] = num // Place num at the corresponding index\n        counter[num] -= 1 // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n
counting_sort.js
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunction countingSort(nums) {\n    // 1. Count the maximum element m in the array\n    let m = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    const n = nums.length;\n    const res = new Array(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.ts
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfunction countingSort(nums: number[]): void {\n    // 1. Count the maximum element m in the array\n    let m: number = Math.max(...nums);\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    const n = nums.length;\n    const res: number[] = new Array<number>(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.dart
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(List<int> nums) {\n  // 1. Count the maximum element m in the array\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. Count the occurrence of each number\n  // counter[_num] represents occurrence count of _num\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n  // That is, counter[_num]-1 is the last occurrence index of _num in res\n  for (int i = 0; i < m; i++) {\n    counter[i + 1] += counter[i];\n  }\n  // 4. Traverse nums in reverse order, placing each element into the result array res\n  // Initialize the array res to record results\n  int n = nums.length;\n  List<int> res = List.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int _num = nums[i];\n    res[counter[_num] - 1] = _num; // Place _num at corresponding index\n    counter[_num]--; // Decrement prefix sum by 1 to get next placement index for _num\n  }\n  // Use result array res to overwrite the original array nums\n  nums.setAll(0, res);\n}\n
counting_sort.rs
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfn counting_sort(nums: &mut [i32]) {\n    // 1. Count the maximum element m in the array\n    let m = *nums.iter().max().unwrap() as usize;\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    let mut counter = vec![0; m + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for i in 0..m {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    let n = nums.len();\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let num = nums[i];\n        res[counter[num as usize] - 1] = num; // Place num at the corresponding index\n        counter[num as usize] -= 1; // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    nums.copy_from_slice(&res)\n}\n
counting_sort.c
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nvoid countingSort(int nums[], int size) {\n    // 1. Count the maximum element m in the array\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    int *counter = calloc(m, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    int *res = malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // Place num at the corresponding index\n        counter[num]--;              // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    memcpy(nums, res, size * sizeof(int));\n    // 5. Free memory\n    free(res);\n    free(counter);\n}\n
counting_sort.kt
/* Counting sort */\n// Complete implementation, can sort objects and is a stable sort\nfun countingSort(nums: IntArray) {\n    // 1. Count the maximum element m in the array\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. Count the occurrence of each number\n    // counter[num] represents the occurrence of num\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n    // counter[num]-1 is the last index where num appears in res\n    for (i in 0..<m) {\n        counter[i + 1] += counter[i]\n    }\n    // 4. Traverse nums in reverse order, placing each element into the result array res\n    // Initialize the array res to record results\n    val n = nums.size\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val num = nums[i]\n        res[counter[num] - 1] = num // Place num at the corresponding index\n        counter[num]-- // Decrement the prefix sum by 1, getting the next index to place num\n    }\n    // Use result array res to overwrite the original array nums\n    for (i in 0..<n) {\n        nums[i] = res[i]\n    }\n}\n
counting_sort.rb
### Counting sort ###\ndef counting_sort(nums)\n  # Complete implementation, can sort objects and is a stable sort\n  # 1. Count the maximum element m in the array\n  m = nums.max\n  # 2. Count the occurrence of each number\n  # counter[num] represents the occurrence of num\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. Calculate the prefix sum of counter, converting \"occurrence count\" to \"tail index\"\n  # counter[num]-1 is the last index where num appears in res\n  (0...m).each { |i| counter[i + 1] += counter[i] }\n  # 4. Traverse nums in reverse, fill elements into result array res\n  # Initialize the array res to record results\n  n = nums.length\n  res = Array.new(n, 0)\n  (n - 1).downto(0).each do |i|\n    num = nums[i]\n    res[counter[num] - 1] = num # Place num at the corresponding index\n    counter[num] -= 1 # Decrement the prefix sum by 1, getting the next index to place num\n  end\n  # Use result array res to overwrite the original array nums\n  (0...n).each { |i| nums[i] = res[i] }\nend\n
","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1193-algorithm-characteristics","level":2,"title":"11.9.3   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n + m)\\), non-adaptive sorting: Involves traversing nums and traversing counter, both using linear time. Generally, \\(n \\gg m\\), and time complexity tends toward \\(O(n)\\).
  • Space complexity of \\(O(n + m)\\), non-in-place sorting: Uses arrays res and counter of lengths \\(n\\) and \\(m\\) respectively.
  • Stable sorting: Since elements are filled into res in a \"right-to-left\" order, traversing nums in reverse can avoid changing the relative positions of equal elements, thereby achieving stable sorting. In fact, traversing nums in forward order can also yield correct sorting results, but the result would be unstable.
","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1194-limitations","level":2,"title":"11.9.4   Limitations","text":"

By this point, you might think counting sort is very clever, as it can achieve efficient sorting just by counting quantities. However, the prerequisites for using counting sort are relatively strict.

Counting sort is only suitable for non-negative integers. If you want to apply it to other types of data, you need to ensure that the data can be converted to non-negative integers without changing the relative size relationships between elements. For example, for an integer array containing negative numbers, you can first add a constant to all numbers to convert them all to positive numbers, and then convert them back after sorting is complete.

Counting sort is suitable for situations where the data volume is large but the data range is small. For example, in the above example, \\(m\\) cannot be too large, otherwise it will occupy too much space. And when \\(n \\ll m\\), counting sort uses \\(O(m)\\) time, which may be slower than \\(O(n \\log n)\\) sorting algorithms.

","path":["Chapter 11. Sorting","11.9   Counting Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/","level":1,"title":"11.7   Heap Sort","text":"

Tip

Before reading this section, please ensure you have completed the \"Heap\" chapter.

Heap sort (heap sort) is an efficient sorting algorithm based on the heap data structure. We can use the \"build heap operation\" and \"element out-heap operation\" that we have already learned to implement heap sort.

  1. Input the array and build a min-heap, at which point the smallest element is at the heap top.
  2. Continuously perform the out-heap operation, record the out-heap elements in sequence, and an ascending sorted sequence can be obtained.

Although the above method is feasible, it requires an additional array to save the popped elements, which is quite wasteful of space. In practice, we usually use a more elegant implementation method.

","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1171-algorithm-flow","level":2,"title":"11.7.1   Algorithm Flow","text":"

Assume the array length is \\(n\\). The flow of heap sort is shown in Figure 11-12.

  1. Input the array and build a max-heap. After completion, the largest element is at the heap top.
  2. Swap the heap top element (first element) with the heap bottom element (last element). After the swap is complete, reduce the heap length by \\(1\\) and increase the count of sorted elements by \\(1\\).
  3. Starting from the heap top element, perform top-to-bottom heapify operation (sift down). After heapify is complete, the heap property is restored.
  4. Loop through steps 2. and 3. After looping \\(n - 1\\) rounds, the array sorting can be completed.

Tip

In fact, the element out-heap operation also includes steps 2. and 3., with just an additional step to pop the element.

<1><2><3><4><5><6><7><8><9><10><11><12>

Figure 11-12   Heap sort steps

In the code implementation, we use the same top-to-bottom heapify function sift_down() from the \"Heap\" chapter. It is worth noting that since the heap length will decrease as the largest element is extracted, we need to add a length parameter \\(n\\) to the sift_down() function to specify the current effective length of the heap. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap_sort.py
def sift_down(nums: list[int], n: int, i: int):\n    \"\"\"Heap length is n, start heapifying node i, from top to bottom\"\"\"\n    while True:\n        # Determine the largest node among i, l, r, noted as ma\n        l = 2 * i + 1\n        r = 2 * i + 2\n        ma = i\n        if l < n and nums[l] > nums[ma]:\n            ma = l\n        if r < n and nums[r] > nums[ma]:\n            ma = r\n        # If node i is the largest or indices l, r are out of bounds, no further heapification needed, break\n        if ma == i:\n            break\n        # Swap two nodes\n        nums[i], nums[ma] = nums[ma], nums[i]\n        # Loop downwards heapification\n        i = ma\n\ndef heap_sort(nums: list[int]):\n    \"\"\"Heap sort\"\"\"\n    # Build heap operation: heapify all nodes except leaves\n    for i in range(len(nums) // 2 - 1, -1, -1):\n        sift_down(nums, len(nums), i)\n    # Extract the largest element from the heap and repeat for n-1 rounds\n    for i in range(len(nums) - 1, 0, -1):\n        # Swap the root node with the rightmost leaf node (swap the first element with the last element)\n        nums[0], nums[i] = nums[i], nums[0]\n        # Start heapifying the root node, from top to bottom\n        sift_down(nums, i, 0)\n
heap_sort.cpp
/* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(vector<int> &nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Swap two nodes\n        if (ma == i) {\n            break;\n        }\n        // Swap two nodes\n        swap(nums[i], nums[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(vector<int> &nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.size() / 2 - 1; i >= 0; --i) {\n        siftDown(nums, nums.size(), i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.size() - 1; i > 0; --i) {\n        // Delete node\n        swap(nums[0], nums[i]);\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.java
/* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(int[] nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(int[] nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.length / 2 - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.cs
/* Heap length is n, start heapifying node i, from top to bottom */\nvoid SiftDown(int[] nums, int n, int i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Swap two nodes\n        if (ma == i)\n            break;\n        // Swap two nodes\n        (nums[ma], nums[i]) = (nums[i], nums[ma]);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid HeapSort(int[] nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = nums.Length / 2 - 1; i >= 0; i--) {\n        SiftDown(nums, nums.Length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // Delete node\n        (nums[i], nums[0]) = (nums[0], nums[i]);\n        // Start heapifying the root node, from top to bottom\n        SiftDown(nums, i, 0);\n    }\n}\n
heap_sort.go
/* Heap length is n, start heapifying node i, from top to bottom */\nfunc siftDown(nums *[]int, n, i int) {\n    for true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        l := 2*i + 1\n        r := 2*i + 2\n        ma := i\n        if l < n && (*nums)[l] > (*nums)[ma] {\n            ma = l\n        }\n        if r < n && (*nums)[r] > (*nums)[ma] {\n            ma = r\n        }\n        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfunc heapSort(nums *[]int) {\n    // Build heap operation: heapify all nodes except leaves\n    for i := len(*nums)/2 - 1; i >= 0; i-- {\n        siftDown(nums, len(*nums), i)\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i := len(*nums) - 1; i > 0; i-- {\n        // Delete node\n        (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0)\n    }\n}\n
heap_sort.swift
/* Heap length is n, start heapifying node i, from top to bottom */\nfunc siftDown(nums: inout [Int], n: Int, i: Int) {\n    var i = i\n    while true {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let l = 2 * i + 1\n        let r = 2 * i + 2\n        var ma = i\n        if l < n, nums[l] > nums[ma] {\n            ma = l\n        }\n        if r < n, nums[r] > nums[ma] {\n            ma = r\n        }\n        // Swap two nodes\n        if ma == i {\n            break\n        }\n        // Swap two nodes\n        nums.swapAt(i, ma)\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfunc heapSort(nums: inout [Int]) {\n    // Build heap operation: heapify all nodes except leaves\n    for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {\n        siftDown(nums: &nums, n: nums.count, i: i)\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i in nums.indices.dropFirst().reversed() {\n        // Delete node\n        nums.swapAt(0, i)\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums: &nums, n: i, i: 0)\n    }\n}\n
heap_sort.js
/* Heap length is n, start heapifying node i, from top to bottom */\nfunction siftDown(nums, n, i) {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // Swap two nodes\n        if (ma === i) {\n            break;\n        }\n        // Swap two nodes\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfunction heapSort(nums) {\n    // Build heap operation: heapify all nodes except leaves\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.ts
/* Heap length is n, start heapifying node i, from top to bottom */\nfunction siftDown(nums: number[], n: number, i: number): void {\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // Swap two nodes\n        if (ma === i) {\n            break;\n        }\n        // Swap two nodes\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfunction heapSort(nums: number[]): void {\n    // Build heap operation: heapify all nodes except leaves\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (let i = nums.length - 1; i > 0; i--) {\n        // Delete node\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.dart
/* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(List<int> nums, int n, int i) {\n  while (true) {\n    // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n    int l = 2 * i + 1;\n    int r = 2 * i + 2;\n    int ma = i;\n    if (l < n && nums[l] > nums[ma]) ma = l;\n    if (r < n && nums[r] > nums[ma]) ma = r;\n    // Swap two nodes\n    if (ma == i) break;\n    // Swap two nodes\n    int temp = nums[i];\n    nums[i] = nums[ma];\n    nums[ma] = temp;\n    // Loop downwards heapification\n    i = ma;\n  }\n}\n\n/* Heap sort */\nvoid heapSort(List<int> nums) {\n  // Build heap operation: heapify all nodes except leaves\n  for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {\n    siftDown(nums, nums.length, i);\n  }\n  // Extract the largest element from the heap and repeat for n-1 rounds\n  for (int i = nums.length - 1; i > 0; i--) {\n    // Delete node\n    int tmp = nums[0];\n    nums[0] = nums[i];\n    nums[i] = tmp;\n    // Start heapifying the root node, from top to bottom\n    siftDown(nums, i, 0);\n  }\n}\n
heap_sort.rs
/* Heap length is n, start heapifying node i, from top to bottom */\nfn sift_down(nums: &mut [i32], n: usize, mut i: usize) {\n    loop {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let mut ma = i;\n        if l < n && nums[l] > nums[ma] {\n            ma = l;\n        }\n        if r < n && nums[r] > nums[ma] {\n            ma = r;\n        }\n        // Swap two nodes\n        if ma == i {\n            break;\n        }\n        // Swap two nodes\n        nums.swap(i, ma);\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nfn heap_sort(nums: &mut [i32]) {\n    // Build heap operation: heapify all nodes except leaves\n    for i in (0..nums.len() / 2).rev() {\n        sift_down(nums, nums.len(), i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for i in (1..nums.len()).rev() {\n        // Delete node\n        nums.swap(0, i);\n        // Start heapifying the root node, from top to bottom\n        sift_down(nums, i, 0);\n    }\n}\n
heap_sort.c
/* Heap length is n, start heapifying node i, from top to bottom */\nvoid siftDown(int nums[], int n, int i) {\n    while (1) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // Swap two nodes\n        if (ma == i) {\n            break;\n        }\n        // Swap two nodes\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // Loop downwards heapification\n        i = ma;\n    }\n}\n\n/* Heap sort */\nvoid heapSort(int nums[], int n) {\n    // Build heap operation: heapify all nodes except leaves\n    for (int i = n / 2 - 1; i >= 0; --i) {\n        siftDown(nums, n, i);\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (int i = n - 1; i > 0; --i) {\n        // Delete node\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.kt
/* Heap length is n, start heapifying node i, from top to bottom */\nfun siftDown(nums: IntArray, n: Int, li: Int) {\n    var i = li\n    while (true) {\n        // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n        val l = 2 * i + 1\n        val r = 2 * i + 2\n        var ma = i\n        if (l < n && nums[l] > nums[ma]) \n            ma = l\n        if (r < n && nums[r] > nums[ma]) \n            ma = r\n        // Swap two nodes\n        if (ma == i) \n            break\n        // Swap two nodes\n        val temp = nums[i]\n        nums[i] = nums[ma]\n        nums[ma] = temp\n        // Loop downwards heapification\n        i = ma\n    }\n}\n\n/* Heap sort */\nfun heapSort(nums: IntArray) {\n    // Build heap operation: heapify all nodes except leaves\n    for (i in nums.size / 2 - 1 downTo 0) {\n        siftDown(nums, nums.size, i)\n    }\n    // Extract the largest element from the heap and repeat for n-1 rounds\n    for (i in nums.size - 1 downTo 1) {\n        // Delete node\n        val temp = nums[0]\n        nums[0] = nums[i]\n        nums[i] = temp\n        // Start heapifying the root node, from top to bottom\n        siftDown(nums, i, 0)\n    }\n}\n
heap_sort.rb
### Heap length is n, heapify from node i, top to bottom ###\ndef sift_down(nums, n, i)\n  while true\n    # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break\n    l = 2 * i + 1\n    r = 2 * i + 2\n    ma = i\n    ma = l if l < n && nums[l] > nums[ma]\n    ma = r if r < n && nums[r] > nums[ma]\n    # Swap two nodes\n    break if ma == i\n    # Swap two nodes\n    nums[i], nums[ma] = nums[ma], nums[i]\n    # Loop downwards heapification\n    i = ma\n  end\nend\n\n### Heap sort ###\ndef heap_sort(nums)\n  # Build heap operation: heapify all nodes except leaves\n  (nums.length / 2 - 1).downto(0) do |i|\n    sift_down(nums, nums.length, i)\n  end\n  # Extract the largest element from the heap and repeat for n-1 rounds\n  (nums.length - 1).downto(1) do |i|\n    # Delete node\n    nums[0], nums[i] = nums[i], nums[0]\n    # Start heapifying the root node, from top to bottom\n    sift_down(nums, i, 0)\n  end\nend\n
","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1172-algorithm-characteristics","level":2,"title":"11.7.2   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n \\log n)\\), non-adaptive sorting: The build heap operation uses \\(O(n)\\) time. Extracting the largest element from the heap has a time complexity of \\(O(\\log n)\\), looping a total of \\(n - 1\\) rounds.
  • Space complexity of \\(O(1)\\), in-place sorting: A few pointer variables use \\(O(1)\\) space. Element swapping and heapify operations are both performed on the original array.
  • Non-stable sorting: When swapping the heap top element and heap bottom element, the relative positions of equal elements may change.
","path":["Chapter 11. Sorting","11.7   Heap Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/","level":1,"title":"11.4   Insertion Sort","text":"

Insertion sort (insertion sort) is a simple sorting algorithm that works very similarly to the process of manually organizing a deck of cards.

Specifically, we select a base element from the unsorted interval, compare the element with elements in the sorted interval to its left one by one, and insert the element into the correct position.

Figure 11-6 shows the operation flow of inserting an element into the array. Let the base element be base. We need to move all elements from the target index to base one position to the right, and then assign base to the target index.

Figure 11-6   Single insertion operation

","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141-algorithm-flow","level":2,"title":"11.4.1   Algorithm Flow","text":"

The overall flow of insertion sort is shown in Figure 11-7.

  1. Initially, the first element of the array has completed sorting.
  2. Select the second element of the array as base, and after inserting it into the correct position, the first 2 elements of the array are sorted.
  3. Select the third element as base, and after inserting it into the correct position, the first 3 elements of the array are sorted.
  4. And so on. In the last round, select the last element as base, and after inserting it into the correct position, all elements are sorted.

Figure 11-7   Insertion sort flow

Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby insertion_sort.py
def insertion_sort(nums: list[int]):\n    \"\"\"Insertion sort\"\"\"\n    # Outer loop: sorted interval is [0, i-1]\n    for i in range(1, len(nums)):\n        base = nums[i]\n        j = i - 1\n        # Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0 and nums[j] > base:\n            nums[j + 1] = nums[j]  # Move nums[j] to the right by one position\n            j -= 1\n        nums[j + 1] = base  # Assign base to the correct position\n
insertion_sort.cpp
/* Insertion sort */\nvoid insertionSort(vector<int> &nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.size(); i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
insertion_sort.java
/* Insertion sort */\nvoid insertionSort(int[] nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.length; i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base;        // Assign base to the correct position\n    }\n}\n
insertion_sort.cs
/* Insertion sort */\nvoid InsertionSort(int[] nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < nums.Length; i++) {\n        int bas = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > bas) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = bas;         // Assign base to the correct position\n    }\n}\n
insertion_sort.go
/* Insertion sort */\nfunc insertionSort(nums []int) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i := 1; i < len(nums); i++ {\n        base := nums[i]\n        j := i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        for j >= 0 && nums[j] > base {\n            nums[j+1] = nums[j] // Move nums[j] to the right by one position\n            j--\n        }\n        nums[j+1] = base // Assign base to the correct position\n    }\n}\n
insertion_sort.swift
/* Insertion sort */\nfunc insertionSort(nums: inout [Int]) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i in nums.indices.dropFirst() {\n        let base = nums[i]\n        var j = i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0, nums[j] > base {\n            nums[j + 1] = nums[j] // Move nums[j] to the right by one position\n            j -= 1\n        }\n        nums[j + 1] = base // Assign base to the correct position\n    }\n}\n
insertion_sort.js
/* Insertion sort */\nfunction insertionSort(nums) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        let base = nums[i],\n            j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
insertion_sort.ts
/* Insertion sort */\nfunction insertionSort(nums: number[]): void {\n    // Outer loop: sorted interval is [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        const base = nums[i];\n        let j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n            j--;\n        }\n        nums[j + 1] = base; // Assign base to the correct position\n    }\n}\n
insertion_sort.dart
/* Insertion sort */\nvoid insertionSort(List<int> nums) {\n  // Outer loop: sorted interval is [0, i-1]\n  for (int i = 1; i < nums.length; i++) {\n    int base = nums[i], j = i - 1;\n    // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n    while (j >= 0 && nums[j] > base) {\n      nums[j + 1] = nums[j]; // Move nums[j] to the right by one position\n      j--;\n    }\n    nums[j + 1] = base; // Assign base to the correct position\n  }\n}\n
insertion_sort.rs
/* Insertion sort */\nfn insertion_sort(nums: &mut [i32]) {\n    // Outer loop: sorted interval is [0, i-1]\n    for i in 1..nums.len() {\n        let (base, mut j) = (nums[i], (i - 1) as i32);\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while j >= 0 && nums[j as usize] > base {\n            nums[(j + 1) as usize] = nums[j as usize]; // Move nums[j] to the right by one position\n            j -= 1;\n        }\n        nums[(j + 1) as usize] = base; // Assign base to the correct position\n    }\n}\n
insertion_sort.c
/* Insertion sort */\nvoid insertionSort(int nums[], int size) {\n    // Outer loop: sorted interval is [0, i-1]\n    for (int i = 1; i < size; i++) {\n        int base = nums[i], j = i - 1;\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            // Move nums[j] to the right by one position\n            nums[j + 1] = nums[j];\n            j--;\n        }\n        // Assign base to the correct position\n        nums[j + 1] = base;\n    }\n}\n
insertion_sort.kt
/* Insertion sort */\nfun insertionSort(nums: IntArray) {\n    // Outer loop: sorted elements are 1, 2, ..., n\n    for (i in nums.indices) {\n        val base = nums[i]\n        var j = i - 1\n        // Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j] // Move nums[j] to the right by one position\n            j--\n        }\n        nums[j + 1] = base        // Assign base to the correct position\n    }\n}\n
insertion_sort.rb
### Insertion sort ###\ndef insertion_sort(nums)\n  n = nums.length\n  # Outer loop: sorted interval is [0, i-1]\n  for i in 1...n\n    base = nums[i]\n    j = i - 1\n    # Inner loop: insert base into the correct position within the sorted interval [0, i-1]\n    while j >= 0 && nums[j] > base\n      nums[j + 1] = nums[j] # Move nums[j] to the right by one position\n      j -= 1\n    end\n    nums[j + 1] = base # Assign base to the correct position\n  end\nend\n
","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1142-algorithm-characteristics","level":2,"title":"11.4.2   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n^2)\\), adaptive sorting: In the worst case, each insertion operation requires loops of \\(n - 1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), \\(1\\), summing to \\((n - 1) n / 2\\), so the time complexity is \\(O(n^2)\\). When encountering ordered data, the insertion operation will terminate early. When the input array is completely ordered, insertion sort achieves the best-case time complexity of \\(O(n)\\).
  • Space complexity of \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
  • Stable sorting: During the insertion operation process, we insert elements to the right of equal elements, without changing their order.
","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1143-advantages-of-insertion-sort","level":2,"title":"11.4.3   Advantages of Insertion Sort","text":"

The time complexity of insertion sort is \\(O(n^2)\\), while the time complexity of quick sort, which we will learn about next, is \\(O(n \\log n)\\). Although insertion sort has a higher time complexity, insertion sort is usually faster for smaller data volumes.

This conclusion is similar to the applicable situations of linear search and binary search. Algorithms like quick sort with \\(O(n \\log n)\\) complexity are sorting algorithms based on divide-and-conquer strategy and often contain more unit computation operations. When the data volume is small, \\(n^2\\) and \\(n \\log n\\) are numerically close, and complexity does not dominate; the number of unit operations per round plays a decisive role.

In fact, the built-in sorting functions in many programming languages (such as Java) adopt insertion sort. The general approach is: for long arrays, use sorting algorithms based on divide-and-conquer strategy, such as quick sort; for short arrays, directly use insertion sort.

Although bubble sort, selection sort, and insertion sort all have a time complexity of \\(O(n^2)\\), in actual situations, insertion sort is used significantly more frequently than bubble sort and selection sort, mainly for the following reasons.

  • Bubble sort is based on element swapping, requiring the use of a temporary variable, involving 3 unit operations; insertion sort is based on element assignment, requiring only 1 unit operation. Therefore, the computational overhead of bubble sort is usually higher than that of insertion sort.
  • Selection sort has a time complexity of \\(O(n^2)\\) in any case. If given a set of partially ordered data, insertion sort is usually more efficient than selection sort.
  • Selection sort is unstable and cannot be applied to multi-level sorting.
","path":["Chapter 11. Sorting","11.4   Insertion Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   Merge Sort","text":"

Merge sort (merge sort) is a sorting algorithm based on the divide-and-conquer strategy, which includes the \"divide\" and \"merge\" phases shown in Figure 11-10.

  1. Divide phase: Recursively split the array from the midpoint, transforming the sorting problem of a long array into the sorting problems of shorter arrays.
  2. Merge phase: When the sub-array length is 1, terminate the division and start merging, continuously merging two shorter sorted arrays into one longer sorted array until the process is complete.

Figure 11-10   Divide and merge phases of merge sort

","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161-algorithm-flow","level":2,"title":"11.6.1   Algorithm Flow","text":"

As shown in Figure 11-11, the \"divide phase\" recursively splits the array from the midpoint into two sub-arrays from top to bottom.

  1. Calculate the array midpoint mid, recursively divide the left sub-array (interval [left, mid]) and right sub-array (interval [mid + 1, right]).
  2. Recursively execute step 1. until the sub-array interval length is 1, then terminate.

The \"merge phase\" merges the left sub-array and right sub-array into a sorted array from bottom to top. Note that merging starts from sub-arrays of length 1, and each sub-array in the merge phase is sorted.

<1><2><3><4><5><6><7><8><9><10>

Figure 11-11   Merge sort steps

It can be observed that the recursive order of merge sort is consistent with the post-order traversal of a binary tree.

  • Post-order traversal: First recursively traverse the left subtree, then recursively traverse the right subtree, and finally process the root node.
  • Merge sort: First recursively process the left sub-array, then recursively process the right sub-array, and finally perform the merge.

The implementation of merge sort is shown in the code below. Note that the interval to be merged in nums is [left, right], while the corresponding interval in tmp is [0, right - left].

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby merge_sort.py
def merge(nums: list[int], left: int, mid: int, right: int):\n    \"\"\"Merge left subarray and right subarray\"\"\"\n    # Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    # Create a temporary array tmp to store the merged results\n    tmp = [0] * (right - left + 1)\n    # Initialize the start indices of the left and right subarrays\n    i, j, k = left, mid + 1, 0\n    # While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while i <= mid and j <= right:\n        if nums[i] <= nums[j]:\n            tmp[k] = nums[i]\n            i += 1\n        else:\n            tmp[k] = nums[j]\n            j += 1\n        k += 1\n    # Copy the remaining elements of the left and right subarrays into the temporary array\n    while i <= mid:\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    while j <= right:\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    # Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k in range(0, len(tmp)):\n        nums[left + k] = tmp[k]\n\ndef merge_sort(nums: list[int], left: int, right: int):\n    \"\"\"Merge sort\"\"\"\n    # Termination condition\n    if left >= right:\n        return  # Terminate recursion when subarray length is 1\n    # Divide and conquer stage\n    mid = (left + right) // 2  # Calculate midpoint\n    merge_sort(nums, left, mid)  # Recursively process the left subarray\n    merge_sort(nums, mid + 1, right)  # Recursively process the right subarray\n    # Merge stage\n    merge(nums, left, mid, right)\n
merge_sort.cpp
/* Merge left subarray and right subarray */\nvoid merge(vector<int> &nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    vector<int> tmp(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.size(); k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid mergeSort(vector<int> &nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    mergeSort(nums, left, mid);      // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.java
/* Merge left subarray and right subarray */\nvoid merge(int[] nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int[] tmp = new int[right - left + 1];\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid mergeSort(int[] nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2; // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.cs
/* Merge left subarray and right subarray */\nvoid Merge(int[] nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int[] tmp = new int[right - left + 1];\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.Length; ++k) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nvoid MergeSort(int[] nums, int left, int right) {\n    // Termination condition\n    if (left >= right) return;       // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    MergeSort(nums, left, mid);      // Recursively process the left subarray\n    MergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    Merge(nums, left, mid, right);\n}\n
merge_sort.go
/* Merge left subarray and right subarray */\nfunc merge(nums []int, left, mid, right int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    tmp := make([]int, right-left+1)\n    // Initialize the start indices of the left and right subarrays\n    i, j, k := left, mid+1, 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    for i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i++\n        } else {\n            tmp[k] = nums[j]\n            j++\n        }\n        k++\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    for i <= mid {\n        tmp[k] = nums[i]\n        i++\n        k++\n    }\n    for j <= right {\n        tmp[k] = nums[j]\n        j++\n        k++\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k := 0; k < len(tmp); k++ {\n        nums[left+k] = tmp[k]\n    }\n}\n\n/* Merge sort */\nfunc mergeSort(nums []int, left, right int) {\n    // Termination condition\n    if left >= right {\n        return\n    }\n    // Divide and conquer stage\n    mid := left + (right - left) / 2\n    mergeSort(nums, left, mid)\n    mergeSort(nums, mid+1, right)\n    // Merge stage\n    merge(nums, left, mid, right)\n}\n
merge_sort.swift
/* Merge left subarray and right subarray */\nfunc merge(nums: inout [Int], left: Int, mid: Int, right: Int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    var tmp = Array(repeating: 0, count: right - left + 1)\n    // Initialize the start indices of the left and right subarrays\n    var i = left, j = mid + 1, k = 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while i <= mid, j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i += 1\n        } else {\n            tmp[k] = nums[j]\n            j += 1\n        }\n        k += 1\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while i <= mid {\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    }\n    while j <= right {\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k in tmp.indices {\n        nums[left + k] = tmp[k]\n    }\n}\n\n/* Merge sort */\nfunc mergeSort(nums: inout [Int], left: Int, right: Int) {\n    // Termination condition\n    if left >= right { // Terminate recursion when subarray length is 1\n        return\n    }\n    // Divide and conquer stage\n    let mid = left + (right - left) / 2 // Calculate midpoint\n    mergeSort(nums: &nums, left: left, right: mid) // Recursively process the left subarray\n    mergeSort(nums: &nums, left: mid + 1, right: right) // Recursively process the right subarray\n    // Merge stage\n    merge(nums: &nums, left: left, mid: mid, right: right)\n}\n
merge_sort.js
/* Merge left subarray and right subarray */\nfunction merge(nums, left, mid, right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    const tmp = new Array(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfunction mergeSort(nums, left, right) {\n    // Termination condition\n    if (left >= right) return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.ts
/* Merge left subarray and right subarray */\nfunction merge(nums: number[], left: number, mid: number, right: number): void {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    const tmp = new Array(right - left + 1);\n    // Initialize the start indices of the left and right subarrays\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfunction mergeSort(nums: number[], left: number, right: number): void {\n    // Termination condition\n    if (left >= right) return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint\n    mergeSort(nums, left, mid); // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.dart
/* Merge left subarray and right subarray */\nvoid merge(List<int> nums, int left, int mid, int right) {\n  // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n  // Create a temporary array tmp to store the merged results\n  List<int> tmp = List.filled(right - left + 1, 0);\n  // Initialize the start indices of the left and right subarrays\n  int i = left, j = mid + 1, k = 0;\n  // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n  while (i <= mid && j <= right) {\n    if (nums[i] <= nums[j])\n      tmp[k++] = nums[i++];\n    else\n      tmp[k++] = nums[j++];\n  }\n  // Copy the remaining elements of the left and right subarrays into the temporary array\n  while (i <= mid) {\n    tmp[k++] = nums[i++];\n  }\n  while (j <= right) {\n    tmp[k++] = nums[j++];\n  }\n  // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n  for (k = 0; k < tmp.length; k++) {\n    nums[left + k] = tmp[k];\n  }\n}\n\n/* Merge sort */\nvoid mergeSort(List<int> nums, int left, int right) {\n  // Termination condition\n  if (left >= right) return; // Terminate recursion when subarray length is 1\n  // Divide and conquer stage\n  int mid = left + (right - left) ~/ 2; // Calculate midpoint\n  mergeSort(nums, left, mid); // Recursively process the left subarray\n  mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n  // Merge stage\n  merge(nums, left, mid, right);\n}\n
merge_sort.rs
/* Merge left subarray and right subarray */\nfn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    let tmp_size = right - left + 1;\n    let mut tmp = vec![0; tmp_size];\n    // Initialize the start indices of the left and right subarrays\n    let (mut i, mut j, mut k) = (left, mid + 1, 0);\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i];\n            i += 1;\n        } else {\n            tmp[k] = nums[j];\n            j += 1;\n        }\n        k += 1;\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while i <= mid {\n        tmp[k] = nums[i];\n        k += 1;\n        i += 1;\n    }\n    while j <= right {\n        tmp[k] = nums[j];\n        k += 1;\n        j += 1;\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for k in 0..tmp_size {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* Merge sort */\nfn merge_sort(nums: &mut [i32], left: usize, right: usize) {\n    // Termination condition\n    if left >= right {\n        return; // Terminate recursion when subarray length is 1\n    }\n\n    // Divide and conquer stage\n    let mid = left + (right - left) / 2; // Calculate midpoint\n    merge_sort(nums, left, mid); // Recursively process the left subarray\n    merge_sort(nums, mid + 1, right); // Recursively process the right subarray\n\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.c
/* Merge left subarray and right subarray */\nvoid merge(int *nums, int left, int mid, int right) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    int tmpSize = right - left + 1;\n    int *tmp = (int *)malloc(tmpSize * sizeof(int));\n    // Initialize the start indices of the left and right subarrays\n    int i = left, j = mid + 1, k = 0;\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (k = 0; k < tmpSize; ++k) {\n        nums[left + k] = tmp[k];\n    }\n    // Free memory\n    free(tmp);\n}\n\n/* Merge sort */\nvoid mergeSort(int *nums, int left, int right) {\n    // Termination condition\n    if (left >= right)\n        return; // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    int mid = left + (right - left) / 2;    // Calculate midpoint\n    mergeSort(nums, left, mid);      // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right); // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right);\n}\n
merge_sort.kt
/* Merge left subarray and right subarray */\nfun merge(nums: IntArray, left: Int, mid: Int, right: Int) {\n    // Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n    // Create a temporary array tmp to store the merged results\n    val tmp = IntArray(right - left + 1)\n    // Initialize the start indices of the left and right subarrays\n    var i = left\n    var j = mid + 1\n    var k = 0\n    // While both subarrays still have elements, compare and copy the smaller element into the temporary array\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++]\n        else\n            tmp[k++] = nums[j++]\n    }\n    // Copy the remaining elements of the left and right subarrays into the temporary array\n    while (i <= mid) {\n        tmp[k++] = nums[i++]\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++]\n    }\n    // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n    for (l in tmp.indices) {\n        nums[left + l] = tmp[l]\n    }\n}\n\n/* Merge sort */\nfun mergeSort(nums: IntArray, left: Int, right: Int) {\n    // Termination condition\n    if (left >= right) return  // Terminate recursion when subarray length is 1\n    // Divide and conquer stage\n    val mid = left + (right - left) / 2 // Calculate midpoint\n    mergeSort(nums, left, mid) // Recursively process the left subarray\n    mergeSort(nums, mid + 1, right) // Recursively process the right subarray\n    // Merge stage\n    merge(nums, left, mid, right)\n}\n
merge_sort.rb
### Merge left and right subarrays ###\ndef merge(nums, left, mid, right)\n  # Left subarray interval is [left, mid], right subarray interval is [mid+1, right]\n  # Create temporary array tmp to store merged result\n  tmp = Array.new(right - left + 1, 0)\n  # Initialize the start indices of the left and right subarrays\n  i, j, k = left, mid + 1, 0\n  # While both subarrays still have elements, compare and copy the smaller element into the temporary array\n  while i <= mid && j <= right\n    if nums[i] <= nums[j]\n      tmp[k] = nums[i]\n      i += 1\n    else\n      tmp[k] = nums[j]\n      j += 1\n    end\n    k += 1\n  end\n  # Copy the remaining elements of the left and right subarrays into the temporary array\n  while i <= mid\n    tmp[k] = nums[i]\n    i += 1\n    k += 1\n  end\n  while j <= right\n    tmp[k] = nums[j]\n    j += 1\n    k += 1\n  end\n  # Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval\n  (0...tmp.length).each do |k|\n    nums[left + k] = tmp[k]\n  end\nend\n\n### Merge sort ###\ndef merge_sort(nums, left, right)\n  # Termination condition\n  # Terminate recursion when subarray length is 1\n  return if left >= right\n  # Divide and conquer stage\n  mid = left + (right - left) / 2 # Calculate midpoint\n  merge_sort(nums, left, mid) # Recursively process the left subarray\n  merge_sort(nums, mid + 1, right) # Recursively process the right subarray\n  # Merge stage\n  merge(nums, left, mid, right)\nend\n
","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1162-algorithm-characteristics","level":2,"title":"11.6.2   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n \\log n)\\), non-adaptive sorting: The division produces a recursion tree of height \\(\\log n\\), and the total number of merge operations at each level is \\(n\\), so the overall time complexity is \\(O(n \\log n)\\).
  • Space complexity of \\(O(n)\\), non-in-place sorting: The recursion depth is \\(\\log n\\), using \\(O(\\log n)\\) size of stack frame space. The merge operation requires the aid of an auxiliary array, using \\(O(n)\\) size of additional space.
  • Stable sorting: In the merge process, the order of equal elements remains unchanged.
","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1163-linked-list-sorting","level":2,"title":"11.6.3   Linked List Sorting","text":"

For linked lists, merge sort has significant advantages over other sorting algorithms, and can optimize the space complexity of linked list sorting tasks to \\(O(1)\\).

  • Divide phase: \"Iteration\" can be used instead of \"recursion\" to implement linked list division work, thus saving the stack frame space used by recursion.
  • Merge phase: In linked lists, node insertion and deletion operations can be achieved by just changing references (pointers), so there is no need to create additional linked lists during the merge phase (merging two short ordered linked lists into one long ordered linked list).

The specific implementation details are quite complex, and interested readers can consult related materials for learning.

","path":["Chapter 11. Sorting","11.6   Merge Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   Quick Sort","text":"

Quick sort (quick sort) is a sorting algorithm based on the divide-and-conquer strategy, which operates efficiently and is widely applied.

The core operation of quick sort is \"sentinel partitioning\", which aims to: select a certain element in the array as the \"pivot\", move all elements smaller than the pivot to its left, and move elements larger than the pivot to its right. Specifically, the flow of sentinel partitioning is shown in Figure 11-8.

  1. Select the leftmost element of the array as the pivot, and initialize two pointers i and j pointing to the two ends of the array.
  2. Set up a loop in which i (j) is used in each round to find the first element larger (smaller) than the pivot, and then swap these two elements.
  3. Loop through step 2. until i and j meet, and finally swap the pivot to the boundary line of the two sub-arrays.
<1><2><3><4><5><6><7><8><9>

Figure 11-8   Sentinel partitioning steps

After sentinel partitioning is complete, the original array is divided into three parts: left sub-array, pivot, right sub-array, satisfying \"any element in left sub-array \\(\\leq\\) pivot \\(\\leq\\) any element in right sub-array\". Therefore, we next only need to sort these two sub-arrays.

Divide-and-conquer strategy of quick sort

The essence of sentinel partitioning is to simplify the sorting problem of a longer array into the sorting problems of two shorter arrays.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Sentinel partition\"\"\"\n    # Use nums[left] as the pivot\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Search from right to left for the first element smaller than the pivot\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Search from left to right for the first element greater than the pivot\n        # Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    # Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Return the index of the pivot\n
quick_sort.cpp
/* Sentinel partition */\nint partition(vector<int> &nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Search from left to right for the first element greater than the pivot\n        swap(nums[i], nums[j]); // Swap these two elements\n    }\n    swap(nums[i], nums[left]);  // Swap the pivot to the boundary between the two subarrays\n    return i;                   // Return the index of the pivot\n}\n
quick_sort.java
/* Swap elements */\nvoid swap(int[] nums, int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Sentinel partition */\nint partition(int[] nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
quick_sort.cs
/* Swap elements */\nvoid Swap(int[] nums, int i, int j) {\n    (nums[j], nums[i]) = (nums[i], nums[j]);\n}\n\n/* Sentinel partition */\nint Partition(int[] nums, int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        Swap(nums, i, j); // Swap these two elements\n    }\n    Swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
quick_sort.go
/* Sentinel partition */\nfunc (q *quickSort) partition(nums []int, left, right int) int {\n    // Use nums[left] as the pivot\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Search from right to left for the first element smaller than the pivot\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Return the index of the pivot\n}\n
quick_sort.swift
/* Sentinel partition */\nfunc partition(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while i < j {\n        while i < j, nums[j] >= nums[left] {\n            j -= 1 // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j, nums[i] <= nums[left] {\n            i += 1 // Search from left to right for the first element greater than the pivot\n        }\n        nums.swapAt(i, j) // Swap these two elements\n    }\n    nums.swapAt(i, left) // Swap the pivot to the boundary between the two subarrays\n    return i // Return the index of the pivot\n}\n
quick_sort.js
/* Swap elements */\nswap(nums, i, j) {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Sentinel partition */\npartition(nums, left, right) {\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
quick_sort.ts
/* Swap elements */\nswap(nums: number[], i: number, j: number): void {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Sentinel partition */\npartition(nums: number[], left: number, right: number): number {\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
quick_sort.dart
/* Swap elements */\nvoid _swap(List<int> nums, int i, int j) {\n  int tmp = nums[i];\n  nums[i] = nums[j];\n  nums[j] = tmp;\n}\n\n/* Sentinel partition */\nint _partition(List<int> nums, int left, int right) {\n  // Use nums[left] as the pivot\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n    while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n    _swap(nums, i, j); // Swap these two elements\n  }\n  _swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n  return i; // Return the index of the pivot\n}\n
quick_sort.rs
/* Sentinel partition */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Use nums[left] as the pivot\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        nums.swap(i, j); // Swap these two elements\n    }\n    nums.swap(i, left); // Swap the pivot to the boundary between the two subarrays\n    i // Return the index of the pivot\n}\n
quick_sort.c
/* Swap elements */\nvoid swap(int nums[], int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* Sentinel partition */\nint partition(int nums[], int left, int right) {\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Search from left to right for the first element greater than the pivot\n        }\n        // Swap these two elements\n        swap(nums, i, j);\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    swap(nums, i, left);\n    // Return the index of the pivot\n    return i;\n}\n
quick_sort.kt
/* Swap elements */\nfun swap(nums: IntArray, i: Int, j: Int) {\n    val temp = nums[i]\n    nums[i] = nums[j]\n    nums[j] = temp\n}\n\n/* Sentinel partition */\nfun partition(nums: IntArray, left: Int, right: Int): Int {\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--           // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++           // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j)  // Swap these two elements\n    }\n    swap(nums, i, left)   // Swap the pivot to the boundary between the two subarrays\n    return i              // Return the index of the pivot\n}\n
quick_sort.rb
### Sentinel partition ###\ndef partition(nums, left, right)\n  # Use nums[left] as the pivot\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Search from right to left for the first element smaller than the pivot\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Search from left to right for the first element greater than the pivot\n    end\n    # Swap elements\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Swap the pivot to the boundary between the two subarrays\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Return the index of the pivot\nend\n
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1151-algorithm-flow","level":2,"title":"11.5.1   Algorithm Flow","text":"

The overall flow of quick sort is shown in Figure 11-9.

  1. First, perform one \"sentinel partitioning\" on the original array to obtain the unsorted left sub-array and right sub-array.
  2. Then, recursively perform \"sentinel partitioning\" on the left sub-array and right sub-array respectively.
  3. Continue recursively until the sub-array length is 1, at which point sorting of the entire array is complete.

Figure 11-9   Quick sort flow

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Quick sort\"\"\"\n    # Terminate recursion when subarray length is 1\n    if left >= right:\n        return\n    # Sentinel partition\n    pivot = self.partition(nums, left, right)\n    # Recursively process the left subarray and right subarray\n    self.quick_sort(nums, left, pivot - 1)\n    self.quick_sort(nums, pivot + 1, right)\n
quick_sort.cpp
/* Quick sort */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.java
/* Quick sort */\nvoid quickSort(int[] nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.cs
/* Quick sort */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right)\n        return;\n    // Sentinel partition\n    int pivot = Partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    QuickSort(nums, left, pivot - 1);\n    QuickSort(nums, pivot + 1, right);\n}\n
quick_sort.go
/* Quick sort */\nfunc (q *quickSort) quickSort(nums []int, left, right int) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return\n    }\n    // Sentinel partition\n    pivot := q.partition(nums, left, right)\n    // Recursively process the left subarray and right subarray\n    q.quickSort(nums, left, pivot-1)\n    q.quickSort(nums, pivot+1, right)\n}\n
quick_sort.swift
/* Quick sort */\nfunc quickSort(nums: inout [Int], left: Int, right: Int) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return\n    }\n    // Sentinel partition\n    let pivot = partition(nums: &nums, left: left, right: right)\n    // Recursively process the left subarray and right subarray\n    quickSort(nums: &nums, left: left, right: pivot - 1)\n    quickSort(nums: &nums, left: pivot + 1, right: right)\n}\n
quick_sort.js
/* Quick sort */\nquickSort(nums, left, right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) return;\n    // Sentinel partition\n    const pivot = this.partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
quick_sort.ts
/* Quick sort */\nquickSort(nums: number[], left: number, right: number): void {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) {\n        return;\n    }\n    // Sentinel partition\n    const pivot = this.partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
quick_sort.dart
/* Quick sort */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Terminate recursion when subarray length is 1\n  if (left >= right) return;\n  // Sentinel partition\n  int pivot = _partition(nums, left, right);\n  // Recursively process the left subarray and right subarray\n  quickSort(nums, left, pivot - 1);\n  quickSort(nums, pivot + 1, right);\n}\n
quick_sort.rs
/* Quick sort */\npub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) {\n    // Terminate recursion when subarray length is 1\n    if left >= right {\n        return;\n    }\n    // Sentinel partition\n    let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n    // Recursively process the left subarray and right subarray\n    Self::quick_sort(left, pivot - 1, nums);\n    Self::quick_sort(pivot + 1, right, nums);\n}\n
quick_sort.c
/* Quick sort */\nvoid quickSort(int nums[], int left, int right) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) {\n        return;\n    }\n    // Sentinel partition\n    int pivot = partition(nums, left, right);\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.kt
/* Quick sort */\nfun quickSort(nums: IntArray, left: Int, right: Int) {\n    // Terminate recursion when subarray length is 1\n    if (left >= right) return\n    // Sentinel partition\n    val pivot = partition(nums, left, right)\n    // Recursively process the left subarray and right subarray\n    quickSort(nums, left, pivot - 1)\n    quickSort(nums, pivot + 1, right)\n}\n
quick_sort.rb
### Quick sort class ###\ndef quick_sort(nums, left, right)\n  # Recurse when subarray length is not 1\n  if left < right\n    # Sentinel partition\n    pivot = partition(nums, left, right)\n    # Recursively process the left subarray and right subarray\n    quick_sort(nums, left, pivot - 1)\n    quick_sort(nums, pivot + 1, right)\n  end\n  nums\nend\n
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1152-algorithm-characteristics","level":2,"title":"11.5.2   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n \\log n)\\), non-adaptive sorting: In the average case, the number of recursive levels of sentinel partitioning is \\(\\log n\\), and the total number of loops at each level is \\(n\\), using \\(O(n \\log n)\\) time overall. In the worst case, each round of sentinel partitioning divides an array of length \\(n\\) into two sub-arrays of length \\(0\\) and \\(n - 1\\), at which point the number of recursive levels reaches \\(n\\), the number of loops at each level is \\(n\\), and the total time used is \\(O(n^2)\\).
  • Space complexity of \\(O(n)\\), in-place sorting: In the case where the input array is completely reversed, the worst recursive depth reaches \\(n\\), using \\(O(n)\\) stack frame space. The sorting operation is performed on the original array without the aid of an additional array.
  • Non-stable sorting: In the last step of sentinel partitioning, the pivot may be swapped to the right of equal elements.
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1153-why-is-quick-sort-fast","level":2,"title":"11.5.3   Why Is Quick Sort Fast","text":"

From the name, we can see that quick sort should have certain advantages in terms of efficiency. Although the average time complexity of quick sort is the same as \"merge sort\" and \"heap sort\", quick sort is usually more efficient, mainly for the following reasons.

  • The probability of the worst case occurring is very low: Although the worst-case time complexity of quick sort is \\(O(n^2)\\), which is not as stable as merge sort, in the vast majority of cases, quick sort can run with a time complexity of \\(O(n \\log n)\\).
  • High cache utilization: When performing sentinel partitioning operations, the system can load the entire sub-array into the cache, so element access efficiency is relatively high. Algorithms like \"heap sort\" require jump-style access to elements, thus lacking this characteristic.
  • Small constant coefficient of complexity: Among the three algorithms mentioned above, quick sort has the smallest total number of operations such as comparisons, assignments, and swaps. This is similar to the reason why \"insertion sort\" is faster than \"bubble sort\".
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154-pivot-optimization","level":2,"title":"11.5.4   Pivot Optimization","text":"

Quick sort may have reduced time efficiency for certain inputs. Take an extreme example: suppose the input array is completely reversed. Since we select the leftmost element as the pivot, after sentinel partitioning is complete, the pivot is swapped to the rightmost end of the array, causing the left sub-array length to be \\(n - 1\\) and the right sub-array length to be \\(0\\). If we recurse down like this, each round of sentinel partitioning will have a sub-array length of \\(0\\), the divide-and-conquer strategy fails, and quick sort degrades to a form approximate to \"bubble sort\".

To avoid this situation as much as possible, we can optimize the pivot selection strategy in sentinel partitioning. For example, we can randomly select an element as the pivot. However, if luck is not good and we select a non-ideal pivot every time, efficiency is still not satisfactory.

It should be noted that programming languages usually generate \"pseudo-random numbers\". If we construct a specific test case for a pseudo-random number sequence, the efficiency of quick sort may still degrade.

For further improvement, we can select three candidate elements in the array (usually the first, last, and middle elements of the array), and use the median of these three candidate elements as the pivot. In this way, the probability that the pivot is \"neither too small nor too large\" will be greatly increased. Of course, we can also select more candidate elements to further improve the robustness of the algorithm. After adopting this method, the probability of time complexity degrading to \\(O(n^2)\\) is greatly reduced.

Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n    \"\"\"Select the median of three candidate elements\"\"\"\n    l, m, r = nums[left], nums[mid], nums[right]\n    if (l <= m <= r) or (r <= m <= l):\n        return mid  # m is between l and r\n    if (m <= l <= r) or (r <= l <= m):\n        return left  # l is between m and r\n    return right\n\ndef partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"Sentinel partition (median of three)\"\"\"\n    # Use nums[left] as the pivot\n    med = self.median_three(nums, left, (left + right) // 2, right)\n    # Swap the median to the array's leftmost position\n    nums[left], nums[med] = nums[med], nums[left]\n    # Use nums[left] as the pivot\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # Search from right to left for the first element smaller than the pivot\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # Search from left to right for the first element greater than the pivot\n        # Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    # Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # Return the index of the pivot\n
quick_sort.cpp
/* Select the median of three candidate elements */\nint medianThree(vector<int> &nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partition(vector<int> &nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums[left], nums[med]);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;                // Search from left to right for the first element greater than the pivot\n        swap(nums[i], nums[j]); // Swap these two elements\n    }\n    swap(nums[i], nums[left]);  // Swap the pivot to the boundary between the two subarrays\n    return i;                   // Return the index of the pivot\n}\n
quick_sort.java
/* Select the median of three candidate elements */\nint medianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partition(int[] nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
quick_sort.cs
/* Select the median of three candidate elements */\nint MedianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint Partition(int[] nums, int left, int right) {\n    // Select the median of three candidate elements\n    int med = MedianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    Swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        Swap(nums, i, j); // Swap these two elements\n    }\n    Swap(nums, i, left);  // Swap the pivot to the boundary between the two subarrays\n    return i;             // Return the index of the pivot\n}\n
quick_sort.go
/* Select the median of three candidate elements */\nfunc (q *quickSortMedian) medianThree(nums []int, left, mid, right int) int {\n    l, m, r := nums[left], nums[mid], nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l is between m and r\n    }\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfunc (q *quickSortMedian) partition(nums []int, left, right int) int {\n    // Use nums[left] as the pivot\n    med := q.medianThree(nums, left, (left+right)/2, right)\n    // Swap the median to the array's leftmost position\n    nums[left], nums[med] = nums[med], nums[left]\n    // Use nums[left] as the pivot\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // Search from right to left for the first element smaller than the pivot\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // Search from left to right for the first element greater than the pivot\n        }\n        // Swap elements\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // Swap the pivot to the boundary between the two subarrays\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // Return the index of the pivot\n}\n
quick_sort.swift
/* Select the median of three candidate elements */\nfunc medianThree(nums: [Int], left: Int, mid: Int, right: Int) -> Int {\n    let l = nums[left]\n    let m = nums[mid]\n    let r = nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l is between m and r\n    }\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int {\n    // Select the median of three candidate elements\n    let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right)\n    // Swap the median to the array's leftmost position\n    nums.swapAt(left, med)\n    return partition(nums: &nums, left: left, right: right)\n}\n
quick_sort.js
/* Select the median of three candidate elements */\nmedianThree(nums, left, mid, right) {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m is between l and r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l is between m and r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Sentinel partition (median of three) */\npartition(nums, left, right) {\n    // Select the median of three candidate elements\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Swap the median to the array's leftmost position\n    this.swap(nums, left, med);\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
quick_sort.ts
/* Select the median of three candidate elements */\nmedianThree(\n    nums: number[],\n    left: number,\n    mid: number,\n    right: number\n): number {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m is between l and r\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l is between m and r\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* Sentinel partition (median of three) */\npartition(nums: number[], left: number, right: number): number {\n    // Select the median of three candidate elements\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // Swap the median to the array's leftmost position\n    this.swap(nums, left, med);\n    // Use nums[left] as the pivot\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // Search from right to left for the first element smaller than the pivot\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // Search from left to right for the first element greater than the pivot\n        }\n        this.swap(nums, i, j); // Swap these two elements\n    }\n    this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i; // Return the index of the pivot\n}\n
quick_sort.dart
/* Select the median of three candidate elements */\nint _medianThree(List<int> nums, int left, int mid, int right) {\n  int l = nums[left], m = nums[mid], r = nums[right];\n  if ((l <= m && m <= r) || (r <= m && m <= l))\n    return mid; // m is between l and r\n  if ((m <= l && l <= r) || (r <= l && l <= m))\n    return left; // l is between m and r\n  return right;\n}\n\n/* Sentinel partition (median of three) */\nint _partition(List<int> nums, int left, int right) {\n  // Select the median of three candidate elements\n  int med = _medianThree(nums, left, (left + right) ~/ 2, right);\n  // Swap the median to the array's leftmost position\n  _swap(nums, left, med);\n  // Use nums[left] as the pivot\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot\n    while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot\n    _swap(nums, i, j); // Swap these two elements\n  }\n  _swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n  return i; // Return the index of the pivot\n}\n
quick_sort.rs
/* Select the median of three candidate elements */\nfn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize {\n    let (l, m, r) = (nums[left], nums[mid], nums[right]);\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid; // m is between l and r\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left; // l is between m and r\n    }\n    right\n}\n\n/* Sentinel partition (median of three) */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // Select the median of three candidate elements\n    let med = Self::median_three(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    nums.swap(left, med);\n    // Use nums[left] as the pivot\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // Search from right to left for the first element smaller than the pivot\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // Search from left to right for the first element greater than the pivot\n        }\n        nums.swap(i, j); // Swap these two elements\n    }\n    nums.swap(i, left); // Swap the pivot to the boundary between the two subarrays\n    i // Return the index of the pivot\n}\n
quick_sort.c
/* Select the median of three candidate elements */\nint medianThree(int nums[], int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m is between l and r\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l is between m and r\n    return right;\n}\n\n/* Sentinel partition (median of three) */\nint partitionMedian(int nums[], int left, int right) {\n    // Select the median of three candidate elements\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med);\n    // Use nums[left] as the pivot\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--; // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++;          // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j); // Swap these two elements\n    }\n    swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays\n    return i;            // Return the index of the pivot\n}\n
quick_sort.kt
/* Select the median of three candidate elements */\nfun medianThree(nums: IntArray, left: Int, mid: Int, right: Int): Int {\n    val l = nums[left]\n    val m = nums[mid]\n    val r = nums[right]\n    if ((m in l..r) || (m in r..l))\n        return mid  // m is between l and r\n    if ((l in m..r) || (l in r..m))\n        return left // l is between m and r\n    return right\n}\n\n/* Sentinel partition (median of three) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n    // Select the median of three candidate elements\n    val med = medianThree(nums, left, (left + right) / 2, right)\n    // Swap the median to the array's leftmost position\n    swap(nums, left, med)\n    // Use nums[left] as the pivot\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--                      // Search from right to left for the first element smaller than the pivot\n        while (i < j && nums[i] <= nums[left])\n            i++                      // Search from left to right for the first element greater than the pivot\n        swap(nums, i, j)             // Swap these two elements\n    }\n    swap(nums, i, left)              // Swap the pivot to the boundary between the two subarrays\n    return i                         // Return the index of the pivot\n}\n
quick_sort.rb
### Select median of three candidate elements ###\ndef median_three(nums, left, mid, right)\n  # Select the median of three candidate elements\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m is between l and r\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l is between m and r\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n### Sentinel partition (median of three) ###\ndef partition(nums, left, right)\n  ### Use nums[left] as pivot\n  med = median_three(nums, left, (left + right) / 2, right)\n  # Swap median to leftmost position of array\n  nums[left], nums[med] = nums[med], nums[left]\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # Search from right to left for the first element smaller than the pivot\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # Search from left to right for the first element greater than the pivot\n    end\n    # Swap elements\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # Swap the pivot to the boundary between the two subarrays\n  nums[i], nums[left] = nums[left], nums[i]\n  i # Return the index of the pivot\nend\n
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1155-recursive-depth-optimization","level":2,"title":"11.5.5   Recursive Depth Optimization","text":"

For certain inputs, quick sort may occupy more space. Taking a completely ordered input array as an example, let the length of the sub-array in recursion be \\(m\\). Each round of sentinel partitioning will produce a left sub-array of length \\(0\\) and a right sub-array of length \\(m - 1\\), which means that the problem scale reduced per recursive call is very small (only one element is reduced), and the height of the recursion tree will reach \\(n - 1\\), at which point \\(O(n)\\) size of stack frame space is required.

To prevent the accumulation of stack frame space, we can compare the lengths of the two sub-arrays after each round of sentinel sorting is complete, and only recurse on the shorter sub-array. Since the length of the shorter sub-array will not exceed \\(n / 2\\), this method can ensure that the recursion depth does not exceed \\(\\log n\\), thus optimizing the worst-case space complexity to \\(O(\\log n)\\). The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"Quick sort (recursion depth optimization)\"\"\"\n    # Terminate when subarray length is 1\n    while left < right:\n        # Sentinel partition operation\n        pivot = self.partition(nums, left, right)\n        # Perform quick sort on the shorter of the two subarrays\n        if pivot - left < right - pivot:\n            self.quick_sort(nums, left, pivot - 1)  # Recursively sort the left subarray\n            left = pivot + 1  # Remaining unsorted interval is [pivot + 1, right]\n        else:\n            self.quick_sort(nums, pivot + 1, right)  # Recursively sort the right subarray\n            right = pivot - 1  # Remaining unsorted interval is [left, pivot - 1]\n
quick_sort.cpp
/* Quick sort (recursion depth optimization) */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1;                 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1;                 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.java
/* Quick sort (recursion depth optimization) */\nvoid quickSort(int[] nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.cs
/* Quick sort (recursion depth optimization) */\nvoid QuickSort(int[] nums, int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = Partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            QuickSort(nums, left, pivot - 1);  // Recursively sort the left subarray\n            left = pivot + 1;  // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            QuickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.go
/* Quick sort (recursion depth optimization) */\nfunc (q *quickSortTailCall) quickSort(nums []int, left, right int) {\n    // Terminate when subarray length is 1\n    for left < right {\n        // Sentinel partition operation\n        pivot := q.partition(nums, left, right)\n        // Perform quick sort on the shorter of the two subarrays\n        if pivot-left < right-pivot {\n            q.quickSort(nums, left, pivot-1) // Recursively sort the left subarray\n            left = pivot + 1                 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            q.quickSort(nums, pivot+1, right) // Recursively sort the right subarray\n            right = pivot - 1                 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.swift
/* Quick sort (recursion depth optimization) */\nfunc quickSortTailCall(nums: inout [Int], left: Int, right: Int) {\n    var left = left\n    var right = right\n    // Terminate when subarray length is 1\n    while left < right {\n        // Sentinel partition operation\n        let pivot = partition(nums: &nums, left: left, right: right)\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left) < (right - pivot) {\n            quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // Recursively sort the left subarray\n            left = pivot + 1 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // Recursively sort the right subarray\n            right = pivot - 1 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.js
/* Quick sort (recursion depth optimization) */\nquickSort(nums, left, right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        let pivot = this.partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.ts
/* Quick sort (recursion depth optimization) */\nquickSort(nums: number[], left: number, right: number): void {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        let pivot = this.partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.dart
/* Quick sort (recursion depth optimization) */\nvoid quickSort(List<int> nums, int left, int right) {\n  // Terminate when subarray length is 1\n  while (left < right) {\n    // Sentinel partition operation\n    int pivot = _partition(nums, left, right);\n    // Perform quick sort on the shorter of the two subarrays\n    if (pivot - left < right - pivot) {\n      quickSort(nums, left, pivot - 1); // Recursively sort the left subarray\n      left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n    } else {\n      quickSort(nums, pivot + 1, right); // Recursively sort the right subarray\n      right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n    }\n  }\n}\n
quick_sort.rs
/* Quick sort (recursion depth optimization) */\npub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {\n    // Terminate when subarray length is 1\n    while left < right {\n        // Sentinel partition operation\n        let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n        // Perform quick sort on the shorter of the two subarrays\n        if pivot - left < right - pivot {\n            Self::quick_sort(left, pivot - 1, nums); // Recursively sort the left subarray\n            left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            Self::quick_sort(pivot + 1, right, nums); // Recursively sort the right subarray\n            right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.c
/* Quick sort (recursion depth optimization) */\nvoid quickSortTailCall(int nums[], int left, int right) {\n    // Terminate when subarray length is 1\n    while (left < right) {\n        // Sentinel partition operation\n        int pivot = partition(nums, left, right);\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - left < right - pivot) {\n            // Recursively sort the left subarray\n            quickSortTailCall(nums, left, pivot - 1);\n            // Remaining unsorted interval is [pivot + 1, right]\n            left = pivot + 1;\n        } else {\n            // Recursively sort the right subarray\n            quickSortTailCall(nums, pivot + 1, right);\n            // Remaining unsorted interval is [left, pivot - 1]\n            right = pivot - 1;\n        }\n    }\n}\n
quick_sort.kt
/* Quick sort (recursion depth optimization) */\nfun quickSortTailCall(nums: IntArray, left: Int, right: Int) {\n    // Terminate when subarray length is 1\n    var l = left\n    var r = right\n    while (l < r) {\n        // Sentinel partition operation\n        val pivot = partition(nums, l, r)\n        // Perform quick sort on the shorter of the two subarrays\n        if (pivot - l < r - pivot) {\n            quickSort(nums, l, pivot - 1) // Recursively sort the left subarray\n            l = pivot + 1 // Remaining unsorted interval is [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, r) // Recursively sort the right subarray\n            r = pivot - 1 // Remaining unsorted interval is [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.rb
### Quick sort (recursion depth optimization) ###\ndef quick_sort(nums, left, right)\n  # Recurse when subarray length is not 1\n  while left < right\n    # Sentinel partition\n    pivot = partition(nums, left, right)\n    # Perform quick sort on the shorter of the two subarrays\n    if pivot - left < right - pivot\n      quick_sort(nums, left, pivot - 1)\n      left = pivot + 1 # Remaining unsorted interval is [pivot + 1, right]\n    else\n      quick_sort(nums, pivot + 1, right)\n      right = pivot - 1 # Remaining unsorted interval is [left, pivot - 1]\n    end\n  end\nend\n
","path":["Chapter 11. Sorting","11.5   Quick Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/","level":1,"title":"11.10   Radix Sort","text":"

The previous section introduced counting sort, which is suitable for situations where the data volume \\(n\\) is large but the data range \\(m\\) is small. Suppose we need to sort \\(n = 10^6\\) student IDs, and the student ID is an 8-digit number, which means the data range \\(m = 10^8\\) is very large. Using counting sort would require allocating a large amount of memory space, whereas radix sort can avoid this situation.

Radix sort (radix sort) has a core idea consistent with counting sort, which also achieves sorting by counting quantities. Building on this, radix sort utilizes the progressive relationship between the digits of numbers, sorting each digit in turn to obtain the final sorting result.

","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11101-algorithm-flow","level":2,"title":"11.10.1   Algorithm Flow","text":"

Taking student ID data as an example, assume the lowest digit is the \\(1\\)st digit and the highest digit is the \\(8\\)th digit. The flow of radix sort is shown in Figure 11-18.

  1. Initialize the digit \\(k = 1\\).
  2. Perform \"counting sort\" on the \\(k\\)th digit of the student IDs. After completion, the data will be sorted from smallest to largest according to the \\(k\\)th digit.
  3. Increase \\(k\\) by \\(1\\), then return to step 2. and continue iterating until all digits are sorted, at which point the process ends.

Figure 11-18   Radix sort algorithm flow

Below we analyze the code implementation. For a \\(d\\)-base number \\(x\\), to get its \\(k\\)th digit \\(x_k\\), the following calculation formula can be used:

\\[ x_k = \\lfloor\\frac{x}{d^{k-1}}\\rfloor \\bmod d \\]

Where \\(\\lfloor a \\rfloor\\) denotes rounding down the floating-point number \\(a\\), and \\(\\bmod \\: d\\) denotes taking the modulo (remainder) with respect to \\(d\\). For student ID data, \\(d = 10\\) and \\(k \\in [1, 8]\\).

Additionally, we need to slightly modify the counting sort code to make it sort based on the \\(k\\)th digit of the number:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby radix_sort.py
def digit(num: int, exp: int) -> int:\n    \"\"\"Get the k-th digit of element num, where exp = 10^(k-1)\"\"\"\n    # Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"Counting sort (based on nums k-th digit)\"\"\"\n    # Decimal digit range is 0~9, therefore need a bucket array of length 10\n    counter = [0] * 10\n    n = len(nums)\n    # Count the occurrence of digits 0~9\n    for i in range(n):\n        d = digit(nums[i], exp)  # Get the k-th digit of nums[i], noted as d\n        counter[d] += 1  # Count the occurrence of digit d\n    # Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in range(1, 10):\n        counter[i] += counter[i - 1]\n    # Traverse in reverse, based on bucket statistics, place each element into res\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        d = digit(nums[i], exp)\n        j = counter[d] - 1  # Get the index j for d in the array\n        res[j] = nums[i]  # Place the current element at index j\n        counter[d] -= 1  # Decrease the count of d by 1\n    # Use result to overwrite the original array nums\n    for i in range(n):\n        nums[i] = res[i]\n\ndef radix_sort(nums: list[int]):\n    \"\"\"Radix sort\"\"\"\n    # Get the maximum element of the array, used to determine the maximum number of digits\n    m = max(nums)\n    # Traverse from the lowest to the highest digit\n    exp = 1\n    while exp <= m:\n        # Perform counting sort on the k-th digit of array elements\n        # k = 1 -> exp = 1\n        # k = 2 -> exp = 10\n        # i.e., exp = 10^(k-1)\n        counting_sort_digit(nums, exp)\n        exp *= 10\n
radix_sort.cpp
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    vector<int> counter(10, 0);\n    int n = nums.size();\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    vector<int> res(n, 0);\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(vector<int> &nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = *max_element(nums.begin(), nums.end());\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10)\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n}\n
radix_sort.java
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(int[] nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int[] counter = new int[10];\n    int n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(int[] nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = Integer.MIN_VALUE;\n    for (int num : nums)\n        if (num > m)\n            m = num;\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.cs
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nint Digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid CountingSortDigit(int[] nums, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int[] counter = new int[10];\n    int n = nums.Length;\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < n; i++) {\n        int d = Digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++;                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = Digit(nums[i], exp);\n        int j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nvoid RadixSort(int[] nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int m = int.MinValue;\n    foreach (int num in nums) {\n        if (num > m) m = num;\n    }\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        CountingSortDigit(nums, exp);\n    }\n}\n
radix_sort.go
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunc digit(num, exp int) int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunc countingSortDigit(nums []int, exp int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    counter := make([]int, 10)\n    n := len(nums)\n    // Count the occurrence of digits 0~9\n    for i := 0; i < n; i++ {\n        d := digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d\n        counter[d]++             // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i := 1; i < 10; i++ {\n        counter[i] += counter[i-1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        d := digit(nums[i], exp)\n        j := counter[d] - 1 // Get the index j for d in the array\n        res[j] = nums[i]    // Place the current element at index j\n        counter[d]--        // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for i := 0; i < n; i++ {\n        nums[i] = res[i]\n    }\n}\n\n/* Radix sort */\nfunc radixSort(nums []int) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    max := math.MinInt\n    for _, num := range nums {\n        if num > max {\n            max = num\n        }\n    }\n    // Traverse from the lowest to the highest digit\n    for exp := 1; max >= exp; exp *= 10 {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n    }\n}\n
radix_sort.swift
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    var counter = Array(repeating: 0, count: 10)\n    // Count the occurrence of digits 0~9\n    for i in nums.indices {\n        let d = digit(num: nums[i], exp: exp) // Get the k-th digit of nums[i], noted as d\n        counter[d] += 1 // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in 1 ..< 10 {\n        counter[i] += counter[i - 1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let d = digit(num: nums[i], exp: exp)\n        let j = counter[d] - 1 // Get the index j for d in the array\n        res[j] = nums[i] // Place the current element at index j\n        counter[d] -= 1 // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n\n/* Radix sort */\nfunc radixSort(nums: inout [Int]) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    var m = Int.min\n    for num in nums {\n        if num > m {\n            m = num\n        }\n    }\n    // Traverse from the lowest to the highest digit\n    for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums: &nums, exp: exp)\n    }\n}\n
radix_sort.js
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunction digit(num, exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return Math.floor(num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunction countingSortDigit(nums, exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d]--; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nfunction radixSort(nums) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m = Math.max(... nums);\n    // Traverse from the lowest to the highest digit\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.ts
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfunction digit(num: number, exp: number): number {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return Math.floor(num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // Count the occurrence of digits 0~9\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d]++; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d]--; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* Radix sort */\nfunction radixSort(nums: number[]): void {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m: number = Math.max(... nums);\n    // Traverse from the lowest to the highest digit\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.dart
/* Get k-th digit of element _num, where exp = 10^(k-1) */\nint digit(int _num, int exp) {\n  // Passing exp instead of k can avoid repeated expensive exponentiation here\n  return (_num ~/ exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(List<int> nums, int exp) {\n  // Decimal digit range is 0~9, therefore need a bucket array of length 10\n  List<int> counter = List<int>.filled(10, 0);\n  int n = nums.length;\n  // Count the occurrence of digits 0~9\n  for (int i = 0; i < n; i++) {\n    int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n    counter[d]++; // Count the occurrence of digit d\n  }\n  // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n  for (int i = 1; i < 10; i++) {\n    counter[i] += counter[i - 1];\n  }\n  // Traverse in reverse, based on bucket statistics, place each element into res\n  List<int> res = List<int>.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int d = digit(nums[i], exp);\n    int j = counter[d] - 1; // Get the index j for d in the array\n    res[j] = nums[i]; // Place the current element at index j\n    counter[d]--; // Decrease the count of d by 1\n  }\n  // Use result to overwrite the original array nums\n  for (int i = 0; i < n; i++) nums[i] = res[i];\n}\n\n/* Radix sort */\nvoid radixSort(List<int> nums) {\n  // Get the maximum element of the array, used to determine the maximum number of digits\n  // In Dart, int length is 64 bits\n  int m = -1 << 63;\n  for (int _num in nums) if (_num > m) m = _num;\n  // Traverse from the lowest to the highest digit\n  for (int exp = 1; exp <= m; exp *= 10)\n    // Perform counting sort on the k-th digit of array elements\n    // k = 1 -> exp = 1\n    // k = 2 -> exp = 10\n    // i.e., exp = 10^(k-1)\n    countingSortDigit(nums, exp);\n}\n
radix_sort.rs
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfn digit(num: i32, exp: i32) -> usize {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return ((num / exp) % 10) as usize;\n}\n\n/* Counting sort (based on nums k-th digit) */\nfn counting_sort_digit(nums: &mut [i32], exp: i32) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    let mut counter = [0; 10];\n    let n = nums.len();\n    // Count the occurrence of digits 0~9\n    for i in 0..n {\n        let d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d\n        counter[d] += 1; // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for i in 1..10 {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let d = digit(nums[i], exp);\n        let j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i]; // Place the current element at index j\n        counter[d] -= 1; // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    nums.copy_from_slice(&res);\n}\n\n/* Radix sort */\nfn radix_sort(nums: &mut [i32]) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    let m = *nums.into_iter().max().unwrap();\n    // Traverse from the lowest to the highest digit\n    let mut exp = 1;\n    while exp <= m {\n        counting_sort_digit(nums, exp);\n        exp *= 10;\n    }\n}\n
radix_sort.c
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10;\n}\n\n/* Counting sort (based on nums k-th digit) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    int *counter = (int *)malloc((sizeof(int) * 10));\n    memset(counter, 0, sizeof(int) * 10); // Initialize to 0 to support subsequent memory release\n    // Count the occurrence of digits 0~9\n    for (int i = 0; i < size; i++) {\n        // Get the k-th digit of nums[i], noted as d\n        int d = digit(nums[i], exp);\n        // Count the occurrence of digit d\n        counter[d]++;\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    int *res = (int *)malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // Get the index j for d in the array\n        res[j] = nums[i];       // Place the current element at index j\n        counter[d]--;           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (int i = 0; i < size; i++) {\n        nums[i] = res[i];\n    }\n    // Free memory\n    free(res);\n    free(counter);\n}\n\n/* Radix sort */\nvoid radixSort(int nums[], int size) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    int max = INT32_MIN;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > max) {\n            max = nums[i];\n        }\n    }\n    // Traverse from the lowest to the highest digit\n    for (int exp = 1; max >= exp; exp *= 10)\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, size, exp);\n}\n
radix_sort.kt
/* Get the k-th digit of element num, where exp = 10^(k-1) */\nfun digit(num: Int, exp: Int): Int {\n    // Passing exp instead of k can avoid repeated expensive exponentiation here\n    return (num / exp) % 10\n}\n\n/* Counting sort (based on nums k-th digit) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n    // Decimal digit range is 0~9, therefore need a bucket array of length 10\n    val counter = IntArray(10)\n    val n = nums.size\n    // Count the occurrence of digits 0~9\n    for (i in 0..<n) {\n        val d = digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d\n        counter[d]++                // Count the occurrence of digit d\n    }\n    // Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n    for (i in 1..9) {\n        counter[i] += counter[i - 1]\n    }\n    // Traverse in reverse, based on bucket statistics, place each element into res\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val d = digit(nums[i], exp)\n        val j = counter[d] - 1 // Get the index j for d in the array\n        res[j] = nums[i]       // Place the current element at index j\n        counter[d]--           // Decrease the count of d by 1\n    }\n    // Use result to overwrite the original array nums\n    for (i in 0..<n)\n        nums[i] = res[i]\n}\n\n/* Radix sort */\nfun radixSort(nums: IntArray) {\n    // Get the maximum element of the array, used to determine the maximum number of digits\n    var m = Int.MIN_VALUE\n    for (num in nums) if (num > m) m = num\n    var exp = 1\n    // Traverse from the lowest to the highest digit\n    while (exp <= m) {\n        // Perform counting sort on the k-th digit of array elements\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // i.e., exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n        exp *= 10\n    }\n}\n
radix_sort.rb
### Get k-th digit of element num, where exp = 10^(k-1) ###\ndef digit(num, exp)\n  # Passing exp instead of k avoids expensive exponentiation calculations\n  (num / exp) % 10\nend\n\n### Counting sort (sort by k-th digit of nums) ###\ndef counting_sort_digit(nums, exp)\n  # Decimal digit range is 0~9, therefore need a bucket array of length 10\n  counter = Array.new(10, 0)\n  n = nums.length\n  # Count the occurrence of digits 0~9\n  for i in 0...n\n    d = digit(nums[i], exp) # Get the k-th digit of nums[i], noted as d\n    counter[d] += 1 # Count the occurrence of digit d\n  end\n  # Calculate prefix sum, converting \"occurrence count\" into \"array index\"\n  (1...10).each { |i| counter[i] += counter[i - 1] }\n  # Traverse in reverse, based on bucket statistics, place each element into res\n  res = Array.new(n, 0)\n  for i in (n - 1).downto(0)\n    d = digit(nums[i], exp)\n    j = counter[d] - 1 # Get the index j for d in the array\n    res[j] = nums[i] # Place the current element at index j\n    counter[d] -= 1 # Decrease the count of d by 1\n  end\n  # Use result to overwrite the original array nums\n  (0...n).each { |i| nums[i] = res[i] }\nend\n\n### Radix sort ###\ndef radix_sort(nums)\n  # Get the maximum element of the array, used to determine the maximum number of digits\n  m = nums.max\n  # Traverse from the lowest to the highest digit\n  exp = 1\n  while exp <= m\n    # Perform counting sort on the k-th digit of array elements\n    # k = 1 -> exp = 1\n    # k = 2 -> exp = 10\n    # i.e., exp = 10^(k-1)\n    counting_sort_digit(nums, exp)\n    exp *= 10\n  end\nend\n

Why start sorting from the lowest digit?

In successive sorting rounds, the result of a later round will override the result of an earlier round. For example, if the first round result is \\(a < b\\), while the second round result is \\(a > b\\), then the second round's result will replace the first round's result. Since higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then sort the higher digits.

","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11102-algorithm-characteristics","level":2,"title":"11.10.2   Algorithm Characteristics","text":"

Compared to counting sort, radix sort is suitable for larger numerical ranges, but the prerequisite is that the data must be representable in a fixed number of digits, and the number of digits should not be too large. For example, floating-point numbers are not suitable for radix sort because their number of digits \\(k\\) may be too large, potentially leading to time complexity \\(O(nk) \\gg O(n^2)\\).

  • Time complexity of \\(O(nk)\\), non-adaptive sorting: Let the data volume be \\(n\\), the data be in base \\(d\\), and the maximum number of digits be \\(k\\). Then performing counting sort on a certain digit uses \\(O(n + d)\\) time, and sorting all \\(k\\) digits uses \\(O((n + d)k)\\) time. Typically, both \\(d\\) and \\(k\\) are relatively small, and the time complexity approaches \\(O(n)\\).
  • Space complexity of \\(O(n + d)\\), non-in-place sorting: Same as counting sort, radix sort requires auxiliary arrays res and counter of lengths \\(n\\) and \\(d\\).
  • Stable sorting: When counting sort is stable, radix sort is also stable; when counting sort is unstable, radix sort cannot guarantee obtaining correct sorting results.
","path":["Chapter 11. Sorting","11.10   Radix Sort"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   Selection Sort","text":"

Selection sort (selection sort) works very simply: it opens a loop, and in each round, selects the smallest element from the unsorted interval and places it at the end of the sorted interval.

Assume the array has length \\(n\\). The algorithm flow of selection sort is shown in Figure 11-2.

  1. Initially, all elements are unsorted, i.e., the unsorted (index) interval is \\([0, n-1]\\).
  2. Select the smallest element in the interval \\([0, n-1]\\) and swap it with the element at index \\(0\\). After completion, the first element of the array is sorted.
  3. Select the smallest element in the interval \\([1, n-1]\\) and swap it with the element at index \\(1\\). After completion, the first 2 elements of the array are sorted.
  4. And so on. After \\(n - 1\\) rounds of selection and swapping, the first \\(n - 1\\) elements of the array are sorted.
  5. The only remaining element must be the largest element, requiring no sorting, so the array sorting is complete.
<1><2><3><4><5><6><7><8><9><10><11>

Figure 11-2   Selection sort steps

In the code, we use \\(k\\) to record the smallest element within the unsorted interval:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby selection_sort.py
def selection_sort(nums: list[int]):\n    \"\"\"Selection sort\"\"\"\n    n = len(nums)\n    # Outer loop: unsorted interval is [i, n-1]\n    for i in range(n - 1):\n        # Inner loop: find the smallest element within the unsorted interval\n        k = i\n        for j in range(i + 1, n):\n            if nums[j] < nums[k]:\n                k = j  # Record the index of the smallest element\n        # Swap the smallest element with the first element of the unsorted interval\n        nums[i], nums[k] = nums[k], nums[i]\n
selection_sort.cpp
/* Selection sort */\nvoid selectionSort(vector<int> &nums) {\n    int n = nums.size();\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        swap(nums[i], nums[k]);\n    }\n}\n
selection_sort.java
/* Selection sort */\nvoid selectionSort(int[] nums) {\n    int n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
selection_sort.cs
/* Selection sort */\nvoid SelectionSort(int[] nums) {\n    int n = nums.Length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        (nums[k], nums[i]) = (nums[i], nums[k]);\n    }\n}\n
selection_sort.go
/* Selection sort */\nfunc selectionSort(nums []int) {\n    n := len(nums)\n    // Outer loop: unsorted interval is [i, n-1]\n    for i := 0; i < n-1; i++ {\n        // Inner loop: find the smallest element within the unsorted interval\n        k := i\n        for j := i + 1; j < n; j++ {\n            if nums[j] < nums[k] {\n                // Record the index of the smallest element\n                k = j\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums[i], nums[k] = nums[k], nums[i]\n\n    }\n}\n
selection_sort.swift
/* Selection sort */\nfunc selectionSort(nums: inout [Int]) {\n    // Outer loop: unsorted interval is [i, n-1]\n    for i in nums.indices.dropLast() {\n        // Inner loop: find the smallest element within the unsorted interval\n        var k = i\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[j] < nums[k] {\n                k = j // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums.swapAt(i, k)\n    }\n}\n
selection_sort.js
/* Selection sort */\nfunction selectionSort(nums) {\n    let n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
selection_sort.ts
/* Selection sort */\nfunction selectionSort(nums: number[]): void {\n    let n = nums.length;\n    // Outer loop: unsorted interval is [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
selection_sort.dart
/* Selection sort */\nvoid selectionSort(List<int> nums) {\n  int n = nums.length;\n  // Outer loop: unsorted interval is [i, n-1]\n  for (int i = 0; i < n - 1; i++) {\n    // Inner loop: find the smallest element within the unsorted interval\n    int k = i;\n    for (int j = i + 1; j < n; j++) {\n      if (nums[j] < nums[k]) k = j; // Record the index of the smallest element\n    }\n    // Swap the smallest element with the first element of the unsorted interval\n    int temp = nums[i];\n    nums[i] = nums[k];\n    nums[k] = temp;\n  }\n}\n
selection_sort.rs
/* Selection sort */\nfn selection_sort(nums: &mut [i32]) {\n    if nums.is_empty() {\n        return;\n    }\n    let n = nums.len();\n    // Outer loop: unsorted interval is [i, n-1]\n    for i in 0..n - 1 {\n        // Inner loop: find the smallest element within the unsorted interval\n        let mut k = i;\n        for j in i + 1..n {\n            if nums[j] < nums[k] {\n                k = j; // Record the index of the smallest element\n            }\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        nums.swap(i, k);\n    }\n}\n
selection_sort.c
/* Selection sort */\nvoid selectionSort(int nums[], int n) {\n    // Outer loop: unsorted interval is [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // Inner loop: find the smallest element within the unsorted interval\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
selection_sort.kt
/* Selection sort */\nfun selectionSort(nums: IntArray) {\n    val n = nums.size\n    // Outer loop: unsorted interval is [i, n-1]\n    for (i in 0..<n - 1) {\n        var k = i\n        // Inner loop: find the smallest element within the unsorted interval\n        for (j in i + 1..<n) {\n            if (nums[j] < nums[k])\n                k = j // Record the index of the smallest element\n        }\n        // Swap the smallest element with the first element of the unsorted interval\n        val temp = nums[i]\n        nums[i] = nums[k]\n        nums[k] = temp\n    }\n}\n
selection_sort.rb
### Selection sort ###\ndef selection_sort(nums)\n  n = nums.length\n  # Outer loop: unsorted interval is [i, n-1]\n  for i in 0...(n - 1)\n    # Inner loop: find the smallest element within the unsorted interval\n    k = i\n    for j in (i + 1)...n\n      if nums[j] < nums[k]\n        k = j # Record the index of the smallest element\n      end\n    end\n    # Swap the smallest element with the first element of the unsorted interval\n    nums[i], nums[k] = nums[k], nums[i]\n  end\nend\n
","path":["Chapter 11. Sorting","11.2   Selection Sort"],"tags":[]},{"location":"chapter_sorting/selection_sort/#1121-algorithm-characteristics","level":2,"title":"11.2.1   Algorithm Characteristics","text":"
  • Time complexity of \\(O(n^2)\\), non-adaptive sorting: The outer loop has \\(n - 1\\) rounds in total. The length of the unsorted interval in the first round is \\(n\\), and the length of the unsorted interval in the last round is \\(2\\). That is, each round of the outer loop contains \\(n\\), \\(n - 1\\), \\(\\dots\\), \\(3\\), \\(2\\) inner loop iterations, summing to \\(\\frac{(n - 1)(n + 2)}{2}\\).
  • Space complexity of \\(O(1)\\), in-place sorting: Pointers \\(i\\) and \\(j\\) use a constant amount of extra space.
  • Non-stable sorting: As shown in Figure 11-3, element nums[i] may be swapped to the right of an element equal to it, causing a change in their relative order.

Figure 11-3   Selection sort non-stability example

","path":["Chapter 11. Sorting","11.2   Selection Sort"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   Sorting Algorithm","text":"

Sorting algorithm (sorting algorithm) is used to arrange a group of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently.

As shown in Figure 11-1, data types in sorting algorithms can be integers, floating-point numbers, characters, or strings, etc. The sorting criterion can be set according to requirements, such as numerical size, character ASCII code order, or custom rules.

Figure 11-1   Data type and criterion examples

","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111-evaluation-dimensions","level":2,"title":"11.1.1   Evaluation Dimensions","text":"

Execution efficiency: We expect the time complexity of sorting algorithms to be as low as possible, with a smaller total number of operations (reducing the constant factor in time complexity). For large data volumes, execution efficiency is particularly important.

In-place property: As the name implies, in-place sorting achieves sorting by operating directly on the original array without requiring additional auxiliary arrays, thus saving memory. Typically, in-place sorting involves fewer data movement operations and runs faster.

Stability: Stable sorting ensures that the relative order of equal elements in the array does not change after sorting is completed.

Stable sorting is a necessary condition for multi-level sorting scenarios. Suppose we have a table storing student information, where column 1 and column 2 are name and age, respectively. In this case, unstable sorting may cause the ordered nature of the input data to be lost:

# Input Data Is Sorted by Name\n# (name, age)\n  ('A', 19)\n  ('B', 18)\n  ('C', 21)\n  ('D', 19)\n  ('E', 23)\n\n# Assuming We Use an Unstable Sorting Algorithm to Sort the List by Age,\n# In the Result, the Relative Positions of ('D', 19) and ('A', 19) Are Changed,\n# And the Property That the Input Data Is Sorted by Name Is Lost\n  ('B', 18)\n  ('D', 19)\n  ('A', 19)\n  ('C', 21)\n  ('E', 23)\n

Adaptability: Adaptive sorting can utilize the existing order information in the input data to reduce the amount of computation, achieving better time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than the average time complexity.

Comparison-based or not: Comparison-based sorting relies on comparison operators (\\(<\\), \\(=\\), \\(>\\)) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of \\(O(n \\log n)\\). Non-comparison sorting does not use comparison operators and can achieve a time complexity of \\(O(n)\\), but its versatility is relatively limited.

","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1112-ideal-sorting-algorithm","level":2,"title":"11.1.2   Ideal Sorting Algorithm","text":"

Fast execution, in-place, stable, adaptive, good versatility. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem.

Next, we will learn about various sorting algorithms together and analyze the advantages and disadvantages of each sorting algorithm based on the above evaluation dimensions.

","path":["Chapter 11. Sorting","11.1   Sorting Algorithm"],"tags":[]},{"location":"chapter_sorting/summary/","level":1,"title":"11.11   Summary","text":"","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_sorting/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • Bubble sort achieves sorting by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to \\(O(n)\\).
  • Insertion sort completes sorting by inserting elements from the unsorted interval into the correct position in the sorted interval each round. Although the time complexity of insertion sort is \\(O(n^2)\\), it is very popular in small data volume sorting tasks because it involves relatively few unit operations.
  • Quick sort is implemented based on sentinel partitioning operations. In sentinel partitioning, it is possible to select the worst pivot every time, causing the time complexity to degrade to \\(O(n^2)\\). Introducing median pivot or random pivot can reduce the probability of such degradation. By preferentially recursing on the shorter sub-interval, the recursion depth can be effectively reduced, optimizing the space complexity to \\(O(\\log n)\\).
  • Merge sort includes two phases: divide and merge, which typically embody the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, with a space complexity of \\(O(n)\\); however, the space complexity of sorting a linked list can be optimized to \\(O(1)\\).
  • Bucket sort consists of three steps: distributing data into buckets, sorting within buckets, and merging results. It also embodies the divide-and-conquer strategy and is suitable for very large data volumes. The key to bucket sort is distributing data evenly.
  • Counting sort is a special case of bucket sort, which achieves sorting by counting the number of occurrences of data. Counting sort is suitable for situations where the data volume is large but the data range is limited, and requires that data can be converted to positive integers.
  • Radix sort achieves data sorting by sorting digit by digit, requiring that data can be represented as fixed-digit numbers.
  • Overall, we hope to find a sorting algorithm that is efficient, stable, in-place, and adaptive, with good versatility. However, just like other data structures and algorithms, no sorting algorithm has been found so far that simultaneously possesses all these characteristics. In practical applications, we need to select the appropriate sorting algorithm based on the specific characteristics of the data.
  • Figure 11-19 compares mainstream sorting algorithms in terms of efficiency, stability, in-place property, and adaptability.

Figure 11-19   Sorting algorithm comparison

","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_sorting/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: In what situations is the stability of sorting algorithms necessary?

In reality, we may sort based on a certain attribute of objects. For example, students have two attributes: name and height. We want to implement multi-level sorting: first sort by name to get (A, 180) (B, 185) (C, 170) (D, 170); then sort by height. Because the sorting algorithm is unstable, we may get (D, 170) (C, 170) (A, 180) (B, 185).

It can be seen that the positions of students D and C have been swapped, and the orderliness of names has been disrupted, which is something we don't want to see.

Q: Can the order of \"searching from right to left\" and \"searching from left to right\" in sentinel partitioning be swapped?

No. When we use the leftmost element as the pivot, we must first \"search from right to left\" and then \"search from left to right\". This conclusion is somewhat counterintuitive; let's analyze the reason.

The last step of sentinel partitioning partition() is to swap nums[left] and nums[i]. After the swap is complete, the elements to the left of the pivot are all <= the pivot, which requires that nums[left] >= nums[i] must hold before the last swap. Suppose we first \"search from left to right\", then if we cannot find an element larger than the pivot, we will exit the loop when i == j, at which point it may be that nums[j] == nums[i] > nums[left]. In other words, the last swap operation will swap an element larger than the pivot to the leftmost end of the array, causing sentinel partitioning to fail.

For example, given the array [0, 0, 0, 0, 1], if we first \"search from left to right\", the array after sentinel partitioning is [1, 0, 0, 0, 0], which is incorrect.

Thinking deeper, if we select nums[right] as the pivot, then it's exactly the opposite - we must first \"search from left to right\".

Q: Regarding the optimization of recursion depth in quick sort, why can selecting the shorter array ensure that the recursion depth does not exceed \\(\\log n\\)?

The recursion depth is the number of currently unreturned recursive methods. Each round of sentinel partitioning divides the original array into two sub-arrays. After recursion depth optimization, the length of the sub-array to be recursively processed is at most half of the original array length. Assuming the worst case is always half the length, the final recursion depth will be \\(\\log n\\).

Reviewing the original quick sort, we may continuously recurse on the longer array. In the worst case, it would be \\(n\\), \\(n - 1\\), \\(\\dots\\), \\(2\\), \\(1\\), with a recursion depth of \\(n\\). Recursion depth optimization can avoid this situation.

Q: When all elements in the array are equal, is the time complexity of quick sort \\(O(n^2)\\)? How should this degenerate case be handled?

Yes. For this situation, consider partitioning the array into three parts through sentinel partitioning: less than, equal to, and greater than the pivot. Only recursively process the less than and greater than parts. Under this method, an array where all input elements are equal can complete sorting in just one round of sentinel partitioning.

Q: Why is the worst-case time complexity of bucket sort \\(O(n^2)\\)?

In the worst case, all elements are distributed into the same bucket. If we use an \\(O(n^2)\\) algorithm to sort these elements, the time complexity will be \\(O(n^2)\\).

","path":["Chapter 11. Sorting","11.11   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/","level":1,"title":"Chapter 5.   Stack and Queue","text":"

Abstract

Stacks are like stacking cats, while queues are like cats lining up.

They represent LIFO (Last In First Out) and FIFO (First In First Out) logic, respectively.

","path":["Chapter 5. Stack and Queue","Chapter 5.   Stack and Queue"],"tags":[]},{"location":"chapter_stack_and_queue/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 5.1   Stack
  • 5.2   Queue
  • 5.3   Double-Ended Queue
  • 5.4   Summary
","path":["Chapter 5. Stack and Queue","Chapter 5.   Stack and Queue"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   Deque","text":"

In a queue, we can only remove elements from the front or add elements at the rear. As shown in Figure 5-7, a double-ended queue (deque) provides greater flexibility, allowing the addition or removal of elements at both the front and rear.

Figure 5-7   Operations of deque

","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#531-common-deque-operations","level":2,"title":"5.3.1   Common Deque Operations","text":"

The common operations on a deque are shown in Table 5-3. The specific method names depend on the programming language used.

Table 5-3   Efficiency of Deque Operations

Method Description Time Complexity push_first() Add element to front \\(O(1)\\) push_last() Add element to rear \\(O(1)\\) pop_first() Remove front element \\(O(1)\\) pop_last() Remove rear element \\(O(1)\\) peek_first() Access front element \\(O(1)\\) peek_last() Access rear element \\(O(1)\\)

Similarly, we can directly use the deque classes already implemented in programming languages:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby deque.py
from collections import deque\n\n# Initialize deque\ndeq: deque[int] = deque()\n\n# Enqueue elements\ndeq.append(2)      # Add to rear\ndeq.append(5)\ndeq.append(4)\ndeq.appendleft(3)  # Add to front\ndeq.appendleft(1)\n\n# Access elements\nfront: int = deq[0]  # Front element\nrear: int = deq[-1]  # Rear element\n\n# Dequeue elements\npop_front: int = deq.popleft()  # Front element dequeue\npop_rear: int = deq.pop()       # Rear element dequeue\n\n# Get deque length\nsize: int = len(deq)\n\n# Check if deque is empty\nis_empty: bool = len(deq) == 0\n
deque.cpp
/* Initialize deque */\ndeque<int> deque;\n\n/* Enqueue elements */\ndeque.push_back(2);   // Add to rear\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3);  // Add to front\ndeque.push_front(1);\n\n/* Access elements */\nint front = deque.front(); // Front element\nint back = deque.back();   // Rear element\n\n/* Dequeue elements */\ndeque.pop_front();  // Front element dequeue\ndeque.pop_back();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.size();\n\n/* Check if deque is empty */\nbool empty = deque.empty();\n
deque.java
/* Initialize deque */\nDeque<Integer> deque = new LinkedList<>();\n\n/* Enqueue elements */\ndeque.offerLast(2);   // Add to rear\ndeque.offerLast(5);\ndeque.offerLast(4);\ndeque.offerFirst(3);  // Add to front\ndeque.offerFirst(1);\n\n/* Access elements */\nint peekFirst = deque.peekFirst();  // Front element\nint peekLast = deque.peekLast();    // Rear element\n\n/* Dequeue elements */\nint popFirst = deque.pollFirst();  // Front element dequeue\nint popLast = deque.pollLast();    // Rear element dequeue\n\n/* Get deque length */\nint size = deque.size();\n\n/* Check if deque is empty */\nboolean isEmpty = deque.isEmpty();\n
deque.cs
/* Initialize deque */\n// In C#, use LinkedList as a deque\nLinkedList<int> deque = new();\n\n/* Enqueue elements */\ndeque.AddLast(2);   // Add to rear\ndeque.AddLast(5);\ndeque.AddLast(4);\ndeque.AddFirst(3);  // Add to front\ndeque.AddFirst(1);\n\n/* Access elements */\nint peekFirst = deque.First.Value;  // Front element\nint peekLast = deque.Last.Value;    // Rear element\n\n/* Dequeue elements */\ndeque.RemoveFirst();  // Front element dequeue\ndeque.RemoveLast();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.Count;\n\n/* Check if deque is empty */\nbool isEmpty = deque.Count == 0;\n
deque_test.go
/* Initialize deque */\n// In Go, use list as a deque\ndeque := list.New()\n\n/* Enqueue elements */\ndeque.PushBack(2)      // Add to rear\ndeque.PushBack(5)\ndeque.PushBack(4)\ndeque.PushFront(3)     // Add to front\ndeque.PushFront(1)\n\n/* Access elements */\nfront := deque.Front() // Front element\nrear := deque.Back()   // Rear element\n\n/* Dequeue elements */\ndeque.Remove(front)    // Front element dequeue\ndeque.Remove(rear)     // Rear element dequeue\n\n/* Get deque length */\nsize := deque.Len()\n\n/* Check if deque is empty */\nisEmpty := deque.Len() == 0\n
deque.swift
/* Initialize deque */\n// Swift does not have a built-in deque class, can use Array as a deque\nvar deque: [Int] = []\n\n/* Enqueue elements */\ndeque.append(2) // Add to rear\ndeque.append(5)\ndeque.append(4)\ndeque.insert(3, at: 0) // Add to front\ndeque.insert(1, at: 0)\n\n/* Access elements */\nlet peekFirst = deque.first! // Front element\nlet peekLast = deque.last! // Rear element\n\n/* Dequeue elements */\n// When using Array simulation, popFirst has O(n) complexity\nlet popFirst = deque.removeFirst() // Front element dequeue\nlet popLast = deque.removeLast() // Rear element dequeue\n\n/* Get deque length */\nlet size = deque.count\n\n/* Check if deque is empty */\nlet isEmpty = deque.isEmpty\n
deque.js
/* Initialize deque */\n// JavaScript does not have a built-in deque, can only use Array as a deque\nconst deque = [];\n\n/* Enqueue elements */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Please note that since it's an array, unshift() has O(n) time complexity\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Access elements */\nconst peekFirst = deque[0];\nconst peekLast = deque[deque.length - 1];\n\n/* Dequeue elements */\n// Please note that since it's an array, shift() has O(n) time complexity\nconst popFront = deque.shift();\nconst popBack = deque.pop();\n\n/* Get deque length */\nconst size = deque.length;\n\n/* Check if deque is empty */\nconst isEmpty = size === 0;\n
deque.ts
/* Initialize deque */\n// TypeScript does not have a built-in deque, can only use Array as a deque\nconst deque: number[] = [];\n\n/* Enqueue elements */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// Please note that since it's an array, unshift() has O(n) time complexity\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* Access elements */\nconst peekFirst: number = deque[0];\nconst peekLast: number = deque[deque.length - 1];\n\n/* Dequeue elements */\n// Please note that since it's an array, shift() has O(n) time complexity\nconst popFront: number = deque.shift() as number;\nconst popBack: number = deque.pop() as number;\n\n/* Get deque length */\nconst size: number = deque.length;\n\n/* Check if deque is empty */\nconst isEmpty: boolean = size === 0;\n
deque.dart
/* Initialize deque */\n// In Dart, Queue is defined as a deque\nQueue<int> deque = Queue<int>();\n\n/* Enqueue elements */\ndeque.addLast(2);  // Add to rear\ndeque.addLast(5);\ndeque.addLast(4);\ndeque.addFirst(3); // Add to front\ndeque.addFirst(1);\n\n/* Access elements */\nint peekFirst = deque.first; // Front element\nint peekLast = deque.last;   // Rear element\n\n/* Dequeue elements */\nint popFirst = deque.removeFirst(); // Front element dequeue\nint popLast = deque.removeLast();   // Rear element dequeue\n\n/* Get deque length */\nint size = deque.length;\n\n/* Check if deque is empty */\nbool isEmpty = deque.isEmpty;\n
deque.rs
/* Initialize deque */\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Enqueue elements */\ndeque.push_back(2);  // Add to rear\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // Add to front\ndeque.push_front(1);\n\n/* Access elements */\nif let Some(front) = deque.front() { // Front element\n}\nif let Some(rear) = deque.back() {   // Rear element\n}\n\n/* Dequeue elements */\nif let Some(pop_front) = deque.pop_front() { // Front element dequeue\n}\nif let Some(pop_rear) = deque.pop_back() {   // Rear element dequeue\n}\n\n/* Get deque length */\nlet size = deque.len();\n\n/* Check if deque is empty */\nlet is_empty = deque.is_empty();\n
deque.c
// C does not provide a built-in deque\n
deque.kt
/* Initialize deque */\nval deque = LinkedList<Int>()\n\n/* Enqueue elements */\ndeque.offerLast(2)  // Add to rear\ndeque.offerLast(5)\ndeque.offerLast(4)\ndeque.offerFirst(3) // Add to front\ndeque.offerFirst(1)\n\n/* Access elements */\nval peekFirst = deque.peekFirst() // Front element\nval peekLast = deque.peekLast()   // Rear element\n\n/* Dequeue elements */\nval popFirst = deque.pollFirst() // Front element dequeue\nval popLast = deque.pollLast()   // Rear element dequeue\n\n/* Get deque length */\nval size = deque.size\n\n/* Check if deque is empty */\nval isEmpty = deque.isEmpty()\n
deque.rb
# Initialize deque\n# Ruby does not have a built-in deque, can only use Array as a deque\ndeque = []\n\n# Enqueue elements\ndeque << 2\ndeque << 5\ndeque << 4\n# Please note that since it's an array, Array#unshift has O(n) time complexity\ndeque.unshift(3)\ndeque.unshift(1)\n\n# Access elements\npeek_first = deque.first\npeek_last = deque.last\n\n# Dequeue elements\n# Please note that since it's an array, Array#shift has O(n) time complexity\npop_front = deque.shift\npop_back = deque.pop\n\n# Get deque length\nsize = deque.length\n\n# Check if deque is empty\nis_empty = size.zero?\n
Code Visualization

Full Screen >

","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#532-deque-implementation","level":2,"title":"5.3.2   Deque Implementation *","text":"

The implementation of a deque is similar to that of a queue. You can choose either a linked list or an array as the underlying data structure.

","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#1-doubly-linked-list-implementation","level":3,"title":"1.   Doubly Linked List Implementation","text":"

Reviewing the previous section, we used a regular singly linked list to implement a queue because it conveniently allows deleting the head node (corresponding to dequeue) and adding new nodes after the tail node (corresponding to enqueue).

For a deque, both the front and rear can perform enqueue and dequeue operations. In other words, a deque needs to implement operations in the opposite direction as well. For this reason, we use a \"doubly linked list\" as the underlying data structure for the deque.

As shown in Figure 5-8, we treat the head and tail nodes of the doubly linked list as the front and rear of the deque, implementing functionality to add and remove nodes at both ends.

LinkedListDequepush_last()push_first()pop_last()pop_first()

Figure 5-8   Enqueue and dequeue operations in linked list implementation of deque

The implementation code is shown below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_deque.py
class ListNode:\n    \"\"\"Doubly linked list node\"\"\"\n\n    def __init__(self, val: int):\n        \"\"\"Constructor\"\"\"\n        self.val: int = val\n        self.next: ListNode | None = None  # Successor node reference\n        self.prev: ListNode | None = None  # Predecessor node reference\n\nclass LinkedListDeque:\n    \"\"\"Double-ended queue based on doubly linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._front: ListNode | None = None  # Head node front\n        self._rear: ListNode | None = None  # Tail node rear\n        self._size: int = 0  # Length of the double-ended queue\n\n    def size(self) -> int:\n        \"\"\"Get the length of the double-ended queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the double-ended queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int, is_front: bool):\n        \"\"\"Enqueue operation\"\"\"\n        node = ListNode(num)\n        # If the linked list is empty, make both front and rear point to node\n        if self.is_empty():\n            self._front = self._rear = node\n        # Front of the queue enqueue operation\n        elif is_front:\n            # Add node to the head of the linked list\n            self._front.prev = node\n            node.next = self._front\n            self._front = node  # Update head node\n        # Rear of the queue enqueue operation\n        else:\n            # Add node to the tail of the linked list\n            self._rear.next = node\n            node.prev = self._rear\n            self._rear = node  # Update tail node\n        self._size += 1  # Update queue length\n\n    def push_first(self, num: int):\n        \"\"\"Front of the queue enqueue\"\"\"\n        self.push(num, True)\n\n    def push_last(self, num: int):\n        \"\"\"Rear of the queue enqueue\"\"\"\n        self.push(num, False)\n\n    def pop(self, is_front: bool) -> int:\n        \"\"\"Dequeue operation\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        # Front of the queue dequeue operation\n        if is_front:\n            val: int = self._front.val  # Temporarily store head node value\n            # Delete head node\n            fnext: ListNode | None = self._front.next\n            if fnext is not None:\n                fnext.prev = None\n                self._front.next = None\n            self._front = fnext  # Update head node\n        # Rear of the queue dequeue operation\n        else:\n            val: int = self._rear.val  # Temporarily store tail node value\n            # Delete tail node\n            rprev: ListNode | None = self._rear.prev\n            if rprev is not None:\n                rprev.next = None\n                self._rear.prev = None\n            self._rear = rprev  # Update tail node\n        self._size -= 1  # Update queue length\n        return val\n\n    def pop_first(self) -> int:\n        \"\"\"Front of the queue dequeue\"\"\"\n        return self.pop(True)\n\n    def pop_last(self) -> int:\n        \"\"\"Rear of the queue dequeue\"\"\"\n        return self.pop(False)\n\n    def peek_first(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._front.val\n\n    def peek_last(self) -> int:\n        \"\"\"Access rear of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._rear.val\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return array for printing\"\"\"\n        node = self._front\n        res = [0] * self.size()\n        for i in range(self.size()):\n            res[i] = node.val\n            node = node.next\n        return res\n
linkedlist_deque.cpp
/* Doubly linked list node */\nstruct DoublyListNode {\n    int val;              // Node value\n    DoublyListNode *next; // Successor node pointer\n    DoublyListNode *prev; // Predecessor node pointer\n    DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {\n    }\n};\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n  private:\n    DoublyListNode *front, *rear; // Head node front, tail node rear\n    int queSize = 0;              // Length of the double-ended queue\n\n  public:\n    /* Constructor */\n    LinkedListDeque() : front(nullptr), rear(nullptr) {\n    }\n\n    /* Destructor */\n    ~LinkedListDeque() {\n        // Traverse linked list to delete nodes and free memory\n        DoublyListNode *pre, *cur = front;\n        while (cur != nullptr) {\n            pre = cur;\n            cur = cur->next;\n            delete pre;\n        }\n    }\n\n    /* Get the length of the double-ended queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue operation */\n    void push(int num, bool isFront) {\n        DoublyListNode *node = new DoublyListNode(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty())\n            front = rear = node;\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front->prev = node;\n            node->next = front;\n            front = node; // Update head node\n        // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear->next = node;\n            node->prev = rear;\n            rear = node; // Update tail node\n        }\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Dequeue operation */\n    int pop(bool isFront) {\n        if (isEmpty())\n            throw out_of_range(\"Queue is empty\");\n        int val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front->val; // Delete head node\n            // Delete head node\n            DoublyListNode *fNext = front->next;\n            if (fNext != nullptr) {\n                fNext->prev = nullptr;\n                front->next = nullptr;\n            }\n            delete front;\n            front = fNext; // Update head node\n        // Temporarily store tail node value\n        } else {\n            val = rear->val; // Delete tail node\n            // Update tail node\n            DoublyListNode *rPrev = rear->prev;\n            if (rPrev != nullptr) {\n                rPrev->next = nullptr;\n                rear->prev = nullptr;\n            }\n            delete rear;\n            rear = rPrev; // Update tail node\n        }\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    int popFirst() {\n        return pop(true);\n    }\n\n    /* Access rear of the queue element */\n    int popLast() {\n        return pop(false);\n    }\n\n    /* Return list for printing */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return front->val;\n    }\n\n    /* Driver Code */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return rear->val;\n    }\n\n    /* Return array for printing */\n    vector<int> toVector() {\n        DoublyListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_deque.java
/* Doubly linked list node */\nclass ListNode {\n    int val; // Node value\n    ListNode next; // Successor node reference\n    ListNode prev; // Predecessor node reference\n\n    ListNode(int val) {\n        this.val = val;\n        prev = next = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private ListNode front, rear; // Head node front, tail node rear\n    private int queSize = 0; // Length of the double-ended queue\n\n    public LinkedListDeque() {\n        front = rear = null;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue operation */\n    private void push(int num, boolean isFront) {\n        ListNode node = new ListNode(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty())\n            front = rear = node;\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front.prev = node;\n            node.next = front;\n            front = node; // Update head node\n        // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear.next = node;\n            node.prev = rear;\n            rear = node; // Update tail node\n        }\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    public void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    public void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* Dequeue operation */\n    private int pop(boolean isFront) {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        int val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front.val; // Delete head node\n            // Delete head node\n            ListNode fNext = front.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front.next = null;\n            }\n            front = fNext; // Update head node\n        // Temporarily store tail node value\n        } else {\n            val = rear.val; // Delete tail node\n            // Update tail node\n            ListNode rPrev = rear.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear.prev = null;\n            }\n            rear = rPrev; // Update tail node\n        }\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    public int popFirst() {\n        return pop(true);\n    }\n\n    /* Access rear of the queue element */\n    public int popLast() {\n        return pop(false);\n    }\n\n    /* Return list for printing */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Driver Code */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return rear.val;\n    }\n\n    /* Return array for printing */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_deque.cs
/* Doubly linked list node */\nclass ListNode(int val) {\n    public int val = val;       // Node value\n    public ListNode? next = null; // Successor node reference\n    public ListNode? prev = null; // Predecessor node reference\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    ListNode? front, rear; // Head node front, tail node rear\n    int queSize = 0;      // Length of the double-ended queue\n\n    public LinkedListDeque() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Enqueue operation */\n    void Push(int num, bool isFront) {\n        ListNode node = new(num);\n        // If the linked list is empty, make both front and rear point to node\n        if (IsEmpty()) {\n            front = node;\n            rear = node;\n        }\n        // Front of the queue enqueue operation\n        else if (isFront) {\n            // Add node to the head of the linked list\n            front!.prev = node;\n            node.next = front;\n            front = node; // Update head node\n        }\n        // Rear of the queue enqueue operation\n        else {\n            // Add node to the tail of the linked list\n            rear!.next = node;\n            node.prev = rear;\n            rear = node;  // Update tail node\n        }\n\n        queSize++; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    public void PushFirst(int num) {\n        Push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    public void PushLast(int num) {\n        Push(num, false);\n    }\n\n    /* Dequeue operation */\n    int? Pop(bool isFront) {\n        if (IsEmpty())\n            throw new Exception();\n        int? val;\n        // Temporarily store head node value\n        if (isFront) {\n            val = front?.val; // Delete head node\n            // Delete head node\n            ListNode? fNext = front?.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front!.next = null;\n            }\n            front = fNext;   // Update head node\n        }\n        // Temporarily store tail node value\n        else {\n            val = rear?.val;  // Delete tail node\n            // Update tail node\n            ListNode? rPrev = rear?.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear!.prev = null;\n            }\n            rear = rPrev;    // Update tail node\n        }\n\n        queSize--; // Update queue length\n        return val;\n    }\n\n    /* Rear of the queue dequeue */\n    public int? PopFirst() {\n        return Pop(true);\n    }\n\n    /* Access rear of the queue element */\n    public int? PopLast() {\n        return Pop(false);\n    }\n\n    /* Return list for printing */\n    public int? PeekFirst() {\n        if (IsEmpty())\n            throw new Exception();\n        return front?.val;\n    }\n\n    /* Driver Code */\n    public int? PeekLast() {\n        if (IsEmpty())\n            throw new Exception();\n        return rear?.val;\n    }\n\n    /* Return array for printing */\n    public int?[] ToArray() {\n        ListNode? node = front;\n        int?[] res = new int?[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node?.val;\n            node = node?.next;\n        }\n\n        return res;\n    }\n}\n
linkedlist_deque.go
/* Double-ended queue based on doubly linked list implementation */\ntype linkedListDeque struct {\n    // Use built-in package list\n    data *list.List\n}\n\n/* Initialize deque */\nfunc newLinkedListDeque() *linkedListDeque {\n    return &linkedListDeque{\n        data: list.New(),\n    }\n}\n\n/* Front element enqueue */\nfunc (s *linkedListDeque) pushFirst(value any) {\n    s.data.PushFront(value)\n}\n\n/* Rear element enqueue */\nfunc (s *linkedListDeque) pushLast(value any) {\n    s.data.PushBack(value)\n}\n\n/* Check if the double-ended queue is empty */\nfunc (s *linkedListDeque) popFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Rear element dequeue */\nfunc (s *linkedListDeque) popLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Return list for printing */\nfunc (s *linkedListDeque) peekFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* Driver Code */\nfunc (s *linkedListDeque) peekLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* Get the length of the queue */\nfunc (s *linkedListDeque) size() int {\n    return s.data.Len()\n}\n\n/* Check if the queue is empty */\nfunc (s *linkedListDeque) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListDeque) toList() *list.List {\n    return s.data\n}\n
linkedlist_deque.swift
/* Doubly linked list node */\nclass ListNode {\n    var val: Int // Node value\n    var next: ListNode? // Successor node reference\n    weak var prev: ListNode? // Predecessor node reference\n\n    init(val: Int) {\n        self.val = val\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private var front: ListNode? // Head node front\n    private var rear: ListNode? // Tail node rear\n    private var _size: Int // Length of the double-ended queue\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the double-ended queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the double-ended queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue operation */\n    private func push(num: Int, isFront: Bool) {\n        let node = ListNode(val: num)\n        // If the linked list is empty, make both front and rear point to node\n        if isEmpty() {\n            front = node\n            rear = node\n        }\n        // Front of the queue enqueue operation\n        else if isFront {\n            // Add node to the head of the linked list\n            front?.prev = node\n            node.next = front\n            front = node // Update head node\n        }\n        // Rear of the queue enqueue operation\n        else {\n            // Add node to the tail of the linked list\n            rear?.next = node\n            node.prev = rear\n            rear = node // Update tail node\n        }\n        _size += 1 // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    func pushFirst(num: Int) {\n        push(num: num, isFront: true)\n    }\n\n    /* Rear of the queue enqueue */\n    func pushLast(num: Int) {\n        push(num: num, isFront: false)\n    }\n\n    /* Dequeue operation */\n    private func pop(isFront: Bool) -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        let val: Int\n        // Temporarily store head node value\n        if isFront {\n            val = front!.val // Delete head node\n            // Delete head node\n            let fNext = front?.next\n            if fNext != nil {\n                fNext?.prev = nil\n                front?.next = nil\n            }\n            front = fNext // Update head node\n        }\n        // Temporarily store tail node value\n        else {\n            val = rear!.val // Delete tail node\n            // Update tail node\n            let rPrev = rear?.prev\n            if rPrev != nil {\n                rPrev?.next = nil\n                rear?.prev = nil\n            }\n            rear = rPrev // Update tail node\n        }\n        _size -= 1 // Update queue length\n        return val\n    }\n\n    /* Rear of the queue dequeue */\n    func popFirst() -> Int {\n        pop(isFront: true)\n    }\n\n    /* Access rear of the queue element */\n    func popLast() -> Int {\n        pop(isFront: false)\n    }\n\n    /* Return list for printing */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return front!.val\n    }\n\n    /* Driver Code */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return rear!.val\n    }\n\n    /* Return array for printing */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_deque.js
/* Doubly linked list node */\nclass ListNode {\n    prev; // Predecessor node reference (pointer)\n    next; // Successor node reference (pointer)\n    val; // Node value\n\n    constructor(val) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    #front; // Head node front\n    #rear; // Tail node rear\n    #queSize; // Length of the double-ended queue\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n        this.#queSize = 0;\n    }\n\n    /* Rear of the queue enqueue operation */\n    pushLast(val) {\n        const node = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Add node to the tail of the linked list\n            this.#rear.next = node;\n            node.prev = this.#rear;\n            this.#rear = node; // Update tail node\n        }\n        this.#queSize++;\n    }\n\n    /* Front of the queue enqueue operation */\n    pushFirst(val) {\n        const node = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // Add node to the head of the linked list\n            this.#front.prev = node;\n            node.next = this.#front;\n            this.#front = node; // Update head node\n        }\n        this.#queSize++;\n    }\n\n    /* Temporarily store tail node value */\n    popLast() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#rear.val; // Store tail node value\n        // Update tail node\n        let temp = this.#rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.#rear.prev = null;\n        }\n        this.#rear = temp; // Update tail node\n        this.#queSize--;\n        return value;\n    }\n\n    /* Temporarily store head node value */\n    popFirst() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#front.val; // Store tail node value\n        // Delete head node\n        let temp = this.#front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.#front.next = null;\n        }\n        this.#front = temp; // Update head node\n        this.#queSize--;\n        return value;\n    }\n\n    /* Driver Code */\n    peekLast() {\n        return this.#queSize === 0 ? null : this.#rear.val;\n    }\n\n    /* Return list for printing */\n    peekFirst() {\n        return this.#queSize === 0 ? null : this.#front.val;\n    }\n\n    /* Get the length of the double-ended queue */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Print deque */\n    print() {\n        const arr = [];\n        let temp = this.#front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
linkedlist_deque.ts
/* Doubly linked list node */\nclass ListNode {\n    prev: ListNode; // Predecessor node reference (pointer)\n    next: ListNode; // Successor node reference (pointer)\n    val: number; // Node value\n\n    constructor(val: number) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private front: ListNode; // Head node front\n    private rear: ListNode; // Tail node rear\n    private queSize: number; // Length of the double-ended queue\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n        this.queSize = 0;\n    }\n\n    /* Rear of the queue enqueue operation */\n    pushLast(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Add node to the tail of the linked list\n            this.rear.next = node;\n            node.prev = this.rear;\n            this.rear = node; // Update tail node\n        }\n        this.queSize++;\n    }\n\n    /* Front of the queue enqueue operation */\n    pushFirst(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // If the linked list is empty, make both front and rear point to node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // Add node to the head of the linked list\n            this.front.prev = node;\n            node.next = this.front;\n            this.front = node; // Update head node\n        }\n        this.queSize++;\n    }\n\n    /* Temporarily store tail node value */\n    popLast(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.rear.val; // Store tail node value\n        // Update tail node\n        let temp: ListNode = this.rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.rear.prev = null;\n        }\n        this.rear = temp; // Update tail node\n        this.queSize--;\n        return value;\n    }\n\n    /* Temporarily store head node value */\n    popFirst(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.front.val; // Store tail node value\n        // Delete head node\n        let temp: ListNode = this.front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.front.next = null;\n        }\n        this.front = temp; // Update head node\n        this.queSize--;\n        return value;\n    }\n\n    /* Driver Code */\n    peekLast(): number {\n        return this.queSize === 0 ? null : this.rear.val;\n    }\n\n    /* Return list for printing */\n    peekFirst(): number {\n        return this.queSize === 0 ? null : this.front.val;\n    }\n\n    /* Get the length of the double-ended queue */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Print deque */\n    print(): void {\n        const arr: number[] = [];\n        let temp: ListNode = this.front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
linkedlist_deque.dart
/* Doubly linked list node */\nclass ListNode {\n  int val; // Node value\n  ListNode? next; // Successor node reference\n  ListNode? prev; // Predecessor node reference\n\n  ListNode(this.val, {this.next, this.prev});\n}\n\n/* Deque implemented based on doubly linked list */\nclass LinkedListDeque {\n  late ListNode? _front; // Head node _front\n  late ListNode? _rear; // Tail node _rear\n  int _queSize = 0; // Length of the double-ended queue\n\n  LinkedListDeque() {\n    this._front = null;\n    this._rear = null;\n  }\n\n  /* Get deque length */\n  int size() {\n    return this._queSize;\n  }\n\n  /* Check if the double-ended queue is empty */\n  bool isEmpty() {\n    return size() == 0;\n  }\n\n  /* Enqueue operation */\n  void push(int _num, bool isFront) {\n    final ListNode node = ListNode(_num);\n    if (isEmpty()) {\n      // If list is empty, let both _front and _rear point to node\n      _front = _rear = node;\n    } else if (isFront) {\n      // Front of the queue enqueue operation\n      // Add node to the head of the linked list\n      _front!.prev = node;\n      node.next = _front;\n      _front = node; // Update head node\n    } else {\n      // Rear of the queue enqueue operation\n      // Add node to the tail of the linked list\n      _rear!.next = node;\n      node.prev = _rear;\n      _rear = node; // Update tail node\n    }\n    _queSize++; // Update queue length\n  }\n\n  /* Front of the queue enqueue */\n  void pushFirst(int _num) {\n    push(_num, true);\n  }\n\n  /* Rear of the queue enqueue */\n  void pushLast(int _num) {\n    push(_num, false);\n  }\n\n  /* Dequeue operation */\n  int? pop(bool isFront) {\n    // If queue is empty, return null directly\n    if (isEmpty()) {\n      return null;\n    }\n    final int val;\n    if (isFront) {\n      // Temporarily store head node value\n      val = _front!.val; // Delete head node\n      // Delete head node\n      ListNode? fNext = _front!.next;\n      if (fNext != null) {\n        fNext.prev = null;\n        _front!.next = null;\n      }\n      _front = fNext; // Update head node\n    } else {\n      // Temporarily store tail node value\n      val = _rear!.val; // Delete tail node\n      // Update tail node\n      ListNode? rPrev = _rear!.prev;\n      if (rPrev != null) {\n        rPrev.next = null;\n        _rear!.prev = null;\n      }\n      _rear = rPrev; // Update tail node\n    }\n    _queSize--; // Update queue length\n    return val;\n  }\n\n  /* Rear of the queue dequeue */\n  int? popFirst() {\n    return pop(true);\n  }\n\n  /* Access rear of the queue element */\n  int? popLast() {\n    return pop(false);\n  }\n\n  /* Return list for printing */\n  int? peekFirst() {\n    return _front?.val;\n  }\n\n  /* Driver Code */\n  int? peekLast() {\n    return _rear?.val;\n  }\n\n  /* Return array for printing */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> res = [];\n    for (int i = 0; i < _queSize; i++) {\n      res.add(node!.val);\n      node = node.next;\n    }\n    return res;\n  }\n}\n
linkedlist_deque.rs
/* Doubly linked list node */\npub struct ListNode<T> {\n    pub val: T,                                 // Node value\n    pub next: Option<Rc<RefCell<ListNode<T>>>>, // Successor node pointer\n    pub prev: Option<Rc<RefCell<ListNode<T>>>>, // Predecessor node pointer\n}\n\nimpl<T> ListNode<T> {\n    pub fn new(val: T) -> Rc<RefCell<ListNode<T>>> {\n        Rc::new(RefCell::new(ListNode {\n            val,\n            next: None,\n            prev: None,\n        }))\n    }\n}\n\n/* Double-ended queue based on doubly linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListDeque<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Head node front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Tail node rear\n    que_size: usize,                         // Length of the double-ended queue\n}\n\nimpl<T: Copy> LinkedListDeque<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* Get the length of the double-ended queue */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Check if the double-ended queue is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Enqueue operation */\n    fn push(&mut self, num: T, is_front: bool) {\n        let node = ListNode::new(num);\n        // Front of the queue enqueue operation\n        if is_front {\n            match self.front.take() {\n                // If the linked list is empty, make both front and rear point to node\n                None => {\n                    self.rear = Some(node.clone());\n                    self.front = Some(node);\n                }\n                // Add node to the head of the linked list\n                Some(old_front) => {\n                    old_front.borrow_mut().prev = Some(node.clone());\n                    node.borrow_mut().next = Some(old_front);\n                    self.front = Some(node); // Update head node\n                }\n            }\n        }\n        // Rear of the queue enqueue operation\n        else {\n            match self.rear.take() {\n                // If the linked list is empty, make both front and rear point to node\n                None => {\n                    self.front = Some(node.clone());\n                    self.rear = Some(node);\n                }\n                // Add node to the tail of the linked list\n                Some(old_rear) => {\n                    old_rear.borrow_mut().next = Some(node.clone());\n                    node.borrow_mut().prev = Some(old_rear);\n                    self.rear = Some(node); // Update tail node\n                }\n            }\n        }\n        self.que_size += 1; // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    pub fn push_first(&mut self, num: T) {\n        self.push(num, true);\n    }\n\n    /* Rear of the queue enqueue */\n    pub fn push_last(&mut self, num: T) {\n        self.push(num, false);\n    }\n\n    /* Dequeue operation */\n    fn pop(&mut self, is_front: bool) -> Option<T> {\n        // If queue is empty, return None directly\n        if self.is_empty() {\n            return None;\n        };\n        // Temporarily store head node value\n        if is_front {\n            self.front.take().map(|old_front| {\n                match old_front.borrow_mut().next.take() {\n                    Some(new_front) => {\n                        new_front.borrow_mut().prev.take();\n                        self.front = Some(new_front); // Update head node\n                    }\n                    None => {\n                        self.rear.take();\n                    }\n                }\n                self.que_size -= 1; // Update queue length\n                old_front.borrow().val\n            })\n        }\n        // Temporarily store tail node value\n        else {\n            self.rear.take().map(|old_rear| {\n                match old_rear.borrow_mut().prev.take() {\n                    Some(new_rear) => {\n                        new_rear.borrow_mut().next.take();\n                        self.rear = Some(new_rear); // Update tail node\n                    }\n                    None => {\n                        self.front.take();\n                    }\n                }\n                self.que_size -= 1; // Update queue length\n                old_rear.borrow().val\n            })\n        }\n    }\n\n    /* Rear of the queue dequeue */\n    pub fn pop_first(&mut self) -> Option<T> {\n        return self.pop(true);\n    }\n\n    /* Access rear of the queue element */\n    pub fn pop_last(&mut self) -> Option<T> {\n        return self.pop(false);\n    }\n\n    /* Return list for printing */\n    pub fn peek_first(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Driver Code */\n    pub fn peek_last(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.rear.as_ref()\n    }\n\n    /* Return array for printing */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n        res\n    }\n}\n
linkedlist_deque.c
/* Doubly linked list node */\ntypedef struct DoublyListNode {\n    int val;                     // Node value\n    struct DoublyListNode *next; // Successor node\n    struct DoublyListNode *prev; // Predecessor node\n} DoublyListNode;\n\n/* Constructor */\nDoublyListNode *newDoublyListNode(int num) {\n    DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode));\n    new->val = num;\n    new->next = NULL;\n    new->prev = NULL;\n    return new;\n}\n\n/* Destructor */\nvoid delDoublyListNode(DoublyListNode *node) {\n    free(node);\n}\n\n/* Double-ended queue based on doubly linked list implementation */\ntypedef struct {\n    DoublyListNode *front, *rear; // Head node front, tail node rear\n    int queSize;                  // Length of the double-ended queue\n} LinkedListDeque;\n\n/* Constructor */\nLinkedListDeque *newLinkedListDeque() {\n    LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque));\n    deque->front = NULL;\n    deque->rear = NULL;\n    deque->queSize = 0;\n    return deque;\n}\n\n/* Destructor */\nvoid delLinkedListdeque(LinkedListDeque *deque) {\n    // Free all nodes\n    for (int i = 0; i < deque->queSize && deque->front != NULL; i++) {\n        DoublyListNode *tmp = deque->front;\n        deque->front = deque->front->next;\n        free(tmp);\n    }\n    // Free deque structure\n    free(deque);\n}\n\n/* Get the length of the queue */\nint size(LinkedListDeque *deque) {\n    return deque->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(LinkedListDeque *deque) {\n    return (size(deque) == 0);\n}\n\n/* Enqueue */\nvoid push(LinkedListDeque *deque, int num, bool isFront) {\n    DoublyListNode *node = newDoublyListNode(num);\n    // If list is empty, set both front and rear to node\n    if (empty(deque)) {\n        deque->front = deque->rear = node;\n    }\n    // Front of the queue enqueue operation\n    else if (isFront) {\n        // Add node to the head of the linked list\n        deque->front->prev = node;\n        node->next = deque->front;\n        deque->front = node; // Update head node\n    }\n    // Rear of the queue enqueue operation\n    else {\n        // Add node to the tail of the linked list\n        deque->rear->next = node;\n        node->prev = deque->rear;\n        deque->rear = node;\n    }\n    deque->queSize++; // Update queue length\n}\n\n/* Front of the queue enqueue */\nvoid pushFirst(LinkedListDeque *deque, int num) {\n    push(deque, num, true);\n}\n\n/* Rear of the queue enqueue */\nvoid pushLast(LinkedListDeque *deque, int num) {\n    push(deque, num, false);\n}\n\n/* Return list for printing */\nint peekFirst(LinkedListDeque *deque) {\n    assert(size(deque) && deque->front);\n    return deque->front->val;\n}\n\n/* Driver Code */\nint peekLast(LinkedListDeque *deque) {\n    assert(size(deque) && deque->rear);\n    return deque->rear->val;\n}\n\n/* Dequeue */\nint pop(LinkedListDeque *deque, bool isFront) {\n    if (empty(deque))\n        return -1;\n    int val;\n    // Temporarily store head node value\n    if (isFront) {\n        val = peekFirst(deque); // Delete head node\n        DoublyListNode *fNext = deque->front->next;\n        if (fNext) {\n            fNext->prev = NULL;\n            deque->front->next = NULL;\n        }\n        delDoublyListNode(deque->front);\n        deque->front = fNext; // Update head node\n    }\n    // Temporarily store tail node value\n    else {\n        val = peekLast(deque); // Delete tail node\n        DoublyListNode *rPrev = deque->rear->prev;\n        if (rPrev) {\n            rPrev->next = NULL;\n            deque->rear->prev = NULL;\n        }\n        delDoublyListNode(deque->rear);\n        deque->rear = rPrev; // Update tail node\n    }\n    deque->queSize--; // Update queue length\n    return val;\n}\n\n/* Rear of the queue dequeue */\nint popFirst(LinkedListDeque *deque) {\n    return pop(deque, true);\n}\n\n/* Access rear of the queue element */\nint popLast(LinkedListDeque *deque) {\n    return pop(deque, false);\n}\n\n/* Print queue */\nvoid printLinkedListDeque(LinkedListDeque *deque) {\n    int *arr = malloc(sizeof(int) * deque->queSize);\n    // Copy data from list to array\n    int i;\n    DoublyListNode *node;\n    for (i = 0, node = deque->front; i < deque->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, deque->queSize);\n    free(arr);\n}\n
linkedlist_deque.kt
/* Doubly linked list node */\nclass ListNode(var _val: Int) {\n    // Node value\n    var next: ListNode? = null // Successor node reference\n    var prev: ListNode? = null // Predecessor node reference\n}\n\n/* Double-ended queue based on doubly linked list implementation */\nclass LinkedListDeque {\n    private var front: ListNode? = null // Head node front\n    private var rear: ListNode? = null // Tail node rear\n    private var queSize: Int = 0 // Length of the double-ended queue\n\n    /* Get the length of the double-ended queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the double-ended queue is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Enqueue operation */\n    fun push(num: Int, isFront: Boolean) {\n        val node = ListNode(num)\n        // If the linked list is empty, make both front and rear point to node\n        if (isEmpty()) {\n            rear = node\n            front = rear\n            // Front of the queue enqueue operation\n        } else if (isFront) {\n            // Add node to the head of the linked list\n            front?.prev = node\n            node.next = front\n            front = node // Update head node\n            // Rear of the queue enqueue operation\n        } else {\n            // Add node to the tail of the linked list\n            rear?.next = node\n            node.prev = rear\n            rear = node // Update tail node\n        }\n        queSize++ // Update queue length\n    }\n\n    /* Front of the queue enqueue */\n    fun pushFirst(num: Int) {\n        push(num, true)\n    }\n\n    /* Rear of the queue enqueue */\n    fun pushLast(num: Int) {\n        push(num, false)\n    }\n\n    /* Dequeue operation */\n    fun pop(isFront: Boolean): Int {\n        if (isEmpty()) \n            throw IndexOutOfBoundsException()\n        val _val: Int\n        // Temporarily store head node value\n        if (isFront) {\n            _val = front!!._val // Delete head node\n            // Delete head node\n            val fNext = front!!.next\n            if (fNext != null) {\n                fNext.prev = null\n                front!!.next = null\n            }\n            front = fNext // Update head node\n            // Temporarily store tail node value\n        } else {\n            _val = rear!!._val // Delete tail node\n            // Update tail node\n            val rPrev = rear!!.prev\n            if (rPrev != null) {\n                rPrev.next = null\n                rear!!.prev = null\n            }\n            rear = rPrev // Update tail node\n        }\n        queSize-- // Update queue length\n        return _val\n    }\n\n    /* Rear of the queue dequeue */\n    fun popFirst(): Int {\n        return pop(true)\n    }\n\n    /* Access rear of the queue element */\n    fun popLast(): Int {\n        return pop(false)\n    }\n\n    /* Return list for printing */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Driver Code */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return rear!!._val\n    }\n\n    /* Return array for printing */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_deque.rb
=begin\nFile: linkedlist_deque.rb\nCreated Time: 2024-04-06\nAuthor: Xuan Khoa Tu Nguyen (ngxktuzkai2000@gmail.com)\n=end\n\n### Doubly linked list node\nclass ListNode\n  attr_accessor :val\n  attr_accessor :next # Successor node reference\n  attr_accessor :prev # Predecessor node reference\n\n  ### Constructor ###\n  def initialize(val)\n    @val = val\n  end\nend\n\n### Deque based on doubly linked list ###\nclass LinkedListDeque\n  ### Get deque length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @front = nil  # Head node front\n    @rear = nil   # Tail node rear\n    @size = 0     # Length of the double-ended queue\n  end\n\n  ### Check if deque is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue operation ###\n  def push(num, is_front)\n    node = ListNode.new(num)\n    # If list is empty, set both front and rear to node\n    if is_empty?\n      @front = @rear = node\n    # Front of the queue enqueue operation\n    elsif is_front\n      # Add node to the head of the linked list\n      @front.prev = node\n      node.next = @front\n      @front = node # Update head node\n    # Rear of the queue enqueue operation\n    else\n      # Add node to the tail of the linked list\n      @rear.next = node\n      node.prev = @rear\n      @rear = node # Update tail node\n    end\n    @size += 1 # Update queue length\n  end\n\n  ### Enqueue at front ###\n  def push_first(num)\n    push(num, true)\n  end\n\n  ### Enqueue at rear ###\n  def push_last(num)\n    push(num, false)\n  end\n\n  ### Dequeue operation ###\n  def pop(is_front)\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    # Temporarily store head node value\n    if is_front\n      val = @front.val # Delete head node\n      # Delete head node\n      fnext = @front.next\n      unless fnext.nil?\n        fnext.prev = nil\n        @front.next = nil\n      end\n      @front = fnext # Update head node\n    # Temporarily store tail node value\n    else\n      val = @rear.val # Delete tail node\n      # Update tail node\n      rprev = @rear.prev\n      unless rprev.nil?\n        rprev.next = nil\n        @rear.prev = nil\n      end\n      @rear = rprev # Update tail node\n    end\n    @size -= 1 # Update queue length\n\n    val\n  end\n\n  ### Dequeue from front ###\n  def pop_first\n    pop(true)\n  end\n\n  ### Dequeue from front ###\n  def pop_last\n    pop(false)\n  end\n\n  ### Access front element ###\n  def peek_first\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @front.val\n  end\n\n  ### Access rear element ###\n  def peek_last\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @rear.val\n  end\n\n  ### Return array for printing ###\n  def to_array\n    node = @front\n    res = Array.new(size, 0)\n    for i in 0...size\n      res[i] = node.val\n      node = node.next\n    end\n    res\n  end\nend\n
","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

As shown in Figure 5-9, similar to implementing a queue based on an array, we can also use a circular array to implement a deque.

ArrayDequepush_last()push_first()pop_last()pop_first()

Figure 5-9   Enqueue and dequeue operations in array implementation of deque

Based on the queue implementation, we only need to add methods for \"enqueue at front\" and \"dequeue from rear\":

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_deque.py
class ArrayDeque:\n    \"\"\"Double-ended queue based on circular array implementation\"\"\"\n\n    def __init__(self, capacity: int):\n        \"\"\"Constructor\"\"\"\n        self._nums: list[int] = [0] * capacity\n        self._front: int = 0\n        self._size: int = 0\n\n    def capacity(self) -> int:\n        \"\"\"Get the capacity of the double-ended queue\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Get the length of the double-ended queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the double-ended queue is empty\"\"\"\n        return self._size == 0\n\n    def index(self, i: int) -> int:\n        \"\"\"Calculate circular array index\"\"\"\n        # Use modulo operation to wrap the array head and tail together\n        # When i passes the tail of the array, return to the head\n        # When i passes the head of the array, return to the tail\n        return (i + self.capacity()) % self.capacity()\n\n    def push_first(self, num: int):\n        \"\"\"Front of the queue enqueue\"\"\"\n        if self._size == self.capacity():\n            print(\"Double-ended queue is full\")\n            return\n        # Front pointer moves one position to the left\n        # Use modulo operation to wrap front around to the tail after passing the head of the array\n        self._front = self.index(self._front - 1)\n        # Add num to the front of the queue\n        self._nums[self._front] = num\n        self._size += 1\n\n    def push_last(self, num: int):\n        \"\"\"Rear of the queue enqueue\"\"\"\n        if self._size == self.capacity():\n            print(\"Double-ended queue is full\")\n            return\n        # Calculate rear pointer, points to rear index + 1\n        rear = self.index(self._front + self._size)\n        # Add num to the rear of the queue\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop_first(self) -> int:\n        \"\"\"Front of the queue dequeue\"\"\"\n        num = self.peek_first()\n        # Front pointer moves one position backward\n        self._front = self.index(self._front + 1)\n        self._size -= 1\n        return num\n\n    def pop_last(self) -> int:\n        \"\"\"Rear of the queue dequeue\"\"\"\n        num = self.peek_last()\n        self._size -= 1\n        return num\n\n    def peek_first(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        return self._nums[self._front]\n\n    def peek_last(self) -> int:\n        \"\"\"Access rear of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Double-ended queue is empty\")\n        # Calculate tail element index\n        last = self.index(self._front + self._size - 1)\n        return self._nums[last]\n\n    def to_array(self) -> list[int]:\n        \"\"\"Return array for printing\"\"\"\n        # Only convert list elements within the valid length range\n        res = []\n        for i in range(self._size):\n            res.append(self._nums[self.index(self._front + i)])\n        return res\n
array_deque.cpp
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n  private:\n    vector<int> nums; // Array for storing double-ended queue elements\n    int front;        // Front pointer, points to the front of the queue element\n    int queSize;      // Double-ended queue length\n\n  public:\n    /* Constructor */\n    ArrayDeque(int capacity) {\n        nums.resize(capacity);\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    int capacity() {\n        return nums.size();\n    }\n\n    /* Get the length of the double-ended queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    int index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity();\n    }\n\n    /* Front of the queue enqueue */\n    void pushFirst(int num) {\n        if (queSize == capacity()) {\n            cout << \"Double-ended queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    void pushLast(int num) {\n        if (queSize == capacity()) {\n            cout << \"Double-ended queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    int popFirst() {\n        int num = peekFirst();\n        // Move front pointer backward by one position\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        return nums[front];\n    }\n\n    /* Driver Code */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"Deque is empty\");\n        // Initialize double-ended queue\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    vector<int> toVector() {\n        // Elements enqueue\n        vector<int> res(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n};\n
array_deque.java
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private int[] nums; // Array for storing double-ended queue elements\n    private int front; // Front pointer, points to the front of the queue element\n    private int queSize; // Double-ended queue length\n\n    /* Constructor */\n    public ArrayDeque(int capacity) {\n        this.nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    private int index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity();\n    }\n\n    /* Front of the queue enqueue */\n    public void pushFirst(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    public void pushLast(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    public int popFirst() {\n        int num = peekFirst();\n        // Move front pointer backward by one position\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    public int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Driver Code */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        // Initialize double-ended queue\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    public int[] toArray() {\n        // Elements enqueue\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.cs
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    int[] nums;  // Array for storing double-ended queue elements\n    int front;   // Front pointer, points to the front of the queue element\n    int queSize; // Double-ended queue length\n\n    /* Constructor */\n    public ArrayDeque(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Get the length of the double-ended queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Calculate circular array index */\n    int Index(int i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + Capacity()) % Capacity();\n    }\n\n    /* Front of the queue enqueue */\n    public void PushFirst(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = Index(front - 1);\n        // Add num to front of queue\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    public void PushLast(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        int rear = Index(front + queSize);\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    public int PopFirst() {\n        int num = PeekFirst();\n        // Move front pointer backward by one position\n        front = Index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    public int PopLast() {\n        int num = PeekLast();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int PeekFirst() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        return nums[front];\n    }\n\n    /* Driver Code */\n    public int PeekLast() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        // Initialize double-ended queue\n        int last = Index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* Return array for printing */\n    public int[] ToArray() {\n        // Elements enqueue\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[Index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.go
/* Double-ended queue based on circular array implementation */\ntype arrayDeque struct {\n    nums        []int // Array for storing double-ended queue elements\n    front       int   // Front pointer, points to the front of the queue element\n    queSize     int   // Double-ended queue length\n    queCapacity int   // Queue capacity (maximum number of elements)\n}\n\n/* Access front of the queue element */\nfunc newArrayDeque(queCapacity int) *arrayDeque {\n    return &arrayDeque{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* Get the length of the double-ended queue */\nfunc (q *arrayDeque) size() int {\n    return q.queSize\n}\n\n/* Check if the double-ended queue is empty */\nfunc (q *arrayDeque) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Calculate circular array index */\nfunc (q *arrayDeque) index(i int) int {\n    // Use modulo operation to wrap the array head and tail together\n    // When i passes the tail of the array, return to the head\n    // When i passes the head of the array, return to the tail\n    return (i + q.queCapacity) % q.queCapacity\n}\n\n/* Front of the queue enqueue */\nfunc (q *arrayDeque) pushFirst(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Double-ended queue is full\")\n        return\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Add num to the front of the queue\n    q.front = q.index(q.front - 1)\n    // Add num to front of queue\n    q.nums[q.front] = num\n    q.queSize++\n}\n\n/* Rear of the queue enqueue */\nfunc (q *arrayDeque) pushLast(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"Double-ended queue is full\")\n        return\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    rear := q.index(q.front + q.queSize)\n    // Front pointer moves one position backward\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Rear of the queue dequeue */\nfunc (q *arrayDeque) popFirst() any {\n    num := q.peekFirst()\n    if num == nil {\n        return nil\n    }\n    // Move front pointer backward by one position\n    q.front = q.index(q.front + 1)\n    q.queSize--\n    return num\n}\n\n/* Access rear of the queue element */\nfunc (q *arrayDeque) popLast() any {\n    num := q.peekLast()\n    if num == nil {\n        return nil\n    }\n    q.queSize--\n    return num\n}\n\n/* Return list for printing */\nfunc (q *arrayDeque) peekFirst() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Driver Code */\nfunc (q *arrayDeque) peekLast() any {\n    if q.isEmpty() {\n        return nil\n    }\n    // Initialize double-ended queue\n    last := q.index(q.front + q.queSize - 1)\n    return q.nums[last]\n}\n\n/* Get Slice for printing */\nfunc (q *arrayDeque) toSlice() []int {\n    // Elements enqueue\n    res := make([]int, q.queSize)\n    for i, j := 0, q.front; i < q.queSize; i++ {\n        res[i] = q.nums[q.index(j)]\n        j++\n    }\n    return res\n}\n
array_deque.swift
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private var nums: [Int] // Array for storing double-ended queue elements\n    private var front: Int // Front pointer, points to the front of the queue element\n    private var _size: Int // Double-ended queue length\n\n    /* Constructor */\n    init(capacity: Int) {\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Get the capacity of the double-ended queue */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Get the length of the double-ended queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the double-ended queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Calculate circular array index */\n    private func index(i: Int) -> Int {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        (i + capacity()) % capacity()\n    }\n\n    /* Front of the queue enqueue */\n    func pushFirst(num: Int) {\n        if size() == capacity() {\n            print(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(i: front - 1)\n        // Add num to front of queue\n        nums[front] = num\n        _size += 1\n    }\n\n    /* Rear of the queue enqueue */\n    func pushLast(num: Int) {\n        if size() == capacity() {\n            print(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        let rear = index(i: front + size())\n        // Front pointer moves one position backward\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Rear of the queue dequeue */\n    func popFirst() -> Int {\n        let num = peekFirst()\n        // Move front pointer backward by one position\n        front = index(i: front + 1)\n        _size -= 1\n        return num\n    }\n\n    /* Access rear of the queue element */\n    func popLast() -> Int {\n        let num = peekLast()\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        return nums[front]\n    }\n\n    /* Driver Code */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"Deque is empty\")\n        }\n        // Initialize double-ended queue\n        let last = index(i: front + size() - 1)\n        return nums[last]\n    }\n\n    /* Return array for printing */\n    func toArray() -> [Int] {\n        // Elements enqueue\n        (front ..< front + size()).map { nums[index(i: $0)] }\n    }\n}\n
array_deque.js
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    #nums; // Array for storing double-ended queue elements\n    #front; // Front pointer, points to the front of the queue element\n    #queSize; // Double-ended queue length\n\n    /* Constructor */\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n        this.#front = 0;\n        this.#queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    capacity() {\n        return this.#nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    size() {\n        return this.#queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Calculate circular array index */\n    index(i) {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Front of the queue enqueue */\n    pushFirst(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        this.#front = this.index(this.#front - 1);\n        // Add num to front of queue\n        this.#nums[this.#front] = num;\n        this.#queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    pushLast(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        const rear = this.index(this.#front + this.#queSize);\n        // Front pointer moves one position backward\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    popFirst() {\n        const num = this.peekFirst();\n        // Move front pointer backward by one position\n        this.#front = this.index(this.#front + 1);\n        this.#queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    popLast() {\n        const num = this.peekLast();\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peekFirst() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.#nums[this.#front];\n    }\n\n    /* Driver Code */\n    peekLast() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Initialize double-ended queue\n        const last = this.index(this.#front + this.#queSize - 1);\n        return this.#nums[last];\n    }\n\n    /* Return array for printing */\n    toArray() {\n        // Elements enqueue\n        const res = [];\n        for (let i = 0, j = this.#front; i < this.#queSize; i++, j++) {\n            res[i] = this.#nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.ts
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n    private nums: number[]; // Array for storing double-ended queue elements\n    private front: number; // Front pointer, points to the front of the queue element\n    private queSize: number; // Double-ended queue length\n\n    /* Constructor */\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = 0;\n        this.queSize = 0;\n    }\n\n    /* Get the capacity of the double-ended queue */\n    capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Get the length of the double-ended queue */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the double-ended queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Calculate circular array index */\n    index(i: number): number {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* Front of the queue enqueue */\n    pushFirst(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        this.front = this.index(this.front - 1);\n        // Add num to front of queue\n        this.nums[this.front] = num;\n        this.queSize++;\n    }\n\n    /* Rear of the queue enqueue */\n    pushLast(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('Double-ended queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        const rear: number = this.index(this.front + this.queSize);\n        // Front pointer moves one position backward\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Rear of the queue dequeue */\n    popFirst(): number {\n        const num: number = this.peekFirst();\n        // Move front pointer backward by one position\n        this.front = this.index(this.front + 1);\n        this.queSize--;\n        return num;\n    }\n\n    /* Access rear of the queue element */\n    popLast(): number {\n        const num: number = this.peekLast();\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peekFirst(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.nums[this.front];\n    }\n\n    /* Driver Code */\n    peekLast(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // Initialize double-ended queue\n        const last = this.index(this.front + this.queSize - 1);\n        return this.nums[last];\n    }\n\n    /* Return array for printing */\n    toArray(): number[] {\n        // Elements enqueue\n        const res: number[] = [];\n        for (let i = 0, j = this.front; i < this.queSize; i++, j++) {\n            res[i] = this.nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.dart
/* Double-ended queue based on circular array implementation */\nclass ArrayDeque {\n  late List<int> _nums; // Array for storing double-ended queue elements\n  late int _front; // Front pointer, points to the front of the queue element\n  late int _queSize; // Double-ended queue length\n\n  /* Constructor */\n  ArrayDeque(int capacity) {\n    this._nums = List.filled(capacity, 0);\n    this._front = this._queSize = 0;\n  }\n\n  /* Get the capacity of the double-ended queue */\n  int capacity() {\n    return _nums.length;\n  }\n\n  /* Get the length of the double-ended queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the double-ended queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Calculate circular array index */\n  int index(int i) {\n    // Use modulo operation to wrap the array head and tail together\n    // When i passes the tail of the array, return to the head\n    // When i passes the head of the array, return to the tail\n    return (i + capacity()) % capacity();\n  }\n\n  /* Front of the queue enqueue */\n  void pushFirst(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Double-ended queue is full\");\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Use modulo operation to wrap _front from array head back to tail\n    _front = index(_front - 1);\n    // Add _num to queue front\n    _nums[_front] = _num;\n    _queSize++;\n  }\n\n  /* Rear of the queue enqueue */\n  void pushLast(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"Double-ended queue is full\");\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    int rear = index(_front + _queSize);\n    // Add _num to queue rear\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Rear of the queue dequeue */\n  int popFirst() {\n    int _num = peekFirst();\n    // Move front pointer right by one\n    _front = index(_front + 1);\n    _queSize--;\n    return _num;\n  }\n\n  /* Access rear of the queue element */\n  int popLast() {\n    int _num = peekLast();\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peekFirst() {\n    if (isEmpty()) {\n      throw Exception(\"Deque is empty\");\n    }\n    return _nums[_front];\n  }\n\n  /* Driver Code */\n  int peekLast() {\n    if (isEmpty()) {\n      throw Exception(\"Deque is empty\");\n    }\n    // Initialize double-ended queue\n    int last = index(_front + _queSize - 1);\n    return _nums[last];\n  }\n\n  /* Return array for printing */\n  List<int> toArray() {\n    // Elements enqueue\n    List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[index(j)];\n    }\n    return res;\n  }\n}\n
array_deque.rs
/* Double-ended queue based on circular array implementation */\nstruct ArrayDeque<T> {\n    nums: Vec<T>,    // Array for storing double-ended queue elements\n    front: usize,    // Front pointer, points to the front of the queue element\n    que_size: usize, // Double-ended queue length\n}\n\nimpl<T: Copy + Default> ArrayDeque<T> {\n    /* Constructor */\n    pub fn new(capacity: usize) -> Self {\n        Self {\n            nums: vec![T::default(); capacity],\n            front: 0,\n            que_size: 0,\n        }\n    }\n\n    /* Get the capacity of the double-ended queue */\n    pub fn capacity(&self) -> usize {\n        self.nums.len()\n    }\n\n    /* Get the length of the double-ended queue */\n    pub fn size(&self) -> usize {\n        self.que_size\n    }\n\n    /* Check if the double-ended queue is empty */\n    pub fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Calculate circular array index */\n    fn index(&self, i: i32) -> usize {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        ((i + self.capacity() as i32) % self.capacity() as i32) as usize\n    }\n\n    /* Front of the queue enqueue */\n    pub fn push_first(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        self.front = self.index(self.front as i32 - 1);\n        // Add num to front of queue\n        self.nums[self.front] = num;\n        self.que_size += 1;\n    }\n\n    /* Rear of the queue enqueue */\n    pub fn push_last(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Double-ended queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        let rear = self.index(self.front as i32 + self.que_size as i32);\n        // Front pointer moves one position backward\n        self.nums[rear] = num;\n        self.que_size += 1;\n    }\n\n    /* Rear of the queue dequeue */\n    fn pop_first(&mut self) -> T {\n        let num = self.peek_first();\n        // Move front pointer backward by one position\n        self.front = self.index(self.front as i32 + 1);\n        self.que_size -= 1;\n        num\n    }\n\n    /* Access rear of the queue element */\n    fn pop_last(&mut self) -> T {\n        let num = self.peek_last();\n        self.que_size -= 1;\n        num\n    }\n\n    /* Return list for printing */\n    fn peek_first(&self) -> T {\n        if self.is_empty() {\n            panic!(\"Deque is empty\")\n        };\n        self.nums[self.front]\n    }\n\n    /* Driver Code */\n    fn peek_last(&self) -> T {\n        if self.is_empty() {\n            panic!(\"Deque is empty\")\n        };\n        // Initialize double-ended queue\n        let last = self.index(self.front as i32 + self.que_size as i32 - 1);\n        self.nums[last]\n    }\n\n    /* Return array for printing */\n    fn to_array(&self) -> Vec<T> {\n        // Elements enqueue\n        let mut res = vec![T::default(); self.que_size];\n        let mut j = self.front;\n        for i in 0..self.que_size {\n            res[i] = self.nums[self.index(j as i32)];\n            j += 1;\n        }\n        res\n    }\n}\n
array_deque.c
/* Double-ended queue based on circular array implementation */\ntypedef struct {\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Rear pointer, points to rear + 1\n    int queCapacity; // Queue capacity\n} ArrayDeque;\n\n/* Constructor */\nArrayDeque *newArrayDeque(int capacity) {\n    ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque));\n    // Initialize array\n    deque->queCapacity = capacity;\n    deque->nums = (int *)malloc(sizeof(int) * deque->queCapacity);\n    deque->front = deque->queSize = 0;\n    return deque;\n}\n\n/* Destructor */\nvoid delArrayDeque(ArrayDeque *deque) {\n    free(deque->nums);\n    free(deque);\n}\n\n/* Get the capacity of the double-ended queue */\nint capacity(ArrayDeque *deque) {\n    return deque->queCapacity;\n}\n\n/* Get the length of the double-ended queue */\nint size(ArrayDeque *deque) {\n    return deque->queSize;\n}\n\n/* Check if the double-ended queue is empty */\nbool empty(ArrayDeque *deque) {\n    return deque->queSize == 0;\n}\n\n/* Calculate circular array index */\nint dequeIndex(ArrayDeque *deque, int i) {\n    // Use modulo operation to wrap the array head and tail together\n    // When i exceeds array end, wrap to head\n    // When i passes the head of the array, return to the tail\n    return ((i + capacity(deque)) % capacity(deque));\n}\n\n/* Front of the queue enqueue */\nvoid pushFirst(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Deque is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap front around to the tail after passing the head of the array\n    // Use modulo to wrap front from array head to rear\n    deque->front = dequeIndex(deque, deque->front - 1);\n    // Add num to queue front\n    deque->nums[deque->front] = num;\n    deque->queSize++;\n}\n\n/* Rear of the queue enqueue */\nvoid pushLast(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"Deque is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    int rear = dequeIndex(deque, deque->front + deque->queSize);\n    // Front pointer moves one position backward\n    deque->nums[rear] = num;\n    deque->queSize++;\n}\n\n/* Return list for printing */\nint peekFirst(ArrayDeque *deque) {\n    // Access error: Deque is empty\n    assert(empty(deque) == 0);\n    return deque->nums[deque->front];\n}\n\n/* Driver Code */\nint peekLast(ArrayDeque *deque) {\n    // Access error: Deque is empty\n    assert(empty(deque) == 0);\n    int last = dequeIndex(deque, deque->front + deque->queSize - 1);\n    return deque->nums[last];\n}\n\n/* Rear of the queue dequeue */\nint popFirst(ArrayDeque *deque) {\n    int num = peekFirst(deque);\n    // Move front pointer backward by one position\n    deque->front = dequeIndex(deque, deque->front + 1);\n    deque->queSize--;\n    return num;\n}\n\n/* Access rear of the queue element */\nint popLast(ArrayDeque *deque) {\n    int num = peekLast(deque);\n    deque->queSize--;\n    return num;\n}\n\n/* Return array for printing */\nint *toArray(ArrayDeque *deque, int *queSize) {\n    *queSize = deque->queSize;\n    int *res = (int *)calloc(deque->queSize, sizeof(int));\n    int j = deque->front;\n    for (int i = 0; i < deque->queSize; i++) {\n        res[i] = deque->nums[j % deque->queCapacity];\n        j++;\n    }\n    return res;\n}\n
array_deque.kt
/* Constructor */\nclass ArrayDeque(capacity: Int) {\n    private var nums: IntArray = IntArray(capacity) // Array for storing double-ended queue elements\n    private var front: Int = 0 // Front pointer, points to the front of the queue element\n    private var queSize: Int = 0 // Double-ended queue length\n\n    /* Get the capacity of the double-ended queue */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Get the length of the double-ended queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the double-ended queue is empty */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Calculate circular array index */\n    private fun index(i: Int): Int {\n        // Use modulo operation to wrap the array head and tail together\n        // When i passes the tail of the array, return to the head\n        // When i passes the head of the array, return to the tail\n        return (i + capacity()) % capacity()\n    }\n\n    /* Front of the queue enqueue */\n    fun pushFirst(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap front around to the tail after passing the head of the array\n        // Add num to the front of the queue\n        front = index(front - 1)\n        // Add num to front of queue\n        nums[front] = num\n        queSize++\n    }\n\n    /* Rear of the queue enqueue */\n    fun pushLast(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Double-ended queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        val rear = index(front + queSize)\n        // Front pointer moves one position backward\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Rear of the queue dequeue */\n    fun popFirst(): Int {\n        val num = peekFirst()\n        // Move front pointer backward by one position\n        front = index(front + 1)\n        queSize--\n        return num\n    }\n\n    /* Access rear of the queue element */\n    fun popLast(): Int {\n        val num = peekLast()\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Driver Code */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // Initialize double-ended queue\n        val last = index(front + queSize - 1)\n        return nums[last]\n    }\n\n    /* Return array for printing */\n    fun toArray(): IntArray {\n        // Elements enqueue\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[index(j)]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
array_deque.rb
### Deque based on circular array ###\nclass ArrayDeque\n  ### Get deque length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize(capacity)\n    @nums = Array.new(capacity, 0)\n    @front = 0\n    @size = 0\n  end\n\n  ### Get deque capacity ###\n  def capacity\n    @nums.length\n  end\n\n  ### Check if deque is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue at front ###\n  def push_first(num)\n    if size == capacity\n      puts 'Double-ended queue is full'\n      return\n    end\n\n    # Use modulo operation to wrap front around to the tail after passing the head of the array\n    # Add num to the front of the queue\n    @front = index(@front - 1)\n    # Add num to front of queue\n    @nums[@front] = num\n    @size += 1\n  end\n\n  ### Enqueue at rear ###\n  def push_last(num)\n    if size == capacity\n      puts 'Double-ended queue is full'\n      return\n    end\n\n    # Use modulo operation to wrap rear around to the head after passing the tail of the array\n    rear = index(@front + size)\n    # Front pointer moves one position backward\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Dequeue from front ###\n  def pop_first\n    num = peek_first\n    # Move front pointer backward by one position\n    @front = index(@front + 1)\n    @size -= 1\n    num\n  end\n\n  ### Dequeue from rear ###\n  def pop_last\n    num = peek_last\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek_first\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Access rear element ###\n  def peek_last\n    raise IndexError, 'Deque is empty' if is_empty?\n\n    # Initialize double-ended queue\n    last = index(@front + size - 1)\n    @nums[last]\n  end\n\n  ### Return array for printing ###\n  def to_array\n    # Elements enqueue\n    res = []\n    for i in 0...size\n      res << @nums[index(@front + i)]\n    end\n    res\n  end\n\n  private\n\n  ### Calculate circular array index ###\n  def index(i)\n    # Use modulo operation to wrap the array head and tail together\n    # When i passes the tail of the array, return to the head\n    # When i passes the head of the array, return to the tail\n    (i + capacity) % capacity\n  end\nend\n
","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#533-deque-applications","level":2,"title":"5.3.3   Deque Applications","text":"

A deque combines the logic of both stacks and queues. Therefore, it can implement all application scenarios of both, while providing greater flexibility.

We know that the \"undo\" function in software is typically implemented using a stack: the system pushes each change operation onto the stack and then implements undo through pop. However, considering system resource limitations, software usually limits the number of undo steps (for example, only allowing 50 steps to be saved). When the stack length exceeds 50, the software needs to perform a deletion operation at the bottom of the stack (front of the queue). But a stack cannot implement this functionality, so a deque is needed to replace the stack. Note that the core logic of \"undo\" still follows the LIFO principle of a stack; it's just that the deque can more flexibly implement some additional logic.

","path":["Chapter 5. Stack and Queue","5.3   Deque"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   Queue","text":"

A queue is a linear data structure that follows the First In First Out (FIFO) rule. As the name suggests, a queue simulates the phenomenon of lining up, where newcomers continuously join the end of the queue, while people at the front of the queue leave one by one.

As shown in Figure 5-4, we call the front of the queue the \"front\" and the end the \"rear.\" The operation of adding an element to the rear is called \"enqueue,\" and the operation of removing the front element is called \"dequeue.\"

Figure 5-4   FIFO rule of queue

","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521-common-queue-operations","level":2,"title":"5.2.1   Common Queue Operations","text":"

The common operations on a queue are shown in Table 5-2. Note that method names may vary across different programming languages. We adopt the same naming convention as for stacks here.

Table 5-2   Efficiency of Queue Operations

Method Description Time Complexity push() Enqueue element, add element to rear \\(O(1)\\) pop() Dequeue front element \\(O(1)\\) peek() Access front element \\(O(1)\\)

We can directly use the ready-made queue classes in programming languages:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby queue.py
from collections import deque\n\n# Initialize queue\n# In Python, we generally use the deque class as a queue\n# Although queue.Queue() is a pure queue class, it is not very user-friendly, so it is not recommended\nque: deque[int] = deque()\n\n# Enqueue elements\nque.append(1)\nque.append(3)\nque.append(2)\nque.append(5)\nque.append(4)\n\n# Access front element\nfront: int = que[0]\n\n# Dequeue element\npop: int = que.popleft()\n\n# Get queue length\nsize: int = len(que)\n\n# Check if queue is empty\nis_empty: bool = len(que) == 0\n
queue.cpp
/* Initialize queue */\nqueue<int> queue;\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nint front = queue.front();\n\n/* Dequeue element */\nqueue.pop();\n\n/* Get queue length */\nint size = queue.size();\n\n/* Check if queue is empty */\nbool empty = queue.empty();\n
queue.java
/* Initialize queue */\nQueue<Integer> queue = new LinkedList<>();\n\n/* Enqueue elements */\nqueue.offer(1);\nqueue.offer(3);\nqueue.offer(2);\nqueue.offer(5);\nqueue.offer(4);\n\n/* Access front element */\nint peek = queue.peek();\n\n/* Dequeue element */\nint pop = queue.poll();\n\n/* Get queue length */\nint size = queue.size();\n\n/* Check if queue is empty */\nboolean isEmpty = queue.isEmpty();\n
queue.cs
/* Initialize queue */\nQueue<int> queue = new();\n\n/* Enqueue elements */\nqueue.Enqueue(1);\nqueue.Enqueue(3);\nqueue.Enqueue(2);\nqueue.Enqueue(5);\nqueue.Enqueue(4);\n\n/* Access front element */\nint peek = queue.Peek();\n\n/* Dequeue element */\nint pop = queue.Dequeue();\n\n/* Get queue length */\nint size = queue.Count;\n\n/* Check if queue is empty */\nbool isEmpty = queue.Count == 0;\n
queue_test.go
/* Initialize queue */\n// In Go, use list as a queue\nqueue := list.New()\n\n/* Enqueue elements */\nqueue.PushBack(1)\nqueue.PushBack(3)\nqueue.PushBack(2)\nqueue.PushBack(5)\nqueue.PushBack(4)\n\n/* Access front element */\npeek := queue.Front()\n\n/* Dequeue element */\npop := queue.Front()\nqueue.Remove(pop)\n\n/* Get queue length */\nsize := queue.Len()\n\n/* Check if queue is empty */\nisEmpty := queue.Len() == 0\n
queue.swift
/* Initialize queue */\n// Swift does not have a built-in queue class, can use Array as a queue\nvar queue: [Int] = []\n\n/* Enqueue elements */\nqueue.append(1)\nqueue.append(3)\nqueue.append(2)\nqueue.append(5)\nqueue.append(4)\n\n/* Access front element */\nlet peek = queue.first!\n\n/* Dequeue element */\n// Since it's an array, removeFirst has O(n) complexity\nlet pool = queue.removeFirst()\n\n/* Get queue length */\nlet size = queue.count\n\n/* Check if queue is empty */\nlet isEmpty = queue.isEmpty\n
queue.js
/* Initialize queue */\n// JavaScript does not have a built-in queue, can use Array as a queue\nconst queue = [];\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nconst peek = queue[0];\n\n/* Dequeue element */\n// The underlying structure is an array, so shift() has O(n) time complexity\nconst pop = queue.shift();\n\n/* Get queue length */\nconst size = queue.length;\n\n/* Check if queue is empty */\nconst empty = queue.length === 0;\n
queue.ts
/* Initialize queue */\n// TypeScript does not have a built-in queue, can use Array as a queue\nconst queue: number[] = [];\n\n/* Enqueue elements */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* Access front element */\nconst peek = queue[0];\n\n/* Dequeue element */\n// The underlying structure is an array, so shift() has O(n) time complexity\nconst pop = queue.shift();\n\n/* Get queue length */\nconst size = queue.length;\n\n/* Check if queue is empty */\nconst empty = queue.length === 0;\n
queue.dart
/* Initialize queue */\n// In Dart, the Queue class is a deque and can also be used as a queue\nQueue<int> queue = Queue();\n\n/* Enqueue elements */\nqueue.add(1);\nqueue.add(3);\nqueue.add(2);\nqueue.add(5);\nqueue.add(4);\n\n/* Access front element */\nint peek = queue.first;\n\n/* Dequeue element */\nint pop = queue.removeFirst();\n\n/* Get queue length */\nint size = queue.length;\n\n/* Check if queue is empty */\nbool isEmpty = queue.isEmpty;\n
queue.rs
/* Initialize deque */\n// In Rust, use deque as a regular queue\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* Enqueue elements */\ndeque.push_back(1);\ndeque.push_back(3);\ndeque.push_back(2);\ndeque.push_back(5);\ndeque.push_back(4);\n\n/* Access front element */\nif let Some(front) = deque.front() {\n}\n\n/* Dequeue element */\nif let Some(pop) = deque.pop_front() {\n}\n\n/* Get queue length */\nlet size = deque.len();\n\n/* Check if queue is empty */\nlet is_empty = deque.is_empty();\n
queue.c
// C does not provide a built-in queue\n
queue.kt
/* Initialize queue */\nval queue = LinkedList<Int>()\n\n/* Enqueue elements */\nqueue.offer(1)\nqueue.offer(3)\nqueue.offer(2)\nqueue.offer(5)\nqueue.offer(4)\n\n/* Access front element */\nval peek = queue.peek()\n\n/* Dequeue element */\nval pop = queue.poll()\n\n/* Get queue length */\nval size = queue.size\n\n/* Check if queue is empty */\nval isEmpty = queue.isEmpty()\n
queue.rb
# Initialize queue\n# Ruby's built-in queue (Thread::Queue) does not have peek and traversal methods, can use Array as a queue\nqueue = []\n\n# Enqueue elements\nqueue.push(1)\nqueue.push(3)\nqueue.push(2)\nqueue.push(5)\nqueue.push(4)\n\n# Access front element\npeek = queue.first\n\n# Dequeue element\n# Please note that since it's an array, Array#shift has O(n) time complexity\npop = queue.shift\n\n# Get queue length\nsize = queue.length\n\n# Check if queue is empty\nis_empty = queue.empty?\n
Code Visualization

Full Screen >

","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#522-queue-implementation","level":2,"title":"5.2.2   Queue Implementation","text":"

To implement a queue, we need a data structure that allows adding elements at one end and removing elements at the other end. Both linked lists and arrays meet this requirement.

","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#1-linked-list-implementation","level":3,"title":"1.   Linked List Implementation","text":"

As shown in Figure 5-5, we can treat the \"head node\" and \"tail node\" of a linked list as the \"front\" and \"rear\" of the queue, respectively, with the rule that nodes can only be added at the rear and removed from the front.

LinkedListQueuepush()pop()

Figure 5-5   Enqueue and dequeue operations in linked list implementation of queue

Below is the code for implementing a queue using a linked list:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_queue.py
class LinkedListQueue:\n    \"\"\"Queue based on linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._front: ListNode | None = None  # Head node front\n        self._rear: ListNode | None = None  # Tail node rear\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Get the length of the queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Enqueue\"\"\"\n        # Add num after the tail node\n        node = ListNode(num)\n        # If the queue is empty, make both front and rear point to the node\n        if self._front is None:\n            self._front = node\n            self._rear = node\n        # If the queue is not empty, add the node after the tail node\n        else:\n            self._rear.next = node\n            self._rear = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Dequeue\"\"\"\n        num = self.peek()\n        # Delete head node\n        self._front = self._front.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Queue is empty\")\n        return self._front.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Convert to list for printing\"\"\"\n        queue = []\n        temp = self._front\n        while temp:\n            queue.append(temp.val)\n            temp = temp.next\n        return queue\n
linkedlist_queue.cpp
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n  private:\n    ListNode *front, *rear; // Head node front, tail node rear\n    int queSize;\n\n  public:\n    LinkedListQueue() {\n        front = nullptr;\n        rear = nullptr;\n        queSize = 0;\n    }\n\n    ~LinkedListQueue() {\n        // Traverse linked list to delete nodes and free memory\n        freeMemoryLinkedList(front);\n    }\n\n    /* Get the length of the queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    void push(int num) {\n        // Add num after the tail node\n        ListNode *node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == nullptr) {\n            front = node;\n            rear = node;\n        }\n        // If the queue is not empty, add the node after the tail node\n        else {\n            rear->next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    int pop() {\n        int num = peek();\n        // Delete head node\n        ListNode *tmp = front;\n        front = front->next;\n        // Free memory\n        delete tmp;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peek() {\n        if (size() == 0)\n            throw out_of_range(\"Queue is empty\");\n        return front->val;\n    }\n\n    /* Convert linked list to Vector and return */\n    vector<int> toVector() {\n        ListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_queue.java
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private ListNode front, rear; // Head node front, tail node rear\n    private int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue */\n    public void push(int num) {\n        // Add num after the tail node\n        ListNode node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node;\n            rear = node;\n        // If the queue is not empty, add the node after the tail node\n        } else {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int pop() {\n        int num = peek();\n        // Delete head node\n        front = front.next;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* Convert linked list to Array and return */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.cs
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n    ListNode? front, rear;  // Head node front, tail node rear\n    int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* Get the length of the queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Enqueue */\n    public void Push(int num) {\n        // Add num after the tail node\n        ListNode node = new(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node;\n            rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else if (rear != null) {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int Pop() {\n        int num = Peek();\n        // Delete head node\n        front = front?.next;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return front!.val;\n    }\n\n    /* Convert linked list to Array and return */\n    public int[] ToArray() {\n        if (front == null)\n            return [];\n\n        ListNode? node = front;\n        int[] res = new int[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.go
/* Queue based on linked list implementation */\ntype linkedListQueue struct {\n    // Use built-in package list to implement queue\n    data *list.List\n}\n\n/* Access front of the queue element */\nfunc newLinkedListQueue() *linkedListQueue {\n    return &linkedListQueue{\n        data: list.New(),\n    }\n}\n\n/* Enqueue */\nfunc (s *linkedListQueue) push(value any) {\n    s.data.PushBack(value)\n}\n\n/* Dequeue */\nfunc (s *linkedListQueue) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Return list for printing */\nfunc (s *linkedListQueue) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* Get the length of the queue */\nfunc (s *linkedListQueue) size() int {\n    return s.data.Len()\n}\n\n/* Check if the queue is empty */\nfunc (s *linkedListQueue) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListQueue) toList() *list.List {\n    return s.data\n}\n
linkedlist_queue.swift
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private var front: ListNode? // Head node\n    private var rear: ListNode? // Tail node\n    private var _size: Int\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue */\n    func push(num: Int) {\n        // Add num after the tail node\n        let node = ListNode(x: num)\n        // If the queue is empty, make both front and rear point to the node\n        if front == nil {\n            front = node\n            rear = node\n        }\n        // If the queue is not empty, add the node after the tail node\n        else {\n            rear?.next = node\n            rear = node\n        }\n        _size += 1\n    }\n\n    /* Dequeue */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Delete head node\n        front = front?.next\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Queue is empty\")\n        }\n        return front!.val\n    }\n\n    /* Convert linked list to Array and return */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_queue.js
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n    #front; // Front node #front\n    #rear; // Rear node #rear\n    #queSize = 0;\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n    }\n\n    /* Get the length of the queue */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Enqueue */\n    push(num) {\n        // Add num after the tail node\n        const node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (!this.#front) {\n            this.#front = node;\n            this.#rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            this.#rear.next = node;\n            this.#rear = node;\n        }\n        this.#queSize++;\n    }\n\n    /* Dequeue */\n    pop() {\n        const num = this.peek();\n        // Delete head node\n        this.#front = this.#front.next;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (this.size === 0) throw new Error('Queue is empty');\n        return this.#front.val;\n    }\n\n    /* Convert linked list to Array and return */\n    toArray() {\n        let node = this.#front;\n        const res = new Array(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.ts
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n    private front: ListNode | null; // Head node front\n    private rear: ListNode | null; // Tail node rear\n    private queSize: number = 0;\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n    }\n\n    /* Get the length of the queue */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Enqueue */\n    push(num: number): void {\n        // Add num after the tail node\n        const node = new ListNode(num);\n        // If the queue is empty, make both front and rear point to the node\n        if (!this.front) {\n            this.front = node;\n            this.rear = node;\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            this.rear!.next = node;\n            this.rear = node;\n        }\n        this.queSize++;\n    }\n\n    /* Dequeue */\n    pop(): number {\n        const num = this.peek();\n        if (!this.front) throw new Error('Queue is empty');\n        // Delete head node\n        this.front = this.front.next;\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (this.size === 0) throw new Error('Queue is empty');\n        return this.front!.val;\n    }\n\n    /* Convert linked list to Array and return */\n    toArray(): number[] {\n        let node = this.front;\n        const res = new Array<number>(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.dart
/* Queue based on linked list implementation */\nclass LinkedListQueue {\n  ListNode? _front; // Head node _front\n  ListNode? _rear; // Tail node _rear\n  int _queSize = 0; // Queue length\n\n  LinkedListQueue() {\n    _front = null;\n    _rear = null;\n  }\n\n  /* Get the length of the queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Enqueue */\n  void push(int _num) {\n    // Add _num after tail node\n    final node = ListNode(_num);\n    // If the queue is empty, make both front and rear point to the node\n    if (_front == null) {\n      _front = node;\n      _rear = node;\n    } else {\n      // If the queue is not empty, add the node after the tail node\n      _rear!.next = node;\n      _rear = node;\n    }\n    _queSize++;\n  }\n\n  /* Dequeue */\n  int pop() {\n    final int _num = peek();\n    // Delete head node\n    _front = _front!.next;\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (_queSize == 0) {\n      throw Exception('Queue is empty');\n    }\n    return _front!.val;\n  }\n\n  /* Convert linked list to Array and return */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> queue = [];\n    while (node != null) {\n      queue.add(node.val);\n      node = node.next;\n    }\n    return queue;\n  }\n}\n
linkedlist_queue.rs
/* Queue based on linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListQueue<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // Head node front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // Tail node rear\n    que_size: usize,                         // Queue length\n}\n\nimpl<T: Copy> LinkedListQueue<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* Get the length of the queue */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* Check if the queue is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* Enqueue */\n    pub fn push(&mut self, num: T) {\n        // Add num after the tail node\n        let new_rear = ListNode::new(num);\n        match self.rear.take() {\n            // If the queue is not empty, add the node after the tail node\n            Some(old_rear) => {\n                old_rear.borrow_mut().next = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n            // If the queue is empty, make both front and rear point to the node\n            None => {\n                self.front = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n        }\n        self.que_size += 1;\n    }\n\n    /* Dequeue */\n    pub fn pop(&mut self) -> Option<T> {\n        self.front.take().map(|old_front| {\n            match old_front.borrow_mut().next.take() {\n                Some(new_front) => {\n                    self.front = Some(new_front);\n                }\n                None => {\n                    self.rear.take();\n                }\n            }\n            self.que_size -= 1;\n            old_front.borrow().val\n        })\n    }\n\n    /* Return list for printing */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* Convert linked list to Array and return */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n\n        res\n    }\n}\n
linkedlist_queue.c
/* Queue based on linked list implementation */\ntypedef struct {\n    ListNode *front, *rear;\n    int queSize;\n} LinkedListQueue;\n\n/* Constructor */\nLinkedListQueue *newLinkedListQueue() {\n    LinkedListQueue *queue = (LinkedListQueue *)malloc(sizeof(LinkedListQueue));\n    queue->front = NULL;\n    queue->rear = NULL;\n    queue->queSize = 0;\n    return queue;\n}\n\n/* Destructor */\nvoid delLinkedListQueue(LinkedListQueue *queue) {\n    // Free all nodes\n    while (queue->front != NULL) {\n        ListNode *tmp = queue->front;\n        queue->front = queue->front->next;\n        free(tmp);\n    }\n    // Free queue structure\n    free(queue);\n}\n\n/* Get the length of the queue */\nint size(LinkedListQueue *queue) {\n    return queue->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(LinkedListQueue *queue) {\n    return (size(queue) == 0);\n}\n\n/* Enqueue */\nvoid push(LinkedListQueue *queue, int num) {\n    // Add node at tail\n    ListNode *node = newListNode(num);\n    // If the queue is empty, make both front and rear point to the node\n    if (queue->front == NULL) {\n        queue->front = node;\n        queue->rear = node;\n    }\n    // If the queue is not empty, add the node after the tail node\n    else {\n        queue->rear->next = node;\n        queue->rear = node;\n    }\n    queue->queSize++;\n}\n\n/* Return list for printing */\nint peek(LinkedListQueue *queue) {\n    assert(size(queue) && queue->front);\n    return queue->front->val;\n}\n\n/* Dequeue */\nint pop(LinkedListQueue *queue) {\n    int num = peek(queue);\n    ListNode *tmp = queue->front;\n    queue->front = queue->front->next;\n    free(tmp);\n    queue->queSize--;\n    return num;\n}\n\n/* Print queue */\nvoid printLinkedListQueue(LinkedListQueue *queue) {\n    int *arr = malloc(sizeof(int) * queue->queSize);\n    // Copy data from list to array\n    int i;\n    ListNode *node;\n    for (i = 0, node = queue->front; i < queue->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, queue->queSize);\n    free(arr);\n}\n
linkedlist_queue.kt
/* Queue based on linked list implementation */\nclass LinkedListQueue(\n    // Head node front, tail node rear\n    private var front: ListNode? = null,\n    private var rear: ListNode? = null,\n    private var queSize: Int = 0\n) {\n\n    /* Get the length of the queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the queue is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Enqueue */\n    fun push(num: Int) {\n        // Add num after the tail node\n        val node = ListNode(num)\n        // If the queue is empty, make both front and rear point to the node\n        if (front == null) {\n            front = node\n            rear = node\n            // If the queue is not empty, add the node after the tail node\n        } else {\n            rear?.next = node\n            rear = node\n        }\n        queSize++\n    }\n\n    /* Dequeue */\n    fun pop(): Int {\n        val num = peek()\n        // Delete head node\n        front = front?.next\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* Convert linked list to Array and return */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_queue.rb
### Queue based on linked list ###\nclass LinkedListQueue\n  ### Get queue length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @front = nil  # Head node front\n    @rear = nil   # Tail node rear\n    @size = 0\n  end\n\n  ### Check if queue is empty ###\n  def is_empty?\n    @front.nil?\n  end\n\n  ### Enqueue ###\n  def push(num)\n    # Add num after the tail node\n    node = ListNode.new(num)\n\n    # If queue is empty, set both front and rear to this node\n    if @front.nil?\n      @front = node\n      @rear = node\n    # If queue is not empty, add this node after rear\n    else\n      @rear.next = node\n      @rear = node\n    end\n\n    @size += 1\n  end\n\n  ### Dequeue ###\n  def pop\n    num = peek\n    # Delete head node\n    @front = @front.next\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek\n    raise IndexError, 'Queue is empty' if is_empty?\n\n    @front.val\n  end\n\n  ### Convert linked list to Array and return ###\n  def to_array\n    queue = []\n    temp = @front\n    while temp\n      queue << temp.val\n      temp = temp.next\n    end\n    queue\n  end\nend\n
","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

Deleting the first element in an array has a time complexity of \\(O(n)\\), which would make the dequeue operation inefficient. However, we can use the following clever method to avoid this problem.

We can use a variable front to point to the index of the front element and maintain a variable size to record the queue length. We define rear = front + size, which calculates the position right after the rear element.

Based on this design, the valid interval containing elements in the array is [front, rear - 1]. The implementation methods for various operations are shown in Figure 5-6:

  • Enqueue operation: Assign the input element to the rear index and increase size by 1.
  • Dequeue operation: Simply increase front by 1 and decrease size by 1.

As you can see, both enqueue and dequeue operations require only one operation, with a time complexity of \\(O(1)\\).

ArrayQueuepush()pop()

Figure 5-6   Enqueue and dequeue operations in array implementation of queue

You may notice a problem: as we continuously enqueue and dequeue, both front and rear move to the right. When they reach the end of the array, they cannot continue moving. To solve this problem, we can treat the array as a \"circular array\" with head and tail connected.

For a circular array, we need to let front or rear wrap around to the beginning of the array when they cross the end. This periodic pattern can be implemented using the \"modulo operation,\" as shown in the code below:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_queue.py
class ArrayQueue:\n    \"\"\"Queue based on circular array implementation\"\"\"\n\n    def __init__(self, size: int):\n        \"\"\"Constructor\"\"\"\n        self._nums: list[int] = [0] * size  # Array for storing queue elements\n        self._front: int = 0  # Front pointer, points to the front of the queue element\n        self._size: int = 0  # Queue length\n\n    def capacity(self) -> int:\n        \"\"\"Get the capacity of the queue\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"Get the length of the queue\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the queue is empty\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"Enqueue\"\"\"\n        if self._size == self.capacity():\n            raise IndexError(\"Queue is full\")\n        # Calculate rear pointer, points to rear index + 1\n        # Use modulo operation to wrap rear around to the head after passing the tail of the array\n        rear: int = (self._front + self._size) % self.capacity()\n        # Add num to the rear of the queue\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Dequeue\"\"\"\n        num: int = self.peek()\n        # Front pointer moves one position backward, if it passes the tail, return to the head of the array\n        self._front = (self._front + 1) % self.capacity()\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access front of the queue element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Queue is empty\")\n        return self._nums[self._front]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Return list for printing\"\"\"\n        res = [0] * self.size()\n        j: int = self._front\n        for i in range(self.size()):\n            res[i] = self._nums[(j % self.capacity())]\n            j += 1\n        return res\n
array_queue.cpp
/* Queue based on circular array implementation */\nclass ArrayQueue {\n  private:\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Queue length\n    int queCapacity; // Queue capacity\n\n  public:\n    ArrayQueue(int capacity) {\n        // Initialize array\n        nums = new int[capacity];\n        queCapacity = capacity;\n        front = queSize = 0;\n    }\n\n    ~ArrayQueue() {\n        delete[] nums;\n    }\n\n    /* Get the capacity of the queue */\n    int capacity() {\n        return queCapacity;\n    }\n\n    /* Get the length of the queue */\n    int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Enqueue */\n    void push(int num) {\n        if (queSize == queCapacity) {\n            cout << \"Queue is full\" << endl;\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % queCapacity;\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    int pop() {\n        int num = peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % queCapacity;\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int peek() {\n        if (isEmpty())\n            throw out_of_range(\"Queue is empty\");\n        return nums[front];\n    }\n\n    /* Convert array to Vector and return */\n    vector<int> toVector() {\n        // Elements enqueue\n        vector<int> arr(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            arr[i] = nums[j % queCapacity];\n        }\n        return arr;\n    }\n};\n
array_queue.java
/* Queue based on circular array implementation */\nclass ArrayQueue {\n    private int[] nums; // Array for storing queue elements\n    private int front; // Front pointer, points to the front of the queue element\n    private int queSize; // Queue length\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* Get the length of the queue */\n    public int size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    public void push(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % capacity();\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int pop() {\n        int num = peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* Return array */\n    public int[] toArray() {\n        // Elements enqueue\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % capacity()];\n        }\n        return res;\n    }\n}\n
array_queue.cs
/* Queue based on circular array implementation */\nclass ArrayQueue {\n    int[] nums;  // Array for storing queue elements\n    int front;   // Front pointer, points to the front of the queue element\n    int queSize; // Queue length\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* Get the length of the queue */\n    public int Size() {\n        return queSize;\n    }\n\n    /* Check if the queue is empty */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* Enqueue */\n    public void Push(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        int rear = (front + queSize) % Capacity();\n        // Front pointer moves one position backward\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* Dequeue */\n    public int Pop() {\n        int num = Peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % Capacity();\n        queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return nums[front];\n    }\n\n    /* Return array */\n    public int[] ToArray() {\n        // Elements enqueue\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % this.Capacity()];\n        }\n        return res;\n    }\n}\n
array_queue.go
/* Queue based on circular array implementation */\ntype arrayQueue struct {\n    nums        []int // Array for storing queue elements\n    front       int   // Front pointer, points to the front of the queue element\n    queSize     int   // Queue length\n    queCapacity int   // Queue capacity (maximum number of elements)\n}\n\n/* Access front of the queue element */\nfunc newArrayQueue(queCapacity int) *arrayQueue {\n    return &arrayQueue{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* Get the length of the queue */\nfunc (q *arrayQueue) size() int {\n    return q.queSize\n}\n\n/* Check if the queue is empty */\nfunc (q *arrayQueue) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* Enqueue */\nfunc (q *arrayQueue) push(num int) {\n    // When rear == queCapacity, queue is full\n    if q.queSize == q.queCapacity {\n        return\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    rear := (q.front + q.queSize) % q.queCapacity\n    // Front pointer moves one position backward\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* Dequeue */\nfunc (q *arrayQueue) pop() any {\n    num := q.peek()\n    if num == nil {\n        return nil\n    }\n\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    q.front = (q.front + 1) % q.queCapacity\n    q.queSize--\n    return num\n}\n\n/* Return list for printing */\nfunc (q *arrayQueue) peek() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* Get Slice for printing */\nfunc (q *arrayQueue) toSlice() []int {\n    rear := (q.front + q.queSize)\n    if rear >= q.queCapacity {\n        rear %= q.queCapacity\n        return append(q.nums[q.front:], q.nums[:rear]...)\n    }\n    return q.nums[q.front:rear]\n}\n
array_queue.swift
/* Queue based on circular array implementation */\nclass ArrayQueue {\n    private var nums: [Int] // Array for storing queue elements\n    private var front: Int // Front pointer, points to the front of the queue element\n    private var _size: Int // Queue length\n\n    init(capacity: Int) {\n        // Initialize array\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* Get the capacity of the queue */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* Get the length of the queue */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the queue is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Enqueue */\n    func push(num: Int) {\n        if size() == capacity() {\n            print(\"Queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        let rear = (front + size()) % capacity()\n        // Front pointer moves one position backward\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* Dequeue */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity()\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Queue is empty\")\n        }\n        return nums[front]\n    }\n\n    /* Return array */\n    func toArray() -> [Int] {\n        // Elements enqueue\n        (front ..< front + size()).map { nums[$0 % capacity()] }\n    }\n}\n
array_queue.js
/* Queue based on circular array implementation */\nclass ArrayQueue {\n    #nums; // Array for storing queue elements\n    #front = 0; // Front pointer, points to the front of the queue element\n    #queSize = 0; // Queue length\n\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n    }\n\n    /* Get the capacity of the queue */\n    get capacity() {\n        return this.#nums.length;\n    }\n\n    /* Get the length of the queue */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* Enqueue */\n    push(num) {\n        if (this.size === this.capacity) {\n            console.log('Queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        const rear = (this.#front + this.size) % this.capacity;\n        // Front pointer moves one position backward\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* Dequeue */\n    pop() {\n        const num = this.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        this.#front = (this.#front + 1) % this.capacity;\n        this.#queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (this.isEmpty()) throw new Error('Queue is empty');\n        return this.#nums[this.#front];\n    }\n\n    /* Return Array */\n    toArray() {\n        // Elements enqueue\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.#front; i < this.size; i++, j++) {\n            arr[i] = this.#nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
array_queue.ts
/* Queue based on circular array implementation */\nclass ArrayQueue {\n    private nums: number[]; // Array for storing queue elements\n    private front: number; // Front pointer, points to the front of the queue element\n    private queSize: number; // Queue length\n\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = this.queSize = 0;\n    }\n\n    /* Get the capacity of the queue */\n    get capacity(): number {\n        return this.nums.length;\n    }\n\n    /* Get the length of the queue */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* Check if the queue is empty */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* Enqueue */\n    push(num: number): void {\n        if (this.size === this.capacity) {\n            console.log('Queue is full');\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        const rear = (this.front + this.queSize) % this.capacity;\n        // Front pointer moves one position backward\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* Dequeue */\n    pop(): number {\n        const num = this.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        this.front = (this.front + 1) % this.capacity;\n        this.queSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (this.isEmpty()) throw new Error('Queue is empty');\n        return this.nums[this.front];\n    }\n\n    /* Return Array */\n    toArray(): number[] {\n        // Elements enqueue\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.front; i < this.size; i++, j++) {\n            arr[i] = this.nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
array_queue.dart
/* Queue based on circular array implementation */\nclass ArrayQueue {\n  late List<int> _nums; // Array for storing queue elements\n  late int _front; // Front pointer, points to the front of the queue element\n  late int _queSize; // Queue length\n\n  ArrayQueue(int capacity) {\n    _nums = List.filled(capacity, 0);\n    _front = _queSize = 0;\n  }\n\n  /* Get the capacity of the queue */\n  int capaCity() {\n    return _nums.length;\n  }\n\n  /* Get the length of the queue */\n  int size() {\n    return _queSize;\n  }\n\n  /* Check if the queue is empty */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* Enqueue */\n  void push(int _num) {\n    if (_queSize == capaCity()) {\n      throw Exception(\"Queue is full\");\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    int rear = (_front + _queSize) % capaCity();\n    // Add _num to queue rear\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* Dequeue */\n  int pop() {\n    int _num = peek();\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    _front = (_front + 1) % capaCity();\n    _queSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"Queue is empty\");\n    }\n    return _nums[_front];\n  }\n\n  /* Return Array */\n  List<int> toArray() {\n    // Elements enqueue\n    final List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[j % capaCity()];\n    }\n    return res;\n  }\n}\n
array_queue.rs
/* Queue based on circular array implementation */\nstruct ArrayQueue<T> {\n    nums: Vec<T>,      // Array for storing queue elements\n    front: i32,        // Front pointer, points to the front of the queue element\n    que_size: i32,     // Queue length\n    que_capacity: i32, // Queue capacity\n}\n\nimpl<T: Copy + Default> ArrayQueue<T> {\n    /* Constructor */\n    fn new(capacity: i32) -> ArrayQueue<T> {\n        ArrayQueue {\n            nums: vec![T::default(); capacity as usize],\n            front: 0,\n            que_size: 0,\n            que_capacity: capacity,\n        }\n    }\n\n    /* Get the capacity of the queue */\n    fn capacity(&self) -> i32 {\n        self.que_capacity\n    }\n\n    /* Get the length of the queue */\n    fn size(&self) -> i32 {\n        self.que_size\n    }\n\n    /* Check if the queue is empty */\n    fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* Enqueue */\n    fn push(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"Queue is full\");\n            return;\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        let rear = (self.front + self.que_size) % self.que_capacity;\n        // Front pointer moves one position backward\n        self.nums[rear as usize] = num;\n        self.que_size += 1;\n    }\n\n    /* Dequeue */\n    fn pop(&mut self) -> T {\n        let num = self.peek();\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        self.front = (self.front + 1) % self.que_capacity;\n        self.que_size -= 1;\n        num\n    }\n\n    /* Return list for printing */\n    fn peek(&self) -> T {\n        if self.is_empty() {\n            panic!(\"index out of bounds\");\n        }\n        self.nums[self.front as usize]\n    }\n\n    /* Return array */\n    fn to_vector(&self) -> Vec<T> {\n        let cap = self.que_capacity;\n        let mut j = self.front;\n        let mut arr = vec![T::default(); cap as usize];\n        for i in 0..self.que_size {\n            arr[i as usize] = self.nums[(j % cap) as usize];\n            j += 1;\n        }\n        arr\n    }\n}\n
array_queue.c
/* Queue based on circular array implementation */\ntypedef struct {\n    int *nums;       // Array for storing queue elements\n    int front;       // Front pointer, points to the front of the queue element\n    int queSize;     // Current number of elements in the queue\n    int queCapacity; // Queue capacity\n} ArrayQueue;\n\n/* Constructor */\nArrayQueue *newArrayQueue(int capacity) {\n    ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue));\n    // Initialize array\n    queue->queCapacity = capacity;\n    queue->nums = (int *)malloc(sizeof(int) * queue->queCapacity);\n    queue->front = queue->queSize = 0;\n    return queue;\n}\n\n/* Destructor */\nvoid delArrayQueue(ArrayQueue *queue) {\n    free(queue->nums);\n    free(queue);\n}\n\n/* Get the capacity of the queue */\nint capacity(ArrayQueue *queue) {\n    return queue->queCapacity;\n}\n\n/* Get the length of the queue */\nint size(ArrayQueue *queue) {\n    return queue->queSize;\n}\n\n/* Check if the queue is empty */\nbool empty(ArrayQueue *queue) {\n    return queue->queSize == 0;\n}\n\n/* Return list for printing */\nint peek(ArrayQueue *queue) {\n    assert(size(queue) != 0);\n    return queue->nums[queue->front];\n}\n\n/* Enqueue */\nvoid push(ArrayQueue *queue, int num) {\n    if (size(queue) == capacity(queue)) {\n        printf(\"Queue is full\\r\\n\");\n        return;\n    }\n    // Use modulo operation to wrap rear around to the head after passing the tail of the array\n    // Add num to the rear of the queue\n    int rear = (queue->front + queue->queSize) % queue->queCapacity;\n    // Front pointer moves one position backward\n    queue->nums[rear] = num;\n    queue->queSize++;\n}\n\n/* Dequeue */\nint pop(ArrayQueue *queue) {\n    int num = peek(queue);\n    // Move front pointer backward by one position, if it passes the tail, return to array head\n    queue->front = (queue->front + 1) % queue->queCapacity;\n    queue->queSize--;\n    return num;\n}\n\n/* Return array for printing */\nint *toArray(ArrayQueue *queue, int *queSize) {\n    *queSize = queue->queSize;\n    int *res = (int *)calloc(queue->queSize, sizeof(int));\n    int j = queue->front;\n    for (int i = 0; i < queue->queSize; i++) {\n        res[i] = queue->nums[j % queue->queCapacity];\n        j++;\n    }\n    return res;\n}\n
array_queue.kt
/* Queue based on circular array implementation */\nclass ArrayQueue(capacity: Int) {\n    private val nums: IntArray = IntArray(capacity) // Array for storing queue elements\n    private var front: Int = 0 // Front pointer, points to the front of the queue element\n    private var queSize: Int = 0 // Queue length\n\n    /* Get the capacity of the queue */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* Get the length of the queue */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* Check if the queue is empty */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* Enqueue */\n    fun push(num: Int) {\n        if (queSize == capacity()) {\n            println(\"Queue is full\")\n            return\n        }\n        // Use modulo operation to wrap rear around to the head after passing the tail of the array\n        // Add num to the rear of the queue\n        val rear = (front + queSize) % capacity()\n        // Front pointer moves one position backward\n        nums[rear] = num\n        queSize++\n    }\n\n    /* Dequeue */\n    fun pop(): Int {\n        val num = peek()\n        // Move front pointer backward by one position, if it passes the tail, return to array head\n        front = (front + 1) % capacity()\n        queSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* Return array */\n    fun toArray(): IntArray {\n        // Elements enqueue\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[j % capacity()]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
array_queue.rb
### Queue based on circular array ###\nclass ArrayQueue\n  ### Get queue length ###\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize(size)\n    @nums = Array.new(size, 0) # Array for storing queue elements\n    @front = 0 # Front pointer, points to the front of the queue element\n    @size = 0 # Queue length\n  end\n\n  ### Get queue capacity ###\n  def capacity\n    @nums.length\n  end\n\n  ### Check if queue is empty ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### Enqueue ###\n  def push(num)\n    raise IndexError, 'Queue is full' if size == capacity\n\n    # Use modulo operation to wrap rear around to the head after passing the tail of the array\n    # Add num to the rear of the queue\n    rear = (@front + size) % capacity\n    # Front pointer moves one position backward\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### Dequeue ###\n  def pop\n    num = peek\n    # Move front pointer backward by one position, if it passes the tail, return to array head\n    @front = (@front + 1) % capacity\n    @size -= 1\n    num\n  end\n\n  ### Access front element ###\n  def peek\n    raise IndexError, 'Queue is empty' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### Return list for printing ###\n  def to_array\n    res = Array.new(size, 0)\n    j = @front\n\n    for i in 0...size\n      res[i] = @nums[j % capacity]\n      j += 1\n    end\n\n    res\n  end\nend\n

The queue implemented above still has limitations: its length is immutable. However, this problem is not difficult to solve. We can replace the array with a dynamic array to introduce an expansion mechanism. Interested readers can try to implement this themselves.

The comparison conclusions for the two implementations are consistent with those for stacks and will not be repeated here.

","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#523-typical-applications-of-queue","level":2,"title":"5.2.3   Typical Applications of Queue","text":"
  • Taobao orders. After shoppers place orders, the orders are added to a queue, and the system subsequently processes the orders in the queue according to their sequence. During Double Eleven, massive orders are generated in a short time, and high concurrency becomes a key challenge that engineers need to tackle.
  • Various to-do tasks. Any scenario that needs to implement \"first come, first served\" functionality, such as a printer's task queue or a restaurant's order queue, can effectively maintain the processing order using queues.
","path":["Chapter 5. Stack and Queue","5.2   Queue"],"tags":[]},{"location":"chapter_stack_and_queue/stack/","level":1,"title":"5.1   Stack","text":"

A stack is a linear data structure that follows the Last In First Out (LIFO) logic.

We can compare a stack to a pile of plates on a table. If we specify that only one plate can be moved at a time, then to get the bottom plate, we must first remove the plates above it one by one. If we replace the plates with various types of elements (such as integers, characters, objects, etc.), we get the stack data structure.

As shown in Figure 5-1, we call the top of the stacked elements the \"top\" and the bottom the \"base.\" The operation of adding an element to the top is called \"push,\" and the operation of removing the top element is called \"pop.\"

Figure 5-1   LIFO rule of stack

","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511-common-stack-operations","level":2,"title":"5.1.1   Common Stack Operations","text":"

The common operations on a stack are shown in Table 5-1. The specific method names depend on the programming language used. Here, we use the common naming convention of push(), pop(), and peek().

Table 5-1   Efficiency of Stack Operations

Method Description Time Complexity push() Push element onto stack (add to top) \\(O(1)\\) pop() Pop top element from stack \\(O(1)\\) peek() Access top element \\(O(1)\\)

Typically, we can directly use the built-in stack class provided by the programming language. However, some languages may not provide a dedicated stack class. In these cases, we can use the language's \"array\" or \"linked list\" as a stack and ignore operations unrelated to the stack in the program logic.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby stack.py
# Initialize stack\n# Python does not have a built-in stack class, can use list as a stack\nstack: list[int] = []\n\n# Push elements\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n# Access top element\npeek: int = stack[-1]\n\n# Pop element\npop: int = stack.pop()\n\n# Get stack length\nsize: int = len(stack)\n\n# Check if empty\nis_empty: bool = len(stack) == 0\n
stack.cpp
/* Initialize stack */\nstack<int> stack;\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nint top = stack.top();\n\n/* Pop element */\nstack.pop(); // No return value\n\n/* Get stack length */\nint size = stack.size();\n\n/* Check if empty */\nbool empty = stack.empty();\n
stack.java
/* Initialize stack */\nStack<Integer> stack = new Stack<>();\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nint peek = stack.peek();\n\n/* Pop element */\nint pop = stack.pop();\n\n/* Get stack length */\nint size = stack.size();\n\n/* Check if empty */\nboolean isEmpty = stack.isEmpty();\n
stack.cs
/* Initialize stack */\nStack<int> stack = new();\n\n/* Push elements */\nstack.Push(1);\nstack.Push(3);\nstack.Push(2);\nstack.Push(5);\nstack.Push(4);\n\n/* Access top element */\nint peek = stack.Peek();\n\n/* Pop element */\nint pop = stack.Pop();\n\n/* Get stack length */\nint size = stack.Count;\n\n/* Check if empty */\nbool isEmpty = stack.Count == 0;\n
stack_test.go
/* Initialize stack */\n// In Go, it is recommended to use Slice as a stack\nvar stack []int\n\n/* Push elements */\nstack = append(stack, 1)\nstack = append(stack, 3)\nstack = append(stack, 2)\nstack = append(stack, 5)\nstack = append(stack, 4)\n\n/* Access top element */\npeek := stack[len(stack)-1]\n\n/* Pop element */\npop := stack[len(stack)-1]\nstack = stack[:len(stack)-1]\n\n/* Get stack length */\nsize := len(stack)\n\n/* Check if empty */\nisEmpty := len(stack) == 0\n
stack.swift
/* Initialize stack */\n// Swift does not have a built-in stack class, can use Array as a stack\nvar stack: [Int] = []\n\n/* Push elements */\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n/* Access top element */\nlet peek = stack.last!\n\n/* Pop element */\nlet pop = stack.removeLast()\n\n/* Get stack length */\nlet size = stack.count\n\n/* Check if empty */\nlet isEmpty = stack.isEmpty\n
stack.js
/* Initialize stack */\n// JavaScript does not have a built-in stack class, can use Array as a stack\nconst stack = [];\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nconst peek = stack[stack.length-1];\n\n/* Pop element */\nconst pop = stack.pop();\n\n/* Get stack length */\nconst size = stack.length;\n\n/* Check if empty */\nconst is_empty = stack.length === 0;\n
stack.ts
/* Initialize stack */\n// TypeScript does not have a built-in stack class, can use Array as a stack\nconst stack: number[] = [];\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nconst peek = stack[stack.length - 1];\n\n/* Pop element */\nconst pop = stack.pop();\n\n/* Get stack length */\nconst size = stack.length;\n\n/* Check if empty */\nconst is_empty = stack.length === 0;\n
stack.dart
/* Initialize stack */\n// Dart does not have a built-in stack class, can use List as a stack\nList<int> stack = [];\n\n/* Push elements */\nstack.add(1);\nstack.add(3);\nstack.add(2);\nstack.add(5);\nstack.add(4);\n\n/* Access top element */\nint peek = stack.last;\n\n/* Pop element */\nint pop = stack.removeLast();\n\n/* Get stack length */\nint size = stack.length;\n\n/* Check if empty */\nbool isEmpty = stack.isEmpty;\n
stack.rs
/* Initialize stack */\n// Use Vec as a stack\nlet mut stack: Vec<i32> = Vec::new();\n\n/* Push elements */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* Access top element */\nlet top = stack.last().unwrap();\n\n/* Pop element */\nlet pop = stack.pop().unwrap();\n\n/* Get stack length */\nlet size = stack.len();\n\n/* Check if empty */\nlet is_empty = stack.is_empty();\n
stack.c
// C does not provide a built-in stack\n
stack.kt
/* Initialize stack */\nval stack = Stack<Int>()\n\n/* Push elements */\nstack.push(1)\nstack.push(3)\nstack.push(2)\nstack.push(5)\nstack.push(4)\n\n/* Access top element */\nval peek = stack.peek()\n\n/* Pop element */\nval pop = stack.pop()\n\n/* Get stack length */\nval size = stack.size\n\n/* Check if empty */\nval isEmpty = stack.isEmpty()\n
stack.rb
# Initialize stack\n# Ruby does not have a built-in stack class, can use Array as a stack\nstack = []\n\n# Push elements\nstack << 1\nstack << 3\nstack << 2\nstack << 5\nstack << 4\n\n# Access top element\npeek = stack.last\n\n# Pop element\npop = stack.pop\n\n# Get stack length\nsize = stack.length\n\n# Check if empty\nis_empty = stack.empty?\n
Code Visualization

Full Screen >

","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512-stack-implementation","level":2,"title":"5.1.2   Stack Implementation","text":"

To gain a deeper understanding of how a stack operates, let's try implementing a stack class ourselves.

A stack follows the LIFO principle, so we can only add or remove elements at the top. However, both arrays and linked lists allow adding and removing elements at any position. Therefore, a stack can be viewed as a restricted array or linked list. In other words, we can \"shield\" some irrelevant operations of arrays or linked lists so that their external logic conforms to the characteristics of a stack.

","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1-linked-list-implementation","level":3,"title":"1.   Linked List Implementation","text":"

When implementing a stack using a linked list, we can treat the head node of the linked list as the top of the stack and the tail node as the base.

As shown in Figure 5-2, for the push operation, we simply insert an element at the head of the linked list. This node insertion method is called the \"head insertion method.\" For the pop operation, we just need to remove the head node from the linked list.

LinkedListStackpush()pop()

Figure 5-2   Push and pop operations in linked list implementation of stack

Below is sample code for implementing a stack based on a linked list:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_stack.py
class LinkedListStack:\n    \"\"\"Stack based on linked list implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._peek: ListNode | None = None\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"Get the length of the stack\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the stack is empty\"\"\"\n        return self._size == 0\n\n    def push(self, val: int):\n        \"\"\"Push\"\"\"\n        node = ListNode(val)\n        node.next = self._peek\n        self._peek = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"Pop\"\"\"\n        num = self.peek()\n        self._peek = self._peek.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"Access top of the stack element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._peek.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"Convert to list for printing\"\"\"\n        arr = []\n        node = self._peek\n        while node:\n            arr.append(node.val)\n            node = node.next\n        arr.reverse()\n        return arr\n
linkedlist_stack.cpp
/* Stack based on linked list implementation */\nclass LinkedListStack {\n  private:\n    ListNode *stackTop; // Use head node as stack top\n    int stkSize;        // Stack length\n\n  public:\n    LinkedListStack() {\n        stackTop = nullptr;\n        stkSize = 0;\n    }\n\n    ~LinkedListStack() {\n        // Traverse linked list to delete nodes and free memory\n        freeMemoryLinkedList(stackTop);\n    }\n\n    /* Get the length of the stack */\n    int size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    void push(int num) {\n        ListNode *node = new ListNode(num);\n        node->next = stackTop;\n        stackTop = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    int pop() {\n        int num = top();\n        ListNode *tmp = stackTop;\n        stackTop = stackTop->next;\n        // Free memory\n        delete tmp;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"Stack is empty\");\n        return stackTop->val;\n    }\n\n    /* Convert List to Array and return */\n    vector<int> toVector() {\n        ListNode *node = stackTop;\n        vector<int> res(size());\n        for (int i = res.size() - 1; i >= 0; i--) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_stack.java
/* Stack based on linked list implementation */\nclass LinkedListStack {\n    private ListNode stackPeek; // Use head node as stack top\n    private int stkSize = 0; // Stack length\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    public int size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    public void push(int num) {\n        ListNode node = new ListNode(num);\n        node.next = stackPeek;\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    public int pop() {\n        int num = peek();\n        stackPeek = stackPeek.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stackPeek.val;\n    }\n\n    /* Convert List to Array and return */\n    public int[] toArray() {\n        ListNode node = stackPeek;\n        int[] res = new int[size()];\n        for (int i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.cs
/* Stack based on linked list implementation */\nclass LinkedListStack {\n    ListNode? stackPeek;  // Use head node as stack top\n    int stkSize = 0;   // Stack length\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    public int Size() {\n        return stkSize;\n    }\n\n    /* Check if the stack is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Push */\n    public void Push(int num) {\n        ListNode node = new(num) {\n            next = stackPeek\n        };\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* Pop */\n    public int Pop() {\n        int num = Peek();\n        stackPeek = stackPeek!.next;\n        stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stackPeek!.val;\n    }\n\n    /* Convert List to Array and return */\n    public int[] ToArray() {\n        if (stackPeek == null)\n            return [];\n\n        ListNode? node = stackPeek;\n        int[] res = new int[Size()];\n        for (int i = res.Length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.go
/* Stack based on linked list implementation */\ntype linkedListStack struct {\n    // Use built-in package list to implement stack\n    data *list.List\n}\n\n/* Access top of the stack element */\nfunc newLinkedListStack() *linkedListStack {\n    return &linkedListStack{\n        data: list.New(),\n    }\n}\n\n/* Push */\nfunc (s *linkedListStack) push(value int) {\n    s.data.PushBack(value)\n}\n\n/* Pop */\nfunc (s *linkedListStack) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* Return list for printing */\nfunc (s *linkedListStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* Get the length of the stack */\nfunc (s *linkedListStack) size() int {\n    return s.data.Len()\n}\n\n/* Check if the stack is empty */\nfunc (s *linkedListStack) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* Get List for printing */\nfunc (s *linkedListStack) toList() *list.List {\n    return s.data\n}\n
linkedlist_stack.swift
/* Stack based on linked list implementation */\nclass LinkedListStack {\n    private var _peek: ListNode? // Use head node as stack top\n    private var _size: Int // Stack length\n\n    init() {\n        _size = 0\n    }\n\n    /* Get the length of the stack */\n    func size() -> Int {\n        _size\n    }\n\n    /* Check if the stack is empty */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* Push */\n    func push(num: Int) {\n        let node = ListNode(x: num)\n        node.next = _peek\n        _peek = node\n        _size += 1\n    }\n\n    /* Pop */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        _peek = _peek?.next\n        _size -= 1\n        return num\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return _peek!.val\n    }\n\n    /* Convert List to Array and return */\n    func toArray() -> [Int] {\n        var node = _peek\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices.reversed() {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_stack.js
/* Stack based on linked list implementation */\nclass LinkedListStack {\n    #stackPeek; // Use head node as stack top\n    #stkSize = 0; // Stack length\n\n    constructor() {\n        this.#stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    get size() {\n        return this.#stkSize;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* Push */\n    push(num) {\n        const node = new ListNode(num);\n        node.next = this.#stackPeek;\n        this.#stackPeek = node;\n        this.#stkSize++;\n    }\n\n    /* Pop */\n    pop() {\n        const num = this.peek();\n        this.#stackPeek = this.#stackPeek.next;\n        this.#stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek() {\n        if (!this.#stackPeek) throw new Error('Stack is empty');\n        return this.#stackPeek.val;\n    }\n\n    /* Convert linked list to Array and return */\n    toArray() {\n        let node = this.#stackPeek;\n        const res = new Array(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.ts
/* Stack based on linked list implementation */\nclass LinkedListStack {\n    private stackPeek: ListNode | null; // Use head node as stack top\n    private stkSize: number = 0; // Stack length\n\n    constructor() {\n        this.stackPeek = null;\n    }\n\n    /* Get the length of the stack */\n    get size(): number {\n        return this.stkSize;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* Push */\n    push(num: number): void {\n        const node = new ListNode(num);\n        node.next = this.stackPeek;\n        this.stackPeek = node;\n        this.stkSize++;\n    }\n\n    /* Pop */\n    pop(): number {\n        const num = this.peek();\n        if (!this.stackPeek) throw new Error('Stack is empty');\n        this.stackPeek = this.stackPeek.next;\n        this.stkSize--;\n        return num;\n    }\n\n    /* Return list for printing */\n    peek(): number {\n        if (!this.stackPeek) throw new Error('Stack is empty');\n        return this.stackPeek.val;\n    }\n\n    /* Convert linked list to Array and return */\n    toArray(): number[] {\n        let node = this.stackPeek;\n        const res = new Array<number>(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.dart
/* Stack implemented based on linked list class */\nclass LinkedListStack {\n  ListNode? _stackPeek; // Use head node as stack top\n  int _stkSize = 0; // Stack length\n\n  LinkedListStack() {\n    _stackPeek = null;\n  }\n\n  /* Get the length of the stack */\n  int size() {\n    return _stkSize;\n  }\n\n  /* Check if the stack is empty */\n  bool isEmpty() {\n    return _stkSize == 0;\n  }\n\n  /* Push */\n  void push(int _num) {\n    final ListNode node = ListNode(_num);\n    node.next = _stackPeek;\n    _stackPeek = node;\n    _stkSize++;\n  }\n\n  /* Pop */\n  int pop() {\n    final int _num = peek();\n    _stackPeek = _stackPeek!.next;\n    _stkSize--;\n    return _num;\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (_stackPeek == null) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stackPeek!.val;\n  }\n\n  /* Convert linked list to List and return */\n  List<int> toList() {\n    ListNode? node = _stackPeek;\n    List<int> list = [];\n    while (node != null) {\n      list.add(node.val);\n      node = node.next;\n    }\n    list = list.reversed.toList();\n    return list;\n  }\n}\n
linkedlist_stack.rs
/* Stack based on linked list implementation */\n#[allow(dead_code)]\npub struct LinkedListStack<T> {\n    stack_peek: Option<Rc<RefCell<ListNode<T>>>>, // Use head node as stack top\n    stk_size: usize,                              // Stack length\n}\n\nimpl<T: Copy> LinkedListStack<T> {\n    pub fn new() -> Self {\n        Self {\n            stack_peek: None,\n            stk_size: 0,\n        }\n    }\n\n    /* Get the length of the stack */\n    pub fn size(&self) -> usize {\n        return self.stk_size;\n    }\n\n    /* Check if the stack is empty */\n    pub fn is_empty(&self) -> bool {\n        return self.size() == 0;\n    }\n\n    /* Push */\n    pub fn push(&mut self, num: T) {\n        let node = ListNode::new(num);\n        node.borrow_mut().next = self.stack_peek.take();\n        self.stack_peek = Some(node);\n        self.stk_size += 1;\n    }\n\n    /* Pop */\n    pub fn pop(&mut self) -> Option<T> {\n        self.stack_peek.take().map(|old_head| {\n            self.stack_peek = old_head.borrow_mut().next.take();\n            self.stk_size -= 1;\n\n            old_head.borrow().val\n        })\n    }\n\n    /* Return list for printing */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.stack_peek.as_ref()\n    }\n\n    /* Convert List to Array and return */\n    pub fn to_array(&self) -> Vec<T> {\n        fn _to_array<T: Sized + Copy>(head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n            if let Some(node) = head {\n                let mut nums = _to_array(node.borrow().next.as_ref());\n                nums.push(node.borrow().val);\n                return nums;\n            }\n            return Vec::new();\n        }\n\n        _to_array(self.peek())\n    }\n}\n
linkedlist_stack.c
/* Stack based on linked list implementation */\ntypedef struct {\n    ListNode *top; // Use head node as stack top\n    int size;      // Stack length\n} LinkedListStack;\n\n/* Constructor */\nLinkedListStack *newLinkedListStack() {\n    LinkedListStack *s = malloc(sizeof(LinkedListStack));\n    s->top = NULL;\n    s->size = 0;\n    return s;\n}\n\n/* Destructor */\nvoid delLinkedListStack(LinkedListStack *s) {\n    while (s->top) {\n        ListNode *n = s->top->next;\n        free(s->top);\n        s->top = n;\n    }\n    free(s);\n}\n\n/* Get the length of the stack */\nint size(LinkedListStack *s) {\n    return s->size;\n}\n\n/* Check if the stack is empty */\nbool isEmpty(LinkedListStack *s) {\n    return size(s) == 0;\n}\n\n/* Push */\nvoid push(LinkedListStack *s, int num) {\n    ListNode *node = (ListNode *)malloc(sizeof(ListNode));\n    node->next = s->top; // Update new node's pointer field\n    node->val = num;     // Update new node's data field\n    s->top = node;       // Update stack top\n    s->size++;           // Update stack size\n}\n\n/* Return list for printing */\nint peek(LinkedListStack *s) {\n    if (s->size == 0) {\n        printf(\"Stack is empty\\n\");\n        return INT_MAX;\n    }\n    return s->top->val;\n}\n\n/* Pop */\nint pop(LinkedListStack *s) {\n    int val = peek(s);\n    ListNode *tmp = s->top;\n    s->top = s->top->next;\n    // Free memory\n    free(tmp);\n    s->size--;\n    return val;\n}\n
linkedlist_stack.kt
/* Stack based on linked list implementation */\nclass LinkedListStack(\n    private var stackPeek: ListNode? = null, // Use head node as stack top\n    private var stkSize: Int = 0 // Stack length\n) {\n\n    /* Get the length of the stack */\n    fun size(): Int {\n        return stkSize\n    }\n\n    /* Check if the stack is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Push */\n    fun push(num: Int) {\n        val node = ListNode(num)\n        node.next = stackPeek\n        stackPeek = node\n        stkSize++\n    }\n\n    /* Pop */\n    fun pop(): Int? {\n        val num = peek()\n        stackPeek = stackPeek?.next\n        stkSize--\n        return num\n    }\n\n    /* Return list for printing */\n    fun peek(): Int? {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stackPeek?._val\n    }\n\n    /* Convert List to Array and return */\n    fun toArray(): IntArray {\n        var node = stackPeek\n        val res = IntArray(size())\n        for (i in res.size - 1 downTo 0) {\n            res[i] = node?._val!!\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_stack.rb
### Stack based on linked list ###\nclass LinkedListStack\n  attr_reader :size\n\n  ### Constructor ###\n  def initialize\n    @size = 0\n  end\n\n  ### Check if stack is empty ###\n  def is_empty?\n    @peek.nil?\n  end\n\n  ### Push ###\n  def push(val)\n    node = ListNode.new(val)\n    node.next = @peek\n    @peek = node\n    @size += 1\n  end\n\n  ### Pop ###\n  def pop\n    num = peek\n    @peek = @peek.next\n    @size -= 1\n    num\n  end\n\n  ### Access top element ###\n  def peek\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @peek.val\n  end\n\n  ### Convert linked list to Array and return ###\n  def to_array\n    arr = []\n    node = @peek\n    while node\n      arr << node.val\n      node = node.next\n    end\n    arr.reverse\n  end\nend\n
","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#2-array-implementation","level":3,"title":"2.   Array Implementation","text":"

When implementing a stack using an array, we can treat the end of the array as the top of the stack. As shown in Figure 5-3, push and pop operations correspond to adding and removing elements at the end of the array, both with a time complexity of \\(O(1)\\).

ArrayStackpush()pop()

Figure 5-3   Push and pop operations in array implementation of stack

Since elements pushed onto the stack may increase continuously, we can use a dynamic array, which eliminates the need to handle array expansion ourselves. Here is the sample code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_stack.py
class ArrayStack:\n    \"\"\"Stack based on array implementation\"\"\"\n\n    def __init__(self):\n        \"\"\"Constructor\"\"\"\n        self._stack: list[int] = []\n\n    def size(self) -> int:\n        \"\"\"Get the length of the stack\"\"\"\n        return len(self._stack)\n\n    def is_empty(self) -> bool:\n        \"\"\"Check if the stack is empty\"\"\"\n        return self.size() == 0\n\n    def push(self, item: int):\n        \"\"\"Push\"\"\"\n        self._stack.append(item)\n\n    def pop(self) -> int:\n        \"\"\"Pop\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._stack.pop()\n\n    def peek(self) -> int:\n        \"\"\"Access top of the stack element\"\"\"\n        if self.is_empty():\n            raise IndexError(\"Stack is empty\")\n        return self._stack[-1]\n\n    def to_list(self) -> list[int]:\n        \"\"\"Return list for printing\"\"\"\n        return self._stack\n
array_stack.cpp
/* Stack based on array implementation */\nclass ArrayStack {\n  private:\n    vector<int> stack;\n\n  public:\n    /* Get the length of the stack */\n    int size() {\n        return stack.size();\n    }\n\n    /* Check if the stack is empty */\n    bool isEmpty() {\n        return stack.size() == 0;\n    }\n\n    /* Push */\n    void push(int num) {\n        stack.push_back(num);\n    }\n\n    /* Pop */\n    int pop() {\n        int num = top();\n        stack.pop_back();\n        return num;\n    }\n\n    /* Return list for printing */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"Stack is empty\");\n        return stack.back();\n    }\n\n    /* Return Vector */\n    vector<int> toVector() {\n        return stack;\n    }\n};\n
array_stack.java
/* Stack based on array implementation */\nclass ArrayStack {\n    private ArrayList<Integer> stack;\n\n    public ArrayStack() {\n        // Initialize list (dynamic array)\n        stack = new ArrayList<>();\n    }\n\n    /* Get the length of the stack */\n    public int size() {\n        return stack.size();\n    }\n\n    /* Check if the stack is empty */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* Push */\n    public void push(int num) {\n        stack.add(num);\n    }\n\n    /* Pop */\n    public int pop() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.remove(size() - 1);\n    }\n\n    /* Return list for printing */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.get(size() - 1);\n    }\n\n    /* Convert List to Array and return */\n    public Object[] toArray() {\n        return stack.toArray();\n    }\n}\n
array_stack.cs
/* Stack based on array implementation */\nclass ArrayStack {\n    List<int> stack;\n    public ArrayStack() {\n        // Initialize list (dynamic array)\n        stack = [];\n    }\n\n    /* Get the length of the stack */\n    public int Size() {\n        return stack.Count;\n    }\n\n    /* Check if the stack is empty */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* Push */\n    public void Push(int num) {\n        stack.Add(num);\n    }\n\n    /* Pop */\n    public int Pop() {\n        if (IsEmpty())\n            throw new Exception();\n        var val = Peek();\n        stack.RemoveAt(Size() - 1);\n        return val;\n    }\n\n    /* Return list for printing */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stack[Size() - 1];\n    }\n\n    /* Convert List to Array and return */\n    public int[] ToArray() {\n        return [.. stack];\n    }\n}\n
array_stack.go
/* Stack based on array implementation */\ntype arrayStack struct {\n    data []int // Data\n}\n\n/* Access top of the stack element */\nfunc newArrayStack() *arrayStack {\n    return &arrayStack{\n        // Set stack length to 0, capacity to 16\n        data: make([]int, 0, 16),\n    }\n}\n\n/* Stack length */\nfunc (s *arrayStack) size() int {\n    return len(s.data)\n}\n\n/* Is stack empty */\nfunc (s *arrayStack) isEmpty() bool {\n    return s.size() == 0\n}\n\n/* Push */\nfunc (s *arrayStack) push(v int) {\n    // Slice will automatically expand\n    s.data = append(s.data, v)\n}\n\n/* Pop */\nfunc (s *arrayStack) pop() any {\n    val := s.peek()\n    s.data = s.data[:len(s.data)-1]\n    return val\n}\n\n/* Get stack top element */\nfunc (s *arrayStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    val := s.data[len(s.data)-1]\n    return val\n}\n\n/* Get Slice for printing */\nfunc (s *arrayStack) toSlice() []int {\n    return s.data\n}\n
array_stack.swift
/* Stack based on array implementation */\nclass ArrayStack {\n    private var stack: [Int]\n\n    init() {\n        // Initialize list (dynamic array)\n        stack = []\n    }\n\n    /* Get the length of the stack */\n    func size() -> Int {\n        stack.count\n    }\n\n    /* Check if the stack is empty */\n    func isEmpty() -> Bool {\n        stack.isEmpty\n    }\n\n    /* Push */\n    func push(num: Int) {\n        stack.append(num)\n    }\n\n    /* Pop */\n    @discardableResult\n    func pop() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return stack.removeLast()\n    }\n\n    /* Return list for printing */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"Stack is empty\")\n        }\n        return stack.last!\n    }\n\n    /* Convert List to Array and return */\n    func toArray() -> [Int] {\n        stack\n    }\n}\n
array_stack.js
/* Stack based on array implementation */\nclass ArrayStack {\n    #stack;\n    constructor() {\n        this.#stack = [];\n    }\n\n    /* Get the length of the stack */\n    get size() {\n        return this.#stack.length;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty() {\n        return this.#stack.length === 0;\n    }\n\n    /* Push */\n    push(num) {\n        this.#stack.push(num);\n    }\n\n    /* Pop */\n    pop() {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.#stack.pop();\n    }\n\n    /* Return list for printing */\n    top() {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.#stack[this.#stack.length - 1];\n    }\n\n    /* Return Array */\n    toArray() {\n        return this.#stack;\n    }\n}\n
array_stack.ts
/* Stack based on array implementation */\nclass ArrayStack {\n    private stack: number[];\n    constructor() {\n        this.stack = [];\n    }\n\n    /* Get the length of the stack */\n    get size(): number {\n        return this.stack.length;\n    }\n\n    /* Check if the stack is empty */\n    isEmpty(): boolean {\n        return this.stack.length === 0;\n    }\n\n    /* Push */\n    push(num: number): void {\n        this.stack.push(num);\n    }\n\n    /* Pop */\n    pop(): number | undefined {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.stack.pop();\n    }\n\n    /* Return list for printing */\n    top(): number | undefined {\n        if (this.isEmpty()) throw new Error('Stack is empty');\n        return this.stack[this.stack.length - 1];\n    }\n\n    /* Return Array */\n    toArray() {\n        return this.stack;\n    }\n}\n
array_stack.dart
/* Stack based on array implementation */\nclass ArrayStack {\n  late List<int> _stack;\n  ArrayStack() {\n    _stack = [];\n  }\n\n  /* Get the length of the stack */\n  int size() {\n    return _stack.length;\n  }\n\n  /* Check if the stack is empty */\n  bool isEmpty() {\n    return _stack.isEmpty;\n  }\n\n  /* Push */\n  void push(int _num) {\n    _stack.add(_num);\n  }\n\n  /* Pop */\n  int pop() {\n    if (isEmpty()) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stack.removeLast();\n  }\n\n  /* Return list for printing */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"Stack is empty\");\n    }\n    return _stack.last;\n  }\n\n  /* Convert stack to Array and return */\n  List<int> toArray() => _stack;\n}\n
array_stack.rs
/* Stack based on array implementation */\nstruct ArrayStack<T> {\n    stack: Vec<T>,\n}\n\nimpl<T> ArrayStack<T> {\n    /* Access top of the stack element */\n    fn new() -> ArrayStack<T> {\n        ArrayStack::<T> {\n            stack: Vec::<T>::new(),\n        }\n    }\n\n    /* Get the length of the stack */\n    fn size(&self) -> usize {\n        self.stack.len()\n    }\n\n    /* Check if the stack is empty */\n    fn is_empty(&self) -> bool {\n        self.size() == 0\n    }\n\n    /* Push */\n    fn push(&mut self, num: T) {\n        self.stack.push(num);\n    }\n\n    /* Pop */\n    fn pop(&mut self) -> Option<T> {\n        self.stack.pop()\n    }\n\n    /* Return list for printing */\n    fn peek(&self) -> Option<&T> {\n        if self.is_empty() {\n            panic!(\"Stack is empty\")\n        };\n        self.stack.last()\n    }\n\n    /* Return &Vec */\n    fn to_array(&self) -> &Vec<T> {\n        &self.stack\n    }\n}\n
array_stack.c
/* Stack based on array implementation */\ntypedef struct {\n    int *data;\n    int size;\n} ArrayStack;\n\n/* Constructor */\nArrayStack *newArrayStack() {\n    ArrayStack *stack = malloc(sizeof(ArrayStack));\n    // Initialize with large capacity to avoid expansion\n    stack->data = malloc(sizeof(int) * MAX_SIZE);\n    stack->size = 0;\n    return stack;\n}\n\n/* Destructor */\nvoid delArrayStack(ArrayStack *stack) {\n    free(stack->data);\n    free(stack);\n}\n\n/* Get the length of the stack */\nint size(ArrayStack *stack) {\n    return stack->size;\n}\n\n/* Check if the stack is empty */\nbool isEmpty(ArrayStack *stack) {\n    return stack->size == 0;\n}\n\n/* Push */\nvoid push(ArrayStack *stack, int num) {\n    if (stack->size == MAX_SIZE) {\n        printf(\"Stack is full\\n\");\n        return;\n    }\n    stack->data[stack->size] = num;\n    stack->size++;\n}\n\n/* Return list for printing */\nint peek(ArrayStack *stack) {\n    if (stack->size == 0) {\n        printf(\"Stack is empty\\n\");\n        return INT_MAX;\n    }\n    return stack->data[stack->size - 1];\n}\n\n/* Pop */\nint pop(ArrayStack *stack) {\n    int val = peek(stack);\n    stack->size--;\n    return val;\n}\n
array_stack.kt
/* Stack based on array implementation */\nclass ArrayStack {\n    // Initialize list (dynamic array)\n    private val stack = mutableListOf<Int>()\n\n    /* Get the length of the stack */\n    fun size(): Int {\n        return stack.size\n    }\n\n    /* Check if the stack is empty */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* Push */\n    fun push(num: Int) {\n        stack.add(num)\n    }\n\n    /* Pop */\n    fun pop(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack.removeAt(size() - 1)\n    }\n\n    /* Return list for printing */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack[size() - 1]\n    }\n\n    /* Convert List to Array and return */\n    fun toArray(): Array<Any> {\n        return stack.toTypedArray()\n    }\n}\n
array_stack.rb
### Stack based on array ###\nclass ArrayStack\n  ### Constructor ###\n  def initialize\n    @stack = []\n  end\n\n  ### Get stack length ###\n  def size\n    @stack.length\n  end\n\n  ### Check if stack is empty ###\n  def is_empty?\n    @stack.empty?\n  end\n\n  ### Push ###\n  def push(item)\n    @stack << item\n  end\n\n  ### Pop ###\n  def pop\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @stack.pop\n  end\n\n  ### Access top element ###\n  def peek\n    raise IndexError, 'Stack is empty' if is_empty?\n\n    @stack.last\n  end\n\n  ### Return list for printing ###\n  def to_array\n    @stack\n  end\nend\n
","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#513-comparison-of-the-two-implementations","level":2,"title":"5.1.3   Comparison of the Two Implementations","text":"

Supported Operations

Both implementations support all operations defined by the stack. The array implementation additionally supports random access, but this goes beyond the stack definition and is generally not used.

Time Efficiency

In the array-based implementation, both push and pop operations occur in pre-allocated contiguous memory, which has good cache locality and is therefore more efficient. However, if pushing exceeds the array capacity, it triggers an expansion mechanism, causing the time complexity of that particular push operation to become \\(O(n)\\).

In the linked list-based implementation, list expansion is very flexible, and there is no issue of reduced efficiency due to array expansion. However, the push operation requires initializing a node object and modifying pointers, so it is relatively less efficient. Nevertheless, if the pushed elements are already node objects, the initialization step can be omitted, thereby improving efficiency.

In summary, when the elements pushed and popped are basic data types such as int or double, we can draw the following conclusions:

  • The array-based stack implementation has reduced efficiency when expansion is triggered, but since expansion is an infrequent operation, the average efficiency is higher.
  • The linked list-based stack implementation can provide more stable efficiency performance.

Space Efficiency

When initializing a list, the system allocates an \"initial capacity\" that may exceed the actual need. Additionally, the expansion mechanism typically expands at a specific ratio (e.g., 2x), and the capacity after expansion may also exceed actual needs. Therefore, the array-based stack implementation may cause some space wastage.

However, since linked list nodes need to store additional pointers, the space occupied by linked list nodes is relatively large.

In summary, we cannot simply determine which implementation is more memory-efficient and need to analyze the specific situation.

","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514-typical-applications-of-stack","level":2,"title":"5.1.4   Typical Applications of Stack","text":"
  • Back and forward in browsers, undo and redo in software. Every time we open a new webpage, the browser pushes the previous page onto the stack, allowing us to return to the previous page via the back operation. The back operation is essentially performing a pop. To support both back and forward, two stacks are needed to work together.
  • Program memory management. Each time a function is called, the system adds a stack frame to the top of the stack to record the function's context information. During recursion, the downward recursive phase continuously performs push operations, while the upward backtracking phase continuously performs pop operations.
","path":["Chapter 5. Stack and Queue","5.1   Stack"],"tags":[]},{"location":"chapter_stack_and_queue/summary/","level":1,"title":"5.4   Summary","text":"","path":["Chapter 5. Stack and Queue","5.4   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • A stack is a data structure that follows the LIFO principle and can be implemented using arrays or linked lists.
  • In terms of time efficiency, the array implementation of a stack has higher average efficiency, but during expansion, the time complexity of a single push operation degrades to \\(O(n)\\). In contrast, the linked list implementation of a stack provides more stable efficiency performance.
  • In terms of space efficiency, the array implementation of a stack may lead to some degree of space wastage. However, it should be noted that the memory space occupied by linked list nodes is larger than that of array elements.
  • A queue is a data structure that follows the FIFO principle and can also be implemented using arrays or linked lists. The conclusions regarding time efficiency and space efficiency comparisons for queues are similar to those for stacks mentioned above.
  • A deque is a queue with greater flexibility that allows adding and removing elements at both ends.
","path":["Chapter 5. Stack and Queue","5.4   Summary"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: Is the browser's forward and backward functionality implemented with a doubly linked list?

The forward and backward functionality of a browser is essentially a manifestation of a \"stack.\" When a user visits a new page, that page is added to the top of the stack; when the user clicks the back button, that page is popped from the top of the stack. Using a deque can conveniently implement some additional operations, as mentioned in the \"Deque\" section.

Q: After popping from the stack, do we need to free the memory of the popped node?

If the popped node will still be needed later, then memory does not need to be freed. If it won't be used afterward, languages like Java and Python have automatic garbage collection, so manual memory deallocation is not required; in C and C++, manual memory deallocation is necessary.

Q: A deque seems like two stacks joined together. What is its purpose?

A deque is like a combination of a stack and a queue, or two stacks joined together. It exhibits the logic of both stack and queue, so it can implement all applications of stacks and queues, and is more flexible.

Q: How are undo and redo specifically implemented?

Use two stacks: stack A for undo and stack B for redo.

  1. Whenever the user performs an operation, push this operation onto stack A and clear stack B.
  2. When the user performs \"undo,\" pop the most recent operation from stack A and push it onto stack B.
  3. When the user performs \"redo,\" pop the most recent operation from stack B and push it onto stack A.
","path":["Chapter 5. Stack and Queue","5.4   Summary"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"Chapter 7.   Tree","text":"

Abstract

Towering trees are full of vitality, with deep roots and lush leaves, spreading branches and flourishing.

They show us the vivid form of divide and conquer in data.

","path":["Chapter 7. Tree","Chapter 7.   Tree"],"tags":[]},{"location":"chapter_tree/#chapter-contents","level":2,"title":"Chapter contents","text":"
  • 7.1   Binary Tree
  • 7.2   Binary Tree Traversal
  • 7.3   Array Representation of Tree
  • 7.4   Binary Search Tree
  • 7.5   AVL Tree *
  • 7.6   Summary
","path":["Chapter 7. Tree","Chapter 7.   Tree"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/","level":1,"title":"7.3   Array Representation of Binary Trees","text":"

Under the linked list representation, the storage unit of a binary tree is a node TreeNode, and nodes are connected by pointers. The previous section introduced the basic operations of binary trees under the linked list representation.

So, can we use an array to represent a binary tree? The answer is yes.

","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#731-representing-perfect-binary-trees","level":2,"title":"7.3.1   Representing Perfect Binary Trees","text":"

Let's analyze a simple case first. Given a perfect binary tree, we store all nodes in an array according to the order of level-order traversal, where each node corresponds to a unique array index.

Based on the characteristics of level-order traversal, we can derive a \"mapping formula\" between parent node index and child node indices: If a node's index is \\(i\\), then its left child index is \\(2i + 1\\) and its right child index is \\(2i + 2\\). Figure 7-12 shows the mapping relationships between various node indices.

Figure 7-12   Array representation of a perfect binary tree

The mapping formula plays a role similar to the node references (pointers) in linked lists. Given any node in the array, we can access its left (right) child node using the mapping formula.

","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732-representing-any-binary-tree","level":2,"title":"7.3.2   Representing Any Binary Tree","text":"

Perfect binary trees are a special case; in the middle levels of a binary tree, there are typically many None values. Since the level-order traversal sequence does not include these None values, we cannot infer the number and distribution of None values based on this sequence alone. This means multiple binary tree structures can correspond to the same level-order traversal sequence.

As shown in Figure 7-13, given a non-perfect binary tree, the above method of array representation fails.

Figure 7-13   Level-order traversal sequence corresponds to multiple binary tree possibilities

To solve this problem, we can consider explicitly writing out all None values in the level-order traversal sequence. As shown in Figure 7-14, after this treatment, the level-order traversal sequence can uniquely represent a binary tree. Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# Array representation of a binary tree\n# Using None to represent empty slots\ntree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]\n
/* Array representation of a binary tree */\n// Using the maximum integer value INT_MAX to mark empty slots\nvector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
/* Array representation of a binary tree */\n// Using the Integer wrapper class allows for using null to mark empty slots\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
/* Array representation of a binary tree */\n// Using nullable int (int?) allows for using null to mark empty slots\nint?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* Array representation of a binary tree */\n// Using an any type slice, allowing for nil to mark empty slots\ntree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}\n
/* Array representation of a binary tree */\n// Using optional Int (Int?) allows for using nil to mark empty slots\nlet tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n
/* Array representation of a binary tree */\n// Using null to represent empty slots\nlet tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* Array representation of a binary tree */\n// Using null to represent empty slots\nlet tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* Array representation of a binary tree */\n// Using nullable int (int?) allows for using null to mark empty slots\nList<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* Array representation of a binary tree */\n// Using None to mark empty slots\nlet tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];\n
/* Array representation of a binary tree */\n// Using the maximum int value to mark empty slots, therefore, node values must not be INT_MAX\nint tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
/* Array representation of a binary tree */\n// Using null to represent empty slots\nval tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )\n
### Array representation of a binary tree ###\n# Using nil to represent empty slots\ntree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n

Figure 7-14   Array representation of any type of binary tree

It's worth noting that complete binary trees are very well-suited for array representation. Recalling the definition of a complete binary tree, None only appears at the bottom level and towards the right, meaning all None values must appear at the end of the level-order traversal sequence.

This means that when using an array to represent a complete binary tree, it's possible to omit storing all None values, which is very convenient. Figure 7-15 gives an example.

Figure 7-15   Array representation of a complete binary tree

The following code implements a binary tree based on array representation, including the following operations:

  • Given a certain node, obtain its value, left (right) child node, and parent node.
  • Obtain the preorder, inorder, postorder, and level-order traversal sequences.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_binary_tree.py
class ArrayBinaryTree:\n    \"\"\"Binary tree class represented by array\"\"\"\n\n    def __init__(self, arr: list[int | None]):\n        \"\"\"Constructor\"\"\"\n        self._tree = list(arr)\n\n    def size(self):\n        \"\"\"List capacity\"\"\"\n        return len(self._tree)\n\n    def val(self, i: int) -> int | None:\n        \"\"\"Get value of node at index i\"\"\"\n        # If index is out of bounds, return None, representing empty position\n        if i < 0 or i >= self.size():\n            return None\n        return self._tree[i]\n\n    def left(self, i: int) -> int | None:\n        \"\"\"Get index of left child node of node at index i\"\"\"\n        return 2 * i + 1\n\n    def right(self, i: int) -> int | None:\n        \"\"\"Get index of right child node of node at index i\"\"\"\n        return 2 * i + 2\n\n    def parent(self, i: int) -> int | None:\n        \"\"\"Get index of parent node of node at index i\"\"\"\n        return (i - 1) // 2\n\n    def level_order(self) -> list[int]:\n        \"\"\"Level-order traversal\"\"\"\n        self.res = []\n        # Traverse array directly\n        for i in range(self.size()):\n            if self.val(i) is not None:\n                self.res.append(self.val(i))\n        return self.res\n\n    def dfs(self, i: int, order: str):\n        \"\"\"Depth-first traversal\"\"\"\n        if self.val(i) is None:\n            return\n        # Preorder traversal\n        if order == \"pre\":\n            self.res.append(self.val(i))\n        self.dfs(self.left(i), order)\n        # Inorder traversal\n        if order == \"in\":\n            self.res.append(self.val(i))\n        self.dfs(self.right(i), order)\n        # Postorder traversal\n        if order == \"post\":\n            self.res.append(self.val(i))\n\n    def pre_order(self) -> list[int]:\n        \"\"\"Preorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"pre\")\n        return self.res\n\n    def in_order(self) -> list[int]:\n        \"\"\"Inorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"in\")\n        return self.res\n\n    def post_order(self) -> list[int]:\n        \"\"\"Postorder traversal\"\"\"\n        self.res = []\n        self.dfs(0, order=\"post\")\n        return self.res\n
array_binary_tree.cpp
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n  public:\n    /* Constructor */\n    ArrayBinaryTree(vector<int> arr) {\n        tree = arr;\n    }\n\n    /* List capacity */\n    int size() {\n        return tree.size();\n    }\n\n    /* Get value of node at index i */\n    int val(int i) {\n        // Return INT_MAX if index out of bounds, representing empty position\n        if (i < 0 || i >= size())\n            return INT_MAX;\n        return tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    int left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    int right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    int parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    vector<int> levelOrder() {\n        vector<int> res;\n        // Traverse array directly\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != INT_MAX)\n                res.push_back(val(i));\n        }\n        return res;\n    }\n\n    /* Preorder traversal */\n    vector<int> preOrder() {\n        vector<int> res;\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    vector<int> inOrder() {\n        vector<int> res;\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    vector<int> postOrder() {\n        vector<int> res;\n        dfs(0, \"post\", res);\n        return res;\n    }\n\n  private:\n    vector<int> tree;\n\n    /* Depth-first traversal */\n    void dfs(int i, string order, vector<int> &res) {\n        // If empty position, return\n        if (val(i) == INT_MAX)\n            return;\n        // Preorder traversal\n        if (order == \"pre\")\n            res.push_back(val(i));\n        dfs(left(i), order, res);\n        // Inorder traversal\n        if (order == \"in\")\n            res.push_back(val(i));\n        dfs(right(i), order, res);\n        // Postorder traversal\n        if (order == \"post\")\n            res.push_back(val(i));\n    }\n};\n
array_binary_tree.java
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    private List<Integer> tree;\n\n    /* Constructor */\n    public ArrayBinaryTree(List<Integer> arr) {\n        tree = new ArrayList<>(arr);\n    }\n\n    /* List capacity */\n    public int size() {\n        return tree.size();\n    }\n\n    /* Get value of node at index i */\n    public Integer val(int i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= size())\n            return null;\n        return tree.get(i);\n    }\n\n    /* Get index of left child node of node at index i */\n    public Integer left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    public Integer right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    public Integer parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    public List<Integer> levelOrder() {\n        List<Integer> res = new ArrayList<>();\n        // Traverse array directly\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != null)\n                res.add(val(i));\n        }\n        return res;\n    }\n\n    /* Depth-first traversal */\n    private void dfs(Integer i, String order, List<Integer> res) {\n        // If empty position, return\n        if (val(i) == null)\n            return;\n        // Preorder traversal\n        if (\"pre\".equals(order))\n            res.add(val(i));\n        dfs(left(i), order, res);\n        // Inorder traversal\n        if (\"in\".equals(order))\n            res.add(val(i));\n        dfs(right(i), order, res);\n        // Postorder traversal\n        if (\"post\".equals(order))\n            res.add(val(i));\n    }\n\n    /* Preorder traversal */\n    public List<Integer> preOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    public List<Integer> inOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    public List<Integer> postOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"post\", res);\n        return res;\n    }\n}\n
array_binary_tree.cs
/* Binary tree class represented by array */\nclass ArrayBinaryTree(List<int?> arr) {\n    List<int?> tree = new(arr);\n\n    /* List capacity */\n    public int Size() {\n        return tree.Count;\n    }\n\n    /* Get value of node at index i */\n    public int? Val(int i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= Size())\n            return null;\n        return tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    public int Left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    public int Right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    public int Parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* Level-order traversal */\n    public List<int> LevelOrder() {\n        List<int> res = [];\n        // Traverse array directly\n        for (int i = 0; i < Size(); i++) {\n            if (Val(i).HasValue)\n                res.Add(Val(i)!.Value);\n        }\n        return res;\n    }\n\n    /* Depth-first traversal */\n    void DFS(int i, string order, List<int> res) {\n        // If empty position, return\n        if (!Val(i).HasValue)\n            return;\n        // Preorder traversal\n        if (order == \"pre\")\n            res.Add(Val(i)!.Value);\n        DFS(Left(i), order, res);\n        // Inorder traversal\n        if (order == \"in\")\n            res.Add(Val(i)!.Value);\n        DFS(Right(i), order, res);\n        // Postorder traversal\n        if (order == \"post\")\n            res.Add(Val(i)!.Value);\n    }\n\n    /* Preorder traversal */\n    public List<int> PreOrder() {\n        List<int> res = [];\n        DFS(0, \"pre\", res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    public List<int> InOrder() {\n        List<int> res = [];\n        DFS(0, \"in\", res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    public List<int> PostOrder() {\n        List<int> res = [];\n        DFS(0, \"post\", res);\n        return res;\n    }\n}\n
array_binary_tree.go
/* Binary tree class represented by array */\ntype arrayBinaryTree struct {\n    tree []any\n}\n\n/* Constructor */\nfunc newArrayBinaryTree(arr []any) *arrayBinaryTree {\n    return &arrayBinaryTree{\n        tree: arr,\n    }\n}\n\n/* List capacity */\nfunc (abt *arrayBinaryTree) size() int {\n    return len(abt.tree)\n}\n\n/* Get value of node at index i */\nfunc (abt *arrayBinaryTree) val(i int) any {\n    // If index out of bounds, return null to represent empty position\n    if i < 0 || i >= abt.size() {\n        return nil\n    }\n    return abt.tree[i]\n}\n\n/* Get index of left child node of node at index i */\nfunc (abt *arrayBinaryTree) left(i int) int {\n    return 2*i + 1\n}\n\n/* Get index of right child node of node at index i */\nfunc (abt *arrayBinaryTree) right(i int) int {\n    return 2*i + 2\n}\n\n/* Get index of parent node of node at index i */\nfunc (abt *arrayBinaryTree) parent(i int) int {\n    return (i - 1) / 2\n}\n\n/* Level-order traversal */\nfunc (abt *arrayBinaryTree) levelOrder() []any {\n    var res []any\n    // Traverse array directly\n    for i := 0; i < abt.size(); i++ {\n        if abt.val(i) != nil {\n            res = append(res, abt.val(i))\n        }\n    }\n    return res\n}\n\n/* Depth-first traversal */\nfunc (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) {\n    // If empty position, return\n    if abt.val(i) == nil {\n        return\n    }\n    // Preorder traversal\n    if order == \"pre\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.left(i), order, res)\n    // Inorder traversal\n    if order == \"in\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.right(i), order, res)\n    // Postorder traversal\n    if order == \"post\" {\n        *res = append(*res, abt.val(i))\n    }\n}\n\n/* Preorder traversal */\nfunc (abt *arrayBinaryTree) preOrder() []any {\n    var res []any\n    abt.dfs(0, \"pre\", &res)\n    return res\n}\n\n/* Inorder traversal */\nfunc (abt *arrayBinaryTree) inOrder() []any {\n    var res []any\n    abt.dfs(0, \"in\", &res)\n    return res\n}\n\n/* Postorder traversal */\nfunc (abt *arrayBinaryTree) postOrder() []any {\n    var res []any\n    abt.dfs(0, \"post\", &res)\n    return res\n}\n
array_binary_tree.swift
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    private var tree: [Int?]\n\n    /* Constructor */\n    init(arr: [Int?]) {\n        tree = arr\n    }\n\n    /* List capacity */\n    func size() -> Int {\n        tree.count\n    }\n\n    /* Get value of node at index i */\n    func val(i: Int) -> Int? {\n        // If index out of bounds, return null to represent empty position\n        if i < 0 || i >= size() {\n            return nil\n        }\n        return tree[i]\n    }\n\n    /* Get index of left child node of node at index i */\n    func left(i: Int) -> Int {\n        2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    func right(i: Int) -> Int {\n        2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    func parent(i: Int) -> Int {\n        (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    func levelOrder() -> [Int] {\n        var res: [Int] = []\n        // Traverse array directly\n        for i in 0 ..< size() {\n            if let val = val(i: i) {\n                res.append(val)\n            }\n        }\n        return res\n    }\n\n    /* Depth-first traversal */\n    private func dfs(i: Int, order: String, res: inout [Int]) {\n        // If empty position, return\n        guard let val = val(i: i) else {\n            return\n        }\n        // Preorder traversal\n        if order == \"pre\" {\n            res.append(val)\n        }\n        dfs(i: left(i: i), order: order, res: &res)\n        // Inorder traversal\n        if order == \"in\" {\n            res.append(val)\n        }\n        dfs(i: right(i: i), order: order, res: &res)\n        // Postorder traversal\n        if order == \"post\" {\n            res.append(val)\n        }\n    }\n\n    /* Preorder traversal */\n    func preOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"pre\", res: &res)\n        return res\n    }\n\n    /* Inorder traversal */\n    func inOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"in\", res: &res)\n        return res\n    }\n\n    /* Postorder traversal */\n    func postOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"post\", res: &res)\n        return res\n    }\n}\n
array_binary_tree.js
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    #tree;\n\n    /* Constructor */\n    constructor(arr) {\n        this.#tree = arr;\n    }\n\n    /* List capacity */\n    size() {\n        return this.#tree.length;\n    }\n\n    /* Get value of node at index i */\n    val(i) {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    left(i) {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    right(i) {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    parent(i) {\n        return Math.floor((i - 1) / 2); // Floor division\n    }\n\n    /* Level-order traversal */\n    levelOrder() {\n        let res = [];\n        // Traverse array directly\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* Depth-first traversal */\n    #dfs(i, order, res) {\n        // If empty position, return\n        if (this.val(i) === null) return;\n        // Preorder traversal\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Inorder traversal\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Postorder traversal\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Preorder traversal */\n    preOrder() {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    inOrder() {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    postOrder() {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
array_binary_tree.ts
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n    #tree: (number | null)[];\n\n    /* Constructor */\n    constructor(arr: (number | null)[]) {\n        this.#tree = arr;\n    }\n\n    /* List capacity */\n    size(): number {\n        return this.#tree.length;\n    }\n\n    /* Get value of node at index i */\n    val(i: number): number | null {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* Get index of left child node of node at index i */\n    left(i: number): number {\n        return 2 * i + 1;\n    }\n\n    /* Get index of right child node of node at index i */\n    right(i: number): number {\n        return 2 * i + 2;\n    }\n\n    /* Get index of parent node of node at index i */\n    parent(i: number): number {\n        return Math.floor((i - 1) / 2); // Floor division\n    }\n\n    /* Level-order traversal */\n    levelOrder(): number[] {\n        let res = [];\n        // Traverse array directly\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* Depth-first traversal */\n    #dfs(i: number, order: Order, res: (number | null)[]): void {\n        // If empty position, return\n        if (this.val(i) === null) return;\n        // Preorder traversal\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // Inorder traversal\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // Postorder traversal\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* Preorder traversal */\n    preOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* Inorder traversal */\n    inOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* Postorder traversal */\n    postOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
array_binary_tree.dart
/* Binary tree class represented by array */\nclass ArrayBinaryTree {\n  late List<int?> _tree;\n\n  /* Constructor */\n  ArrayBinaryTree(this._tree);\n\n  /* List capacity */\n  int size() {\n    return _tree.length;\n  }\n\n  /* Get value of node at index i */\n  int? val(int i) {\n    // If index out of bounds, return null to represent empty position\n    if (i < 0 || i >= size()) {\n      return null;\n    }\n    return _tree[i];\n  }\n\n  /* Get index of left child node of node at index i */\n  int? left(int i) {\n    return 2 * i + 1;\n  }\n\n  /* Get index of right child node of node at index i */\n  int? right(int i) {\n    return 2 * i + 2;\n  }\n\n  /* Get index of parent node of node at index i */\n  int? parent(int i) {\n    return (i - 1) ~/ 2;\n  }\n\n  /* Level-order traversal */\n  List<int> levelOrder() {\n    List<int> res = [];\n    for (int i = 0; i < size(); i++) {\n      if (val(i) != null) {\n        res.add(val(i)!);\n      }\n    }\n    return res;\n  }\n\n  /* Depth-first traversal */\n  void dfs(int i, String order, List<int?> res) {\n    // If empty position, return\n    if (val(i) == null) {\n      return;\n    }\n    // Preorder traversal\n    if (order == 'pre') {\n      res.add(val(i));\n    }\n    dfs(left(i)!, order, res);\n    // Inorder traversal\n    if (order == 'in') {\n      res.add(val(i));\n    }\n    dfs(right(i)!, order, res);\n    // Postorder traversal\n    if (order == 'post') {\n      res.add(val(i));\n    }\n  }\n\n  /* Preorder traversal */\n  List<int?> preOrder() {\n    List<int?> res = [];\n    dfs(0, 'pre', res);\n    return res;\n  }\n\n  /* Inorder traversal */\n  List<int?> inOrder() {\n    List<int?> res = [];\n    dfs(0, 'in', res);\n    return res;\n  }\n\n  /* Postorder traversal */\n  List<int?> postOrder() {\n    List<int?> res = [];\n    dfs(0, 'post', res);\n    return res;\n  }\n}\n
array_binary_tree.rs
/* Binary tree class represented by array */\nstruct ArrayBinaryTree {\n    tree: Vec<Option<i32>>,\n}\n\nimpl ArrayBinaryTree {\n    /* Constructor */\n    fn new(arr: Vec<Option<i32>>) -> Self {\n        Self { tree: arr }\n    }\n\n    /* List capacity */\n    fn size(&self) -> i32 {\n        self.tree.len() as i32\n    }\n\n    /* Get value of node at index i */\n    fn val(&self, i: i32) -> Option<i32> {\n        // If index is out of bounds, return None, representing empty position\n        if i < 0 || i >= self.size() {\n            None\n        } else {\n            self.tree[i as usize]\n        }\n    }\n\n    /* Get index of left child node of node at index i */\n    fn left(&self, i: i32) -> i32 {\n        2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    fn right(&self, i: i32) -> i32 {\n        2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    fn parent(&self, i: i32) -> i32 {\n        (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    fn level_order(&self) -> Vec<i32> {\n        self.tree.iter().filter_map(|&x| x).collect()\n    }\n\n    /* Depth-first traversal */\n    fn dfs(&self, i: i32, order: &'static str, res: &mut Vec<i32>) {\n        if self.val(i).is_none() {\n            return;\n        }\n        let val = self.val(i).unwrap();\n        // Preorder traversal\n        if order == \"pre\" {\n            res.push(val);\n        }\n        self.dfs(self.left(i), order, res);\n        // Inorder traversal\n        if order == \"in\" {\n            res.push(val);\n        }\n        self.dfs(self.right(i), order, res);\n        // Postorder traversal\n        if order == \"post\" {\n            res.push(val);\n        }\n    }\n\n    /* Preorder traversal */\n    fn pre_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"pre\", &mut res);\n        res\n    }\n\n    /* Inorder traversal */\n    fn in_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"in\", &mut res);\n        res\n    }\n\n    /* Postorder traversal */\n    fn post_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"post\", &mut res);\n        res\n    }\n}\n
array_binary_tree.c
/* Binary tree structure in array representation */\ntypedef struct {\n    int *tree;\n    int size;\n} ArrayBinaryTree;\n\n/* Constructor */\nArrayBinaryTree *newArrayBinaryTree(int *arr, int arrSize) {\n    ArrayBinaryTree *abt = (ArrayBinaryTree *)malloc(sizeof(ArrayBinaryTree));\n    abt->tree = malloc(sizeof(int) * arrSize);\n    memcpy(abt->tree, arr, sizeof(int) * arrSize);\n    abt->size = arrSize;\n    return abt;\n}\n\n/* Destructor */\nvoid delArrayBinaryTree(ArrayBinaryTree *abt) {\n    free(abt->tree);\n    free(abt);\n}\n\n/* List capacity */\nint size(ArrayBinaryTree *abt) {\n    return abt->size;\n}\n\n/* Get value of node at index i */\nint val(ArrayBinaryTree *abt, int i) {\n    // Return INT_MAX if index out of bounds, representing empty position\n    if (i < 0 || i >= size(abt))\n        return INT_MAX;\n    return abt->tree[i];\n}\n\n/* Level-order traversal */\nint *levelOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    // Traverse array directly\n    for (int i = 0; i < size(abt); i++) {\n        if (val(abt, i) != INT_MAX)\n            res[index++] = val(abt, i);\n    }\n    *returnSize = index;\n    return res;\n}\n\n/* Depth-first traversal */\nvoid dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) {\n    // If empty position, return\n    if (val(abt, i) == INT_MAX)\n        return;\n    // Preorder traversal\n    if (strcmp(order, \"pre\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, left(i), order, res, index);\n    // Inorder traversal\n    if (strcmp(order, \"in\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, right(i), order, res, index);\n    // Postorder traversal\n    if (strcmp(order, \"post\") == 0)\n        res[(*index)++] = val(abt, i);\n}\n\n/* Preorder traversal */\nint *preOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"pre\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* Inorder traversal */\nint *inOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"in\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* Postorder traversal */\nint *postOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"post\", res, &index);\n    *returnSize = index;\n    return res;\n}\n
array_binary_tree.kt
/* Binary tree class represented by array */\nclass ArrayBinaryTree(val tree: MutableList<Int?>) {\n    /* List capacity */\n    fun size(): Int {\n        return tree.size\n    }\n\n    /* Get value of node at index i */\n    fun _val(i: Int): Int? {\n        // If index out of bounds, return null to represent empty position\n        if (i < 0 || i >= size()) return null\n        return tree[i]\n    }\n\n    /* Get index of left child node of node at index i */\n    fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* Get index of right child node of node at index i */\n    fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* Get index of parent node of node at index i */\n    fun parent(i: Int): Int {\n        return (i - 1) / 2\n    }\n\n    /* Level-order traversal */\n    fun levelOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        // Traverse array directly\n        for (i in 0..<size()) {\n            if (_val(i) != null)\n                res.add(_val(i))\n        }\n        return res\n    }\n\n    /* Depth-first traversal */\n    fun dfs(i: Int, order: String, res: MutableList<Int?>) {\n        // If empty position, return\n        if (_val(i) == null)\n            return\n        // Preorder traversal\n        if (\"pre\" == order)\n            res.add(_val(i))\n        dfs(left(i), order, res)\n        // Inorder traversal\n        if (\"in\" == order)\n            res.add(_val(i))\n        dfs(right(i), order, res)\n        // Postorder traversal\n        if (\"post\" == order)\n            res.add(_val(i))\n    }\n\n    /* Preorder traversal */\n    fun preOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"pre\", res)\n        return res\n    }\n\n    /* Inorder traversal */\n    fun inOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"in\", res)\n        return res\n    }\n\n    /* Postorder traversal */\n    fun postOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"post\", res)\n        return res\n    }\n}\n
array_binary_tree.rb
### Array representation of binary tree class ###\nclass ArrayBinaryTree\n  ### Constructor ###\n  def initialize(arr)\n    @tree = arr.to_a\n  end\n\n  ### List capacity ###\n  def size\n    @tree.length\n  end\n\n  ### Get value of node at index i ###\n  def val(i)\n    # Return nil if index out of bounds, representing empty position\n    return if i < 0 || i >= size\n\n    @tree[i]\n  end\n\n  ### Get left child index of node at index i ###\n  def left(i)\n    2 * i + 1\n  end\n\n  ### Get right child index of node at index i ###\n  def right(i)\n    2 * i + 2\n  end\n\n  ### Get parent node index of node at index i ###\n  def parent(i)\n    (i - 1) / 2\n  end\n\n  ### Level-order traversal ###\n  def level_order\n    @res = []\n\n    # Traverse array directly\n    for i in 0...size\n      @res << val(i) unless val(i).nil?\n    end\n\n    @res\n  end\n\n  ### Depth-first traversal ###\n  def dfs(i, order)\n    return if val(i).nil?\n    # Preorder traversal\n    @res << val(i) if order == :pre\n    dfs(left(i), order)\n    # Inorder traversal\n    @res << val(i) if order == :in\n    dfs(right(i), order)\n    # Postorder traversal\n    @res << val(i) if order == :post\n  end\n\n  ### Pre-order traversal ###\n  def pre_order\n    @res = []\n    dfs(0, :pre)\n    @res\n  end\n\n  ### In-order traversal ###\n  def in_order\n    @res = []\n    dfs(0, :in)\n    @res\n  end\n\n  ### Post-order traversal ###\n  def post_order\n    @res = []\n    dfs(0, :post)\n    @res\n  end\nend\n
","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#733-advantages-and-limitations","level":2,"title":"7.3.3   Advantages and Limitations","text":"

The array representation of binary trees has the following advantages:

  • Arrays are stored in contiguous memory space, which is cache-friendly, allowing faster access and traversal.
  • It does not require storing pointers, which saves space.
  • It allows random access to nodes.

However, the array representation also has some limitations:

  • Array storage requires contiguous memory space, so it is not suitable for storing trees with a large amount of data.
  • Adding or removing nodes requires array insertion and deletion operations, which have lower efficiency.
  • When there are many None values in the binary tree, the proportion of node data contained in the array is low, leading to lower space utilization.
","path":["Chapter 7. Tree","7.3   Array Representation of Binary Trees"],"tags":[]},{"location":"chapter_tree/avl_tree/","level":1,"title":"7.5   Avl Tree *","text":"

In the \"Binary Search Tree\" section, we mentioned that after multiple insertion and removal operations, a binary search tree may degenerate into a linked list. In this case, the time complexity of all operations degrades from \\(O(\\log n)\\) to \\(O(n)\\).

As shown in Figure 7-24, after two node removal operations, this binary search tree will degrade into a linked list.

Figure 7-24   Degradation of an AVL tree after removing nodes

For example, in the perfect binary tree shown in Figure 7-25, after inserting two nodes, the tree will lean heavily to the left, and the time complexity of search operations will also degrade.

Figure 7-25   Degradation of an AVL tree after inserting nodes

In 1962, G. M. Adelson-Velsky and E. M. Landis proposed the AVL tree in their paper \"An algorithm for the organization of information\". The paper described in detail a series of operations ensuring that after continuously adding and removing nodes, the AVL tree does not degenerate, thus keeping the time complexity of various operations at the \\(O(\\log n)\\) level. In other words, in scenarios requiring frequent insertions, deletions, searches, and modifications, the AVL tree can always maintain efficient data operation performance, making it very valuable in applications.

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#751-common-terminology-in-avl-trees","level":2,"title":"7.5.1   Common Terminology in Avl Trees","text":"

An AVL tree is both a binary search tree and a balanced binary tree, simultaneously satisfying all the properties of these two types of binary trees, hence it is a balanced binary search tree.

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-node-height","level":3,"title":"1.   Node Height","text":"

Since the operations related to AVL trees require obtaining node heights, we need to add a height variable to the node class:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class TreeNode:\n    \"\"\"AVL tree node\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                 # Node value\n        self.height: int = 0                # Node height\n        self.left: TreeNode | None = None   # Left child reference\n        self.right: TreeNode | None = None  # Right child reference\n
/* AVL tree node */\nstruct TreeNode {\n    int val{};          // Node value\n    int height = 0;     // Node height\n    TreeNode *left{};   // Left child\n    TreeNode *right{};  // Right child\n    TreeNode() = default;\n    explicit TreeNode(int x) : val(x){}\n};\n
/* AVL tree node */\nclass TreeNode {\n    public int val;        // Node value\n    public int height;     // Node height\n    public TreeNode left;  // Left child\n    public TreeNode right; // Right child\n    public TreeNode(int x) { val = x; }\n}\n
/* AVL tree node */\nclass TreeNode(int? x) {\n    public int? val = x;    // Node value\n    public int height;      // Node height\n    public TreeNode? left;  // Left child reference\n    public TreeNode? right; // Right child reference\n}\n
/* AVL tree node */\ntype TreeNode struct {\n    Val    int       // Node value\n    Height int       // Node height\n    Left   *TreeNode // Left child reference\n    Right  *TreeNode // Right child reference\n}\n
/* AVL tree node */\nclass TreeNode {\n    var val: Int // Node value\n    var height: Int // Node height\n    var left: TreeNode? // Left child\n    var right: TreeNode? // Right child\n\n    init(x: Int) {\n        val = x\n        height = 0\n    }\n}\n
/* AVL tree node */\nclass TreeNode {\n    val; // Node value\n    height; // Node height\n    left; // Left child pointer\n    right; // Right child pointer\n    constructor(val, left, right, height) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* AVL tree node */\nclass TreeNode {\n    val: number;            // Node value\n    height: number;         // Node height\n    left: TreeNode | null;  // Left child pointer\n    right: TreeNode | null; // Right child pointer\n    constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height; \n        this.left = left === undefined ? null : left; \n        this.right = right === undefined ? null : right; \n    }\n}\n
/* AVL tree node */\nclass TreeNode {\n  int val;         // Node value\n  int height;      // Node height\n  TreeNode? left;  // Left child\n  TreeNode? right; // Right child\n  TreeNode(this.val, [this.height = 0, this.left, this.right]);\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* AVL tree node */\nstruct TreeNode {\n    val: i32,                               // Node value\n    height: i32,                            // Node height\n    left: Option<Rc<RefCell<TreeNode>>>,    // Left child\n    right: Option<Rc<RefCell<TreeNode>>>,   // Right child\n}\n\nimpl TreeNode {\n    /* Constructor */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            height: 0,\n            left: None,\n            right: None\n        }))\n    }\n}\n
/* AVL tree node */\ntypedef struct TreeNode {\n    int val;\n    int height;\n    struct TreeNode *left;\n    struct TreeNode *right;\n} TreeNode;\n\n/* Constructor */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
/* AVL tree node */\nclass TreeNode(val _val: Int) {  // Node value\n    val height: Int = 0          // Node height\n    val left: TreeNode? = null   // Left child\n    val right: TreeNode? = null  // Right child\n}\n
### AVL tree node class ###\nclass TreeNode\n  attr_accessor :val    # Node value\n  attr_accessor :height # Node height\n  attr_accessor :left   # Left child reference\n  attr_accessor :right  # Right child reference\n\n  def initialize(val)\n    @val = val\n    @height = 0\n  end\nend\n

The \"node height\" refers to the distance from that node to its farthest leaf node, i.e., the number of \"edges\" passed. It is important to note that the height of a leaf node is \\(0\\), and the height of a null node is \\(-1\\). We will create two utility functions for getting and updating the height of a node:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def height(self, node: TreeNode | None) -> int:\n    \"\"\"Get node height\"\"\"\n    # Empty node height is -1, leaf node height is 0\n    if node is not None:\n        return node.height\n    return -1\n\ndef update_height(self, node: TreeNode | None):\n    \"\"\"Update node height\"\"\"\n    # Node height equals the height of the tallest subtree + 1\n    node.height = max([self.height(node.left), self.height(node.right)]) + 1\n
avl_tree.cpp
/* Get node height */\nint height(TreeNode *node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == nullptr ? -1 : node->height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode *node) {\n    // Node height equals the height of the tallest subtree + 1\n    node->height = max(height(node->left), height(node->right)) + 1;\n}\n
avl_tree.java
/* Get node height */\nint height(TreeNode node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height = Math.max(height(node.left), height(node.right)) + 1;\n}\n
avl_tree.cs
/* Get node height */\nint Height(TreeNode? node) {\n    // Empty node height is -1, leaf node height is 0\n    return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid UpdateHeight(TreeNode node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height = Math.Max(Height(node.left), Height(node.right)) + 1;\n}\n
avl_tree.go
/* Get node height */\nfunc (t *aVLTree) height(node *TreeNode) int {\n    // Empty node height is -1, leaf node height is 0\n    if node != nil {\n        return node.Height\n    }\n    return -1\n}\n\n/* Update node height */\nfunc (t *aVLTree) updateHeight(node *TreeNode) {\n    lh := t.height(node.Left)\n    rh := t.height(node.Right)\n    // Node height equals the height of the tallest subtree + 1\n    if lh > rh {\n        node.Height = lh + 1\n    } else {\n        node.Height = rh + 1\n    }\n}\n
avl_tree.swift
/* Get node height */\nfunc height(node: TreeNode?) -> Int {\n    // Empty node height is -1, leaf node height is 0\n    node?.height ?? -1\n}\n\n/* Update node height */\nfunc updateHeight(node: TreeNode?) {\n    // Node height equals the height of the tallest subtree + 1\n    node?.height = max(height(node: node?.left), height(node: node?.right)) + 1\n}\n
avl_tree.js
/* Get node height */\nheight(node) {\n    // Empty node height is -1, leaf node height is 0\n    return node === null ? -1 : node.height;\n}\n\n/* Update node height */\n#updateHeight(node) {\n    // Node height equals the height of the tallest subtree + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
avl_tree.ts
/* Get node height */\nheight(node: TreeNode): number {\n    // Empty node height is -1, leaf node height is 0\n    return node === null ? -1 : node.height;\n}\n\n/* Update node height */\nupdateHeight(node: TreeNode): void {\n    // Node height equals the height of the tallest subtree + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
avl_tree.dart
/* Get node height */\nint height(TreeNode? node) {\n  // Empty node height is -1, leaf node height is 0\n  return node == null ? -1 : node.height;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode? node) {\n  // Node height equals the height of the tallest subtree + 1\n  node!.height = max(height(node.left), height(node.right)) + 1;\n}\n
avl_tree.rs
/* Get node height */\nfn height(node: OptionTreeNodeRc) -> i32 {\n    // Empty node height is -1, leaf node height is 0\n    match node {\n        Some(node) => node.borrow().height,\n        None => -1,\n    }\n}\n\n/* Update node height */\nfn update_height(node: OptionTreeNodeRc) {\n    if let Some(node) = node {\n        let left = node.borrow().left.clone();\n        let right = node.borrow().right.clone();\n        // Node height equals the height of the tallest subtree + 1\n        node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1;\n    }\n}\n
avl_tree.c
/* Get node height */\nint height(TreeNode *node) {\n    // Empty node height is -1, leaf node height is 0\n    if (node != NULL) {\n        return node->height;\n    }\n    return -1;\n}\n\n/* Update node height */\nvoid updateHeight(TreeNode *node) {\n    int lh = height(node->left);\n    int rh = height(node->right);\n    // Node height equals the height of the tallest subtree + 1\n    if (lh > rh) {\n        node->height = lh + 1;\n    } else {\n        node->height = rh + 1;\n    }\n}\n
avl_tree.kt
/* Get node height */\nfun height(node: TreeNode?): Int {\n    // Empty node height is -1, leaf node height is 0\n    return node?.height ?: -1\n}\n\n/* Update node height */\nfun updateHeight(node: TreeNode?) {\n    // Node height equals the height of the tallest subtree + 1\n    node?.height = max(height(node?.left), height(node?.right)) + 1\n}\n
avl_tree.rb
### Get node height ###\ndef height(node)\n  # Empty node height is -1, leaf node height is 0\n  return node.height unless node.nil?\n\n  -1\nend\n\n### Update node height ###\ndef update_height(node)\n  # Node height equals the height of the tallest subtree + 1\n  node.height = [height(node.left), height(node.right)].max + 1\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-node-balance-factor","level":3,"title":"2.   Node Balance Factor","text":"

The balance factor of a node is defined as the height of the node's left subtree minus the height of its right subtree, and the balance factor of a null node is defined as \\(0\\). We also encapsulate the function to obtain the node's balance factor for convenient subsequent use:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def balance_factor(self, node: TreeNode | None) -> int:\n    \"\"\"Get balance factor\"\"\"\n    # Empty node balance factor is 0\n    if node is None:\n        return 0\n    # Node balance factor = left subtree height - right subtree height\n    return self.height(node.left) - self.height(node.right)\n
avl_tree.cpp
/* Get balance factor */\nint balanceFactor(TreeNode *node) {\n    // Empty node balance factor is 0\n    if (node == nullptr)\n        return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return height(node->left) - height(node->right);\n}\n
avl_tree.java
/* Get balance factor */\nint balanceFactor(TreeNode node) {\n    // Empty node balance factor is 0\n    if (node == null)\n        return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return height(node.left) - height(node.right);\n}\n
avl_tree.cs
/* Get balance factor */\nint BalanceFactor(TreeNode? node) {\n    // Empty node balance factor is 0\n    if (node == null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return Height(node.left) - Height(node.right);\n}\n
avl_tree.go
/* Get balance factor */\nfunc (t *aVLTree) balanceFactor(node *TreeNode) int {\n    // Empty node balance factor is 0\n    if node == nil {\n        return 0\n    }\n    // Node balance factor = left subtree height - right subtree height\n    return t.height(node.Left) - t.height(node.Right)\n}\n
avl_tree.swift
/* Get balance factor */\nfunc balanceFactor(node: TreeNode?) -> Int {\n    // Empty node balance factor is 0\n    guard let node = node else { return 0 }\n    // Node balance factor = left subtree height - right subtree height\n    return height(node: node.left) - height(node: node.right)\n}\n
avl_tree.js
/* Get balance factor */\nbalanceFactor(node) {\n    // Empty node balance factor is 0\n    if (node === null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return this.height(node.left) - this.height(node.right);\n}\n
avl_tree.ts
/* Get balance factor */\nbalanceFactor(node: TreeNode): number {\n    // Empty node balance factor is 0\n    if (node === null) return 0;\n    // Node balance factor = left subtree height - right subtree height\n    return this.height(node.left) - this.height(node.right);\n}\n
avl_tree.dart
/* Get balance factor */\nint balanceFactor(TreeNode? node) {\n  // Empty node balance factor is 0\n  if (node == null) return 0;\n  // Node balance factor = left subtree height - right subtree height\n  return height(node.left) - height(node.right);\n}\n
avl_tree.rs
/* Get balance factor */\nfn balance_factor(node: OptionTreeNodeRc) -> i32 {\n    match node {\n        // Empty node balance factor is 0\n        None => 0,\n        // Node balance factor = left subtree height - right subtree height\n        Some(node) => {\n            Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone())\n        }\n    }\n}\n
avl_tree.c
/* Get balance factor */\nint balanceFactor(TreeNode *node) {\n    // Empty node balance factor is 0\n    if (node == NULL) {\n        return 0;\n    }\n    // Node balance factor = left subtree height - right subtree height\n    return height(node->left) - height(node->right);\n}\n
avl_tree.kt
/* Get balance factor */\nfun balanceFactor(node: TreeNode?): Int {\n    // Empty node balance factor is 0\n    if (node == null) return 0\n    // Node balance factor = left subtree height - right subtree height\n    return height(node.left) - height(node.right)\n}\n
avl_tree.rb
### Get balance factor ###\ndef balance_factor(node)\n  # Empty node balance factor is 0\n  return 0 if node.nil?\n\n  # Node balance factor = left subtree height - right subtree height\n  height(node.left) - height(node.right)\nend\n

Tip

Let the balance factor be \\(f\\), then the balance factor of any node in an AVL tree satisfies \\(-1 \\le f \\le 1\\).

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#752-rotations-in-avl-trees","level":2,"title":"7.5.2   Rotations in Avl Trees","text":"

The characteristic of AVL trees lies in the \"rotation\" operation, which can restore balance to unbalanced nodes without affecting the inorder traversal sequence of the binary tree. In other words, rotation operations can both maintain the property of a \"binary search tree\" and make the tree return to a \"balanced binary tree\".

We call nodes with a balance factor absolute value \\(> 1\\) \"unbalanced nodes\". Depending on the imbalance situation, rotation operations are divided into four types: right rotation, left rotation, left rotation then right rotation, and right rotation then left rotation. Below we describe these rotation operations in detail.

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-right-rotation","level":3,"title":"1.   Right Rotation","text":"

As shown in Figure 7-26, the value below the node is the balance factor. From bottom to top, the first unbalanced node in the binary tree is \"node 3\". We focus on the subtree with this unbalanced node as the root, denoting the node as node and its left child as child, and perform a \"right rotation\" operation. After the right rotation is completed, the subtree regains balance and still maintains the properties of a binary search tree.

<1><2><3><4>

Figure 7-26   Steps of right rotation

As shown in Figure 7-27, when the child node has a right child (denoted as grand_child), a step needs to be added in the right rotation: set grand_child as the left child of node.

Figure 7-27   Right rotation with grand_child

\"Right rotation\" is a figurative term; in practice, it is achieved by modifying node pointers, as shown in the following code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def right_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Right rotation operation\"\"\"\n    child = node.left\n    grand_child = child.right\n    # Using child as pivot, rotate node to the right\n    child.right = node\n    node.left = grand_child\n    # Update node height\n    self.update_height(node)\n    self.update_height(child)\n    # Return root node of subtree after rotation\n    return child\n
avl_tree.cpp
/* Right rotation operation */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child = node->left;\n    TreeNode *grandChild = child->right;\n    // Using child as pivot, rotate node to the right\n    child->right = node;\n    node->left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.java
/* Right rotation operation */\nTreeNode rightRotate(TreeNode node) {\n    TreeNode child = node.left;\n    TreeNode grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.cs
/* Right rotation operation */\nTreeNode? RightRotate(TreeNode? node) {\n    TreeNode? child = node?.left;\n    TreeNode? grandChild = child?.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.go
/* Right rotation operation */\nfunc (t *aVLTree) rightRotate(node *TreeNode) *TreeNode {\n    child := node.Left\n    grandChild := child.Right\n    // Using child as pivot, rotate node to the right\n    child.Right = node\n    node.Left = grandChild\n    // Update node height\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.swift
/* Right rotation operation */\nfunc rightRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.left\n    let grandChild = child?.right\n    // Using child as pivot, rotate node to the right\n    child?.right = node\n    node?.left = grandChild\n    // Update node height\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.js
/* Right rotation operation */\n#rightRotate(node) {\n    const child = node.left;\n    const grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.ts
/* Right rotation operation */\nrightRotate(node: TreeNode): TreeNode {\n    const child = node.left;\n    const grandChild = child.right;\n    // Using child as pivot, rotate node to the right\n    child.right = node;\n    node.left = grandChild;\n    // Update node height\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.dart
/* Right rotation operation */\nTreeNode? rightRotate(TreeNode? node) {\n  TreeNode? child = node!.left;\n  TreeNode? grandChild = child!.right;\n  // Using child as pivot, rotate node to the right\n  child.right = node;\n  node.left = grandChild;\n  // Update node height\n  updateHeight(node);\n  updateHeight(child);\n  // Return root node of subtree after rotation\n  return child;\n}\n
avl_tree.rs
/* Right rotation operation */\nfn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().left.clone().unwrap();\n            let grand_child = child.borrow().right.clone();\n            // Using child as pivot, rotate node to the right\n            child.borrow_mut().right = Some(node.clone());\n            node.borrow_mut().left = grand_child;\n            // Update node height\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Return root node of subtree after rotation\n            Some(child)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* Right rotation operation */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->left;\n    grandChild = child->right;\n    // Using child as pivot, rotate node to the right\n    child->right = node;\n    node->left = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.kt
/* Right rotation operation */\nfun rightRotate(node: TreeNode?): TreeNode {\n    val child = node!!.left\n    val grandChild = child!!.right\n    // Using child as pivot, rotate node to the right\n    child.right = node\n    node.left = grandChild\n    // Update node height\n    updateHeight(node)\n    updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.rb
### Right rotation ###\ndef right_rotate(node)\n  child = node.left\n  grand_child = child.right\n  # Using child as pivot, rotate node to the right\n  child.right = node\n  node.left = grand_child\n  # Update node height\n  update_height(node)\n  update_height(child)\n  # Return root node of subtree after rotation\n  child\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-left-rotation","level":3,"title":"2.   Left Rotation","text":"

Correspondingly, if considering the \"mirror\" of the above unbalanced binary tree, the \"left rotation\" operation shown in Figure 7-28 needs to be performed.

Figure 7-28   Left rotation operation

Similarly, as shown in Figure 7-29, when the child node has a left child (denoted as grand_child), a step needs to be added in the left rotation: set grand_child as the right child of node.

Figure 7-29   Left rotation with grand_child

It can be observed that right rotation and left rotation operations are mirror symmetric in logic, and the two imbalance cases they solve are also symmetric. Based on symmetry, we only need to replace all left in the right rotation implementation code with right, and all right with left, to obtain the left rotation implementation code:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def left_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Left rotation operation\"\"\"\n    child = node.right\n    grand_child = child.left\n    # Using child as pivot, rotate node to the left\n    child.left = node\n    node.right = grand_child\n    # Update node height\n    self.update_height(node)\n    self.update_height(child)\n    # Return root node of subtree after rotation\n    return child\n
avl_tree.cpp
/* Left rotation operation */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child = node->right;\n    TreeNode *grandChild = child->left;\n    // Using child as pivot, rotate node to the left\n    child->left = node;\n    node->right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.java
/* Left rotation operation */\nTreeNode leftRotate(TreeNode node) {\n    TreeNode child = node.right;\n    TreeNode grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.cs
/* Left rotation operation */\nTreeNode? LeftRotate(TreeNode? node) {\n    TreeNode? child = node?.right;\n    TreeNode? grandChild = child?.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.go
/* Left rotation operation */\nfunc (t *aVLTree) leftRotate(node *TreeNode) *TreeNode {\n    child := node.Right\n    grandChild := child.Left\n    // Using child as pivot, rotate node to the left\n    child.Left = node\n    node.Right = grandChild\n    // Update node height\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.swift
/* Left rotation operation */\nfunc leftRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.right\n    let grandChild = child?.left\n    // Using child as pivot, rotate node to the left\n    child?.left = node\n    node?.right = grandChild\n    // Update node height\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.js
/* Left rotation operation */\n#leftRotate(node) {\n    const child = node.right;\n    const grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.ts
/* Left rotation operation */\nleftRotate(node: TreeNode): TreeNode {\n    const child = node.right;\n    const grandChild = child.left;\n    // Using child as pivot, rotate node to the left\n    child.left = node;\n    node.right = grandChild;\n    // Update node height\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.dart
/* Left rotation operation */\nTreeNode? leftRotate(TreeNode? node) {\n  TreeNode? child = node!.right;\n  TreeNode? grandChild = child!.left;\n  // Using child as pivot, rotate node to the left\n  child.left = node;\n  node.right = grandChild;\n  // Update node height\n  updateHeight(node);\n  updateHeight(child);\n  // Return root node of subtree after rotation\n  return child;\n}\n
avl_tree.rs
/* Left rotation operation */\nfn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().right.clone().unwrap();\n            let grand_child = child.borrow().left.clone();\n            // Using child as pivot, rotate node to the left\n            child.borrow_mut().left = Some(node.clone());\n            node.borrow_mut().right = grand_child;\n            // Update node height\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // Return root node of subtree after rotation\n            Some(child)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* Left rotation operation */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->right;\n    grandChild = child->left;\n    // Using child as pivot, rotate node to the left\n    child->left = node;\n    node->right = grandChild;\n    // Update node height\n    updateHeight(node);\n    updateHeight(child);\n    // Return root node of subtree after rotation\n    return child;\n}\n
avl_tree.kt
/* Left rotation operation */\nfun leftRotate(node: TreeNode?): TreeNode {\n    val child = node!!.right\n    val grandChild = child!!.left\n    // Using child as pivot, rotate node to the left\n    child.left = node\n    node.right = grandChild\n    // Update node height\n    updateHeight(node)\n    updateHeight(child)\n    // Return root node of subtree after rotation\n    return child\n}\n
avl_tree.rb
### Left rotation ###\ndef left_rotate(node)\n  child = node.right\n  grand_child = child.left\n  # Using child as pivot, rotate node to the left\n  child.left = node\n  node.right = grand_child\n  # Update node height\n  update_height(node)\n  update_height(child)\n  # Return root node of subtree after rotation\n  child\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3-left-rotation-then-right-rotation","level":3,"title":"3.   Left Rotation Then Right Rotation","text":"

For the unbalanced node 3 in Figure 7-30, using either left rotation or right rotation alone cannot restore the subtree to balance. In this case, a \"left rotation\" needs to be performed on child first, followed by a \"right rotation\" on node.

Figure 7-30   Left-right rotation

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4-right-rotation-then-left-rotation","level":3,"title":"4.   Right Rotation Then Left Rotation","text":"

As shown in Figure 7-31, for the mirror case of the above unbalanced binary tree, a \"right rotation\" needs to be performed on child first, then a \"left rotation\" on node.

Figure 7-31   Right-left rotation

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5-choice-of-rotation","level":3,"title":"5.   Choice of Rotation","text":"

The four imbalances shown in Figure 7-32 correspond one-to-one with the above cases, requiring right rotation, left rotation then right rotation, right rotation then left rotation, and left rotation operations respectively.

Figure 7-32   The four rotation cases of AVL tree

As shown in Table 7-3, we determine which case the unbalanced node belongs to by judging the signs of the balance factor of the unbalanced node and the balance factor of its taller-side child node.

Table 7-3   Conditions for Choosing Among the Four Rotation Cases

Balance factor of the unbalanced node Balance factor of the child node Rotation method to apply \\(> 1\\) (left-leaning tree) \\(\\geq 0\\) Right rotation \\(> 1\\) (left-leaning tree) \\(<0\\) Left rotation then right rotation \\(< -1\\) (right-leaning tree) \\(\\leq 0\\) Left rotation \\(< -1\\) (right-leaning tree) \\(>0\\) Right rotation then left rotation

For ease of use, we encapsulate the rotation operations into a function. With this function, we can perform rotations for various imbalance situations, restoring balance to unbalanced nodes. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"Perform rotation operation to restore balance to this subtree\"\"\"\n    # Get balance factor of node\n    balance_factor = self.balance_factor(node)\n    # Left-leaning tree\n    if balance_factor > 1:\n        if self.balance_factor(node.left) >= 0:\n            # Right rotation\n            return self.right_rotate(node)\n        else:\n            # First left rotation then right rotation\n            node.left = self.left_rotate(node.left)\n            return self.right_rotate(node)\n    # Right-leaning tree\n    elif balance_factor < -1:\n        if self.balance_factor(node.right) <= 0:\n            # Left rotation\n            return self.left_rotate(node)\n        else:\n            # First right rotation then left rotation\n            node.right = self.right_rotate(node.right)\n            return self.left_rotate(node)\n    # Balanced tree, no rotation needed, return directly\n    return node\n
avl_tree.cpp
/* Perform rotation operation to restore balance to this subtree */\nTreeNode *rotate(TreeNode *node) {\n    // Get balance factor of node\n    int _balanceFactor = balanceFactor(node);\n    // Left-leaning tree\n    if (_balanceFactor > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (_balanceFactor < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.java
/* Perform rotation operation to restore balance to this subtree */\nTreeNode rotate(TreeNode node) {\n    // Get balance factor of node\n    int balanceFactor = balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = leftRotate(node.left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = rightRotate(node.right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.cs
/* Perform rotation operation to restore balance to this subtree */\nTreeNode? Rotate(TreeNode? node) {\n    // Get balance factor of node\n    int balanceFactorInt = BalanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactorInt > 1) {\n        if (BalanceFactor(node?.left) >= 0) {\n            // Right rotation\n            return RightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node!.left = LeftRotate(node!.left);\n            return RightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactorInt < -1) {\n        if (BalanceFactor(node?.right) <= 0) {\n            // Left rotation\n            return LeftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node!.right = RightRotate(node!.right);\n            return LeftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.go
/* Perform rotation operation to restore balance to this subtree */\nfunc (t *aVLTree) rotate(node *TreeNode) *TreeNode {\n    // Get balance factor of node\n    // Go recommends short variables, here bf refers to t.balanceFactor\n    bf := t.balanceFactor(node)\n    // Left-leaning tree\n    if bf > 1 {\n        if t.balanceFactor(node.Left) >= 0 {\n            // Right rotation\n            return t.rightRotate(node)\n        } else {\n            // First left rotation then right rotation\n            node.Left = t.leftRotate(node.Left)\n            return t.rightRotate(node)\n        }\n    }\n    // Right-leaning tree\n    if bf < -1 {\n        if t.balanceFactor(node.Right) <= 0 {\n            // Left rotation\n            return t.leftRotate(node)\n        } else {\n            // First right rotation then left rotation\n            node.Right = t.rightRotate(node.Right)\n            return t.leftRotate(node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
avl_tree.swift
/* Perform rotation operation to restore balance to this subtree */\nfunc rotate(node: TreeNode?) -> TreeNode? {\n    // Get balance factor of node\n    let balanceFactor = balanceFactor(node: node)\n    // Left-leaning tree\n    if balanceFactor > 1 {\n        if self.balanceFactor(node: node?.left) >= 0 {\n            // Right rotation\n            return rightRotate(node: node)\n        } else {\n            // First left rotation then right rotation\n            node?.left = leftRotate(node: node?.left)\n            return rightRotate(node: node)\n        }\n    }\n    // Right-leaning tree\n    if balanceFactor < -1 {\n        if self.balanceFactor(node: node?.right) <= 0 {\n            // Left rotation\n            return leftRotate(node: node)\n        } else {\n            // First right rotation then left rotation\n            node?.right = rightRotate(node: node?.right)\n            return leftRotate(node: node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
avl_tree.js
/* Perform rotation operation to restore balance to this subtree */\n#rotate(node) {\n    // Get balance factor of node\n    const balanceFactor = this.balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return this.#rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = this.#leftRotate(node.left);\n            return this.#rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return this.#leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = this.#rightRotate(node.right);\n            return this.#leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.ts
/* Perform rotation operation to restore balance to this subtree */\nrotate(node: TreeNode): TreeNode {\n    // Get balance factor of node\n    const balanceFactor = this.balanceFactor(node);\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return this.rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node.left = this.leftRotate(node.left);\n            return this.rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return this.leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node.right = this.rightRotate(node.right);\n            return this.leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.dart
/* Perform rotation operation to restore balance to this subtree */\nTreeNode? rotate(TreeNode? node) {\n  // Get balance factor of node\n  int factor = balanceFactor(node);\n  // Left-leaning tree\n  if (factor > 1) {\n    if (balanceFactor(node!.left) >= 0) {\n      // Right rotation\n      return rightRotate(node);\n    } else {\n      // First left rotation then right rotation\n      node.left = leftRotate(node.left);\n      return rightRotate(node);\n    }\n  }\n  // Right-leaning tree\n  if (factor < -1) {\n    if (balanceFactor(node!.right) <= 0) {\n      // Left rotation\n      return leftRotate(node);\n    } else {\n      // First right rotation then left rotation\n      node.right = rightRotate(node.right);\n      return leftRotate(node);\n    }\n  }\n  // Balanced tree, no rotation needed, return directly\n  return node;\n}\n
avl_tree.rs
/* Perform rotation operation to restore balance to this subtree */\nfn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    // Get balance factor of node\n    let balance_factor = Self::balance_factor(node.clone());\n    // Left-leaning tree\n    if balance_factor > 1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().left.clone()) >= 0 {\n            // Right rotation\n            Self::right_rotate(Some(node))\n        } else {\n            // First left rotation then right rotation\n            let left = node.borrow().left.clone();\n            node.borrow_mut().left = Self::left_rotate(left);\n            Self::right_rotate(Some(node))\n        }\n    }\n    // Right-leaning tree\n    else if balance_factor < -1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().right.clone()) <= 0 {\n            // Left rotation\n            Self::left_rotate(Some(node))\n        } else {\n            // First right rotation then left rotation\n            let right = node.borrow().right.clone();\n            node.borrow_mut().right = Self::right_rotate(right);\n            Self::left_rotate(Some(node))\n        }\n    } else {\n        // Balanced tree, no rotation needed, return directly\n        node\n    }\n}\n
avl_tree.c
/* Perform rotation operation to restore balance to this subtree */\nTreeNode *rotate(TreeNode *node) {\n    // Get balance factor of node\n    int bf = balanceFactor(node);\n    // Left-leaning tree\n    if (bf > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // Right rotation\n            return rightRotate(node);\n        } else {\n            // First left rotation then right rotation\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // Right-leaning tree\n    if (bf < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // Left rotation\n            return leftRotate(node);\n        } else {\n            // First right rotation then left rotation\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node;\n}\n
avl_tree.kt
/* Perform rotation operation to restore balance to this subtree */\nfun rotate(node: TreeNode): TreeNode {\n    // Get balance factor of node\n    val balanceFactor = balanceFactor(node)\n    // Left-leaning tree\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // Right rotation\n            return rightRotate(node)\n        } else {\n            // First left rotation then right rotation\n            node.left = leftRotate(node.left)\n            return rightRotate(node)\n        }\n    }\n    // Right-leaning tree\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // Left rotation\n            return leftRotate(node)\n        } else {\n            // First right rotation then left rotation\n            node.right = rightRotate(node.right)\n            return leftRotate(node)\n        }\n    }\n    // Balanced tree, no rotation needed, return directly\n    return node\n}\n
avl_tree.rb
### Perform rotation to rebalance subtree ###\ndef rotate(node)\n  # Get balance factor of node\n  balance_factor = balance_factor(node)\n  # Left-heavy tree\n  if balance_factor > 1\n    if balance_factor(node.left) >= 0\n      # Right rotation\n      return right_rotate(node)\n    else\n      # First left rotation then right rotation\n      node.left = left_rotate(node.left)\n      return right_rotate(node)\n    end\n  # Right-heavy tree\n  elsif balance_factor < -1\n    if balance_factor(node.right) <= 0\n      # Left rotation\n      return left_rotate(node)\n    else\n      # First right rotation then left rotation\n      node.right = right_rotate(node.right)\n      return left_rotate(node)\n    end\n  end\n  # Balanced tree, no rotation needed, return directly\n  node\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#753-common-operations-in-avl-trees","level":2,"title":"7.5.3   Common Operations in Avl Trees","text":"","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1-node-insertion","level":3,"title":"1.   Node Insertion","text":"

The node insertion operation in AVL trees is similar in principle to that in binary search trees. The only difference is that after inserting a node in an AVL tree, a series of unbalanced nodes may appear on the path from that node to the root. Therefore, we need to start from this node and perform rotation operations from bottom to top, restoring balance to all unbalanced nodes. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def insert(self, val):\n    \"\"\"Insert node\"\"\"\n    self._root = self.insert_helper(self._root, val)\n\ndef insert_helper(self, node: TreeNode | None, val: int) -> TreeNode:\n    \"\"\"Recursively insert node (helper method)\"\"\"\n    if node is None:\n        return TreeNode(val)\n    # 1. Find insertion position and insert node\n    if val < node.val:\n        node.left = self.insert_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.insert_helper(node.right, val)\n    else:\n        # Duplicate node not inserted, return directly\n        return node\n    # Update node height\n    self.update_height(node)\n    # 2. Perform rotation operation to restore balance to this subtree\n    return self.rotate(node)\n
avl_tree.cpp
/* Insert node */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\n    if (val < node->val)\n        node->left = insertHelper(node->left, val);\n    else if (val > node->val)\n        node->right = insertHelper(node->right, val);\n    else\n        return node;    // Duplicate node not inserted, return directly\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.java
/* Insert node */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode insertHelper(TreeNode node, int val) {\n    if (node == null)\n        return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\n    if (val < node.val)\n        node.left = insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = insertHelper(node.right, val);\n    else\n        return node; // Duplicate node not inserted, return directly\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.cs
/* Insert node */\nvoid Insert(int val) {\n    root = InsertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode? InsertHelper(TreeNode? node, int val) {\n    if (node == null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\n    if (val < node.val)\n        node.left = InsertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = InsertHelper(node.right, val);\n    else\n        return node;     // Duplicate node not inserted, return directly\n    UpdateHeight(node);  // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = Rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.go
/* Insert node */\nfunc (t *aVLTree) insert(val int) {\n    t.root = t.insertHelper(t.root, val)\n}\n\n/* Recursively insert node (helper function) */\nfunc (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return NewTreeNode(val)\n    }\n    /* 1. Find insertion position and insert node */\n    if val < node.Val.(int) {\n        node.Left = t.insertHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.insertHelper(node.Right, val)\n    } else {\n        // Duplicate node not inserted, return directly\n        return node\n    }\n    // Update node height\n    t.updateHeight(node)\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = t.rotate(node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.swift
/* Insert node */\nfunc insert(val: Int) {\n    root = insertHelper(node: root, val: val)\n}\n\n/* Recursively insert node (helper method) */\nfunc insertHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return TreeNode(x: val)\n    }\n    /* 1. Find insertion position and insert node */\n    if val < node!.val {\n        node?.left = insertHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = insertHelper(node: node?.right, val: val)\n    } else {\n        return node // Duplicate node not inserted, return directly\n    }\n    updateHeight(node: node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node: node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.js
/* Insert node */\ninsert(val) {\n    this.root = this.#insertHelper(this.root, val);\n}\n\n/* Recursively insert node (helper method) */\n#insertHelper(node, val) {\n    if (node === null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\n    if (val < node.val) node.left = this.#insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#insertHelper(node.right, val);\n    else return node; // Duplicate node not inserted, return directly\n    this.#updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.#rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.ts
/* Insert node */\ninsert(val: number): void {\n    this.root = this.insertHelper(this.root, val);\n}\n\n/* Recursively insert node (helper method) */\ninsertHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return new TreeNode(val);\n    /* 1. Find insertion position and insert node */\n    if (val < node.val) {\n        node.left = this.insertHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.insertHelper(node.right, val);\n    } else {\n        return node; // Duplicate node not inserted, return directly\n    }\n    this.updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.dart
/* Insert node */\nvoid insert(int val) {\n  root = insertHelper(root, val);\n}\n\n/* Recursively insert node (helper method) */\nTreeNode? insertHelper(TreeNode? node, int val) {\n  if (node == null) return TreeNode(val);\n  /* 1. Find insertion position and insert node */\n  if (val < node.val)\n    node.left = insertHelper(node.left, val);\n  else if (val > node.val)\n    node.right = insertHelper(node.right, val);\n  else\n    return node; // Duplicate node not inserted, return directly\n  updateHeight(node); // Update node height\n  /* 2. Perform rotation operation to restore balance to this subtree */\n  node = rotate(node);\n  // Return root node of subtree\n  return node;\n}\n
avl_tree.rs
/* Insert node */\nfn insert(&mut self, val: i32) {\n    self.root = Self::insert_helper(self.root.clone(), val);\n}\n\n/* Recursively insert node (helper method) */\nfn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Find insertion position and insert node */\n            match {\n                let node_val = node.borrow().val;\n                node_val\n            }\n            .cmp(&val)\n            {\n                Ordering::Greater => {\n                    let left = node.borrow().left.clone();\n                    node.borrow_mut().left = Self::insert_helper(left, val);\n                }\n                Ordering::Less => {\n                    let right = node.borrow().right.clone();\n                    node.borrow_mut().right = Self::insert_helper(right, val);\n                }\n                Ordering::Equal => {\n                    return Some(node); // Duplicate node not inserted, return directly\n                }\n            }\n            Self::update_height(Some(node.clone())); // Update node height\n\n            /* 2. Perform rotation operation to restore balance to this subtree */\n            node = Self::rotate(Some(node)).unwrap();\n            // Return root node of subtree\n            Some(node)\n        }\n        None => Some(TreeNode::new(val)),\n    }\n}\n
avl_tree.c
/* Insert node */\nvoid insert(AVLTree *tree, int val) {\n    tree->root = insertHelper(tree->root, val);\n}\n\n/* Recursively insert node (helper function) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == NULL) {\n        return newTreeNode(val);\n    }\n    /* 1. Find insertion position and insert node */\n    if (val < node->val) {\n        node->left = insertHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = insertHelper(node->right, val);\n    } else {\n        // Duplicate node not inserted, return directly\n        return node;\n    }\n    // Update node height\n    updateHeight(node);\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.kt
/* Insert node */\nfun insert(_val: Int) {\n    root = insertHelper(root, _val)\n}\n\n/* Recursively insert node (helper method) */\nfun insertHelper(n: TreeNode?, _val: Int): TreeNode {\n    if (n == null)\n        return TreeNode(_val)\n    var node = n\n    /* 1. Find insertion position and insert node */\n    if (_val < node._val)\n        node.left = insertHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = insertHelper(node.right, _val)\n    else\n        return node // Duplicate node not inserted, return directly\n    updateHeight(node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.rb
### Insert node ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n### Recursively insert node (helper method) ###\ndef insert_helper(node, val)\n  return TreeNode.new(val) if node.nil?\n  # 1. Find insertion position and insert node\n  if val < node.val\n    node.left = insert_helper(node.left, val)\n  elsif val > node.val\n    node.right = insert_helper(node.right, val)\n  else\n    # Duplicate node not inserted, return directly\n    return node\n  end\n  # Update node height\n  update_height(node)\n  # 2. Perform rotation operation to restore balance to this subtree\n  rotate(node)\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2-node-removal","level":3,"title":"2.   Node Removal","text":"

Similarly, on the basis of the binary search tree's node removal method, rotation operations need to be performed from bottom to top to restore balance to all unbalanced nodes. The code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def remove(self, val: int):\n    \"\"\"Delete node\"\"\"\n    self._root = self.remove_helper(self._root, val)\n\ndef remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None:\n    \"\"\"Recursively delete node (helper method)\"\"\"\n    if node is None:\n        return None\n    # 1. Find node and delete\n    if val < node.val:\n        node.left = self.remove_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.remove_helper(node.right, val)\n    else:\n        if node.left is None or node.right is None:\n            child = node.left or node.right\n            # Number of child nodes = 0, delete node directly and return\n            if child is None:\n                return None\n            # Number of child nodes = 1, delete node directly\n            else:\n                node = child\n        else:\n            # Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            temp = node.right\n            while temp.left is not None:\n                temp = temp.left\n            node.right = self.remove_helper(node.right, temp.val)\n            node.val = temp.val\n    # Update node height\n    self.update_height(node)\n    # 2. Perform rotation operation to restore balance to this subtree\n    return self.rotate(node)\n
avl_tree.cpp
/* Remove node */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return nullptr;\n    /* 1. Find node and delete */\n    if (val < node->val)\n        node->left = removeHelper(node->left, val);\n    else if (val > node->val)\n        node->right = removeHelper(node->right, val);\n    else {\n        if (node->left == nullptr || node->right == nullptr) {\n            TreeNode *child = node->left != nullptr ? node->left : node->right;\n            // Number of child nodes = 0, delete node directly and return\n            if (child == nullptr) {\n                delete node;\n                return nullptr;\n            }\n            // Number of child nodes = 1, delete node directly\n            else {\n                delete node;\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            TreeNode *temp = node->right;\n            while (temp->left != nullptr) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.java
/* Remove node */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode removeHelper(TreeNode node, int val) {\n    if (node == null)\n        return null;\n    /* 1. Find node and delete */\n    if (val < node.val)\n        node.left = removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = removeHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode child = node.left != null ? node.left : node.right;\n            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null;\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            TreeNode temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.cs
/* Remove node */\nvoid Remove(int val) {\n    root = RemoveHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode? RemoveHelper(TreeNode? node, int val) {\n    if (node == null) return null;\n    /* 1. Find node and delete */\n    if (val < node.val)\n        node.left = RemoveHelper(node.left, val);\n    else if (val > node.val)\n        node.right = RemoveHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode? child = node.left ?? node.right;\n            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null;\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            TreeNode? temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = RemoveHelper(node.right, temp.val!.Value);\n            node.val = temp.val;\n        }\n    }\n    UpdateHeight(node);  // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = Rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.go
/* Remove node */\nfunc (t *aVLTree) remove(val int) {\n    t.root = t.removeHelper(t.root, val)\n}\n\n/* Recursively remove node (helper function) */\nfunc (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return nil\n    }\n    /* 1. Find node and delete */\n    if val < node.Val.(int) {\n        node.Left = t.removeHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.removeHelper(node.Right, val)\n    } else {\n        if node.Left == nil || node.Right == nil {\n            child := node.Left\n            if node.Right != nil {\n                child = node.Right\n            }\n            if child == nil {\n                // Number of child nodes = 0, delete node directly and return\n                return nil\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            temp := node.Right\n            for temp.Left != nil {\n                temp = temp.Left\n            }\n            node.Right = t.removeHelper(node.Right, temp.Val.(int))\n            node.Val = temp.Val\n        }\n    }\n    // Update node height\n    t.updateHeight(node)\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = t.rotate(node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.swift
/* Remove node */\nfunc remove(val: Int) {\n    root = removeHelper(node: root, val: val)\n}\n\n/* Recursively delete node (helper method) */\nfunc removeHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return nil\n    }\n    /* 1. Find node and delete */\n    if val < node!.val {\n        node?.left = removeHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = removeHelper(node: node?.right, val: val)\n    } else {\n        if node?.left == nil || node?.right == nil {\n            let child = node?.left ?? node?.right\n            // Number of child nodes = 0, delete node directly and return\n            if child == nil {\n                return nil\n            }\n            // Number of child nodes = 1, delete node directly\n            else {\n                node = child\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            var temp = node?.right\n            while temp?.left != nil {\n                temp = temp?.left\n            }\n            node?.right = removeHelper(node: node?.right, val: temp!.val)\n            node?.val = temp!.val\n        }\n    }\n    updateHeight(node: node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node: node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.js
/* Remove node */\nremove(val) {\n    this.root = this.#removeHelper(this.root, val);\n}\n\n/* Recursively delete node (helper method) */\n#removeHelper(node, val) {\n    if (node === null) return null;\n    /* 1. Find node and delete */\n    if (val < node.val) node.left = this.#removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#removeHelper(node.right, val);\n    else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // Number of child nodes = 0, delete node directly and return\n            if (child === null) return null;\n            // Number of child nodes = 1, delete node directly\n            else node = child;\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.#removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.#updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.#rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.ts
/* Remove node */\nremove(val: number): void {\n    this.root = this.removeHelper(this.root, val);\n}\n\n/* Recursively delete node (helper method) */\nremoveHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return null;\n    /* 1. Find node and delete */\n    if (val < node.val) {\n        node.left = this.removeHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.removeHelper(node.right, val);\n    } else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // Number of child nodes = 0, delete node directly and return\n            if (child === null) {\n                return null;\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.updateHeight(node); // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = this.rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.dart
/* Remove node */\nvoid remove(int val) {\n  root = removeHelper(root, val);\n}\n\n/* Recursively delete node (helper method) */\nTreeNode? removeHelper(TreeNode? node, int val) {\n  if (node == null) return null;\n  /* 1. Find node and delete */\n  if (val < node.val)\n    node.left = removeHelper(node.left, val);\n  else if (val > node.val)\n    node.right = removeHelper(node.right, val);\n  else {\n    if (node.left == null || node.right == null) {\n      TreeNode? child = node.left ?? node.right;\n      // Number of child nodes = 0, delete node directly and return\n      if (child == null)\n        return null;\n      // Number of child nodes = 1, delete node directly\n      else\n        node = child;\n    } else {\n      // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n      TreeNode? temp = node.right;\n      while (temp!.left != null) {\n        temp = temp.left;\n      }\n      node.right = removeHelper(node.right, temp.val);\n      node.val = temp.val;\n    }\n  }\n  updateHeight(node); // Update node height\n  /* 2. Perform rotation operation to restore balance to this subtree */\n  node = rotate(node);\n  // Return root node of subtree\n  return node;\n}\n
avl_tree.rs
/* Remove node */\nfn remove(&self, val: i32) {\n    Self::remove_helper(self.root.clone(), val);\n}\n\n/* Recursively delete node (helper method) */\nfn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. Find node and delete */\n            if val < node.borrow().val {\n                let left = node.borrow().left.clone();\n                node.borrow_mut().left = Self::remove_helper(left, val);\n            } else if val > node.borrow().val {\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, val);\n            } else if node.borrow().left.is_none() || node.borrow().right.is_none() {\n                let child = if node.borrow().left.is_some() {\n                    node.borrow().left.clone()\n                } else {\n                    node.borrow().right.clone()\n                };\n                match child {\n                    // Number of child nodes = 0, delete node directly and return\n                    None => {\n                        return None;\n                    }\n                    // Number of child nodes = 1, delete node directly\n                    Some(child) => node = child,\n                }\n            } else {\n                // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n                let mut temp = node.borrow().right.clone().unwrap();\n                loop {\n                    let temp_left = temp.borrow().left.clone();\n                    if temp_left.is_none() {\n                        break;\n                    }\n                    temp = temp_left.unwrap();\n                }\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val);\n                node.borrow_mut().val = temp.borrow().val;\n            }\n            Self::update_height(Some(node.clone())); // Update node height\n\n            /* 2. Perform rotation operation to restore balance to this subtree */\n            node = Self::rotate(Some(node)).unwrap();\n            // Return root node of subtree\n            Some(node)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* Remove node */\n// Cannot use remove keyword here due to stdio.h inclusion\nvoid removeItem(AVLTree *tree, int val) {\n    TreeNode *root = removeHelper(tree->root, val);\n}\n\n/* Recursively remove node (helper function) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    TreeNode *child, *grandChild;\n    if (node == NULL) {\n        return NULL;\n    }\n    /* 1. Find node and delete */\n    if (val < node->val) {\n        node->left = removeHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = removeHelper(node->right, val);\n    } else {\n        if (node->left == NULL || node->right == NULL) {\n            child = node->left;\n            if (node->right != NULL) {\n                child = node->right;\n            }\n            // Number of child nodes = 0, delete node directly and return\n            if (child == NULL) {\n                return NULL;\n            } else {\n                // Number of child nodes = 1, delete node directly\n                node = child;\n            }\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            TreeNode *temp = node->right;\n            while (temp->left != NULL) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    // Update node height\n    updateHeight(node);\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node);\n    // Return root node of subtree\n    return node;\n}\n
avl_tree.kt
/* Remove node */\nfun remove(_val: Int) {\n    root = removeHelper(root, _val)\n}\n\n/* Recursively delete node (helper method) */\nfun removeHelper(n: TreeNode?, _val: Int): TreeNode? {\n    var node = n ?: return null\n    /* 1. Find node and delete */\n    if (_val < node._val)\n        node.left = removeHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = removeHelper(node.right, _val)\n    else {\n        if (node.left == null || node.right == null) {\n            val child = if (node.left != null)\n                node.left\n            else\n                node.right\n            // Number of child nodes = 0, delete node directly and return\n            if (child == null)\n                return null\n            // Number of child nodes = 1, delete node directly\n            else\n                node = child\n        } else {\n            // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n            var temp = node.right\n            while (temp!!.left != null) {\n                temp = temp.left\n            }\n            node.right = removeHelper(node.right, temp._val)\n            node._val = temp._val\n        }\n    }\n    updateHeight(node) // Update node height\n    /* 2. Perform rotation operation to restore balance to this subtree */\n    node = rotate(node)\n    // Return root node of subtree\n    return node\n}\n
avl_tree.rb
### Delete node ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n### Recursively delete node (helper method) ###\ndef remove_helper(node, val)\n  return if node.nil?\n  # 1. Find node and delete\n  if val < node.val\n    node.left = remove_helper(node.left, val)\n  elsif val > node.val\n    node.right = remove_helper(node.right, val)\n  else\n    if node.left.nil? || node.right.nil?\n      child = node.left || node.right\n      # Number of child nodes = 0, delete node directly and return\n      return if child.nil?\n      # Number of child nodes = 1, delete node directly\n      node = child\n    else\n      # Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it\n      temp = node.right\n      while !temp.left.nil?\n        temp = temp.left\n      end\n      node.right = remove_helper(node.right, temp.val)\n      node.val = temp.val\n    end\n  end\n  # Update node height\n  update_height(node)\n  # 2. Perform rotation operation to restore balance to this subtree\n  rotate(node)\nend\n
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3-node-search","level":3,"title":"3.   Node Search","text":"

The node search operation in AVL trees is consistent with that in binary search trees, and will not be elaborated here.

","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/avl_tree/#754-typical-applications-of-avl-trees","level":2,"title":"7.5.4   Typical Applications of Avl Trees","text":"
  • Organizing and storing large-scale data, suitable for scenarios with high-frequency searches and low-frequency insertions and deletions.
  • Used to build index systems in databases.
  • Red-black trees are also a common type of balanced binary search tree. Compared to AVL trees, red-black trees have more relaxed balance conditions, require fewer rotation operations for node insertion and deletion, and have higher average efficiency for node addition and deletion operations.
","path":["Chapter 7. Tree","7.5   Avl Tree *"],"tags":[]},{"location":"chapter_tree/binary_search_tree/","level":1,"title":"7.4   Binary Search Tree","text":"

As shown in Figure 7-16, a binary search tree satisfies the following conditions.

  1. For the root node, the value of all nodes in the left subtree \\(<\\) the value of the root node \\(<\\) the value of all nodes in the right subtree.
  2. The left and right subtrees of any node are also binary search trees, i.e., they satisfy condition 1. as well.

Figure 7-16   Binary search tree

","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#741-operations-on-a-binary-search-tree","level":2,"title":"7.4.1   Operations on a Binary Search Tree","text":"

We encapsulate the binary search tree as a class BinarySearchTree and declare a member variable root pointing to the tree's root node.

","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#1-searching-for-a-node","level":3,"title":"1.   Searching for a Node","text":"

Given a target node value num, we can search according to the properties of the binary search tree. As shown in Figure 7-17, we declare a node cur and start from the binary tree's root node root, looping to compare the node value cur.val with num.

  • If cur.val < num, it means the target node is in cur's right subtree, thus execute cur = cur.right.
  • If cur.val > num, it means the target node is in cur's left subtree, thus execute cur = cur.left.
  • If cur.val = num, it means the target node is found, exit the loop, and return the node.
<1><2><3><4>

Figure 7-17   Example of searching for a node in a binary search tree

The search operation in a binary search tree works on the same principle as the binary search algorithm, both eliminating half of the cases in each round. The number of loop iterations is at most the height of the binary tree. When the binary tree is balanced, it uses \\(O(\\log n)\\) time. The example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def search(self, num: int) -> TreeNode | None:\n    \"\"\"Search node\"\"\"\n    cur = self._root\n    # Loop search, exit after passing leaf node\n    while cur is not None:\n        # Target node is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Target node is in cur's left subtree\n        elif cur.val > num:\n            cur = cur.left\n        # Found target node, exit loop\n        else:\n            break\n    return cur\n
binary_search_tree.cpp
/* Search node */\nTreeNode *search(int num) {\n    TreeNode *cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Target node is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Target node is in cur's left subtree\n        else if (cur->val > num)\n            cur = cur->left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.java
/* Search node */\nTreeNode search(int num) {\n    TreeNode cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num)\n            cur = cur.left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.cs
/* Search node */\nTreeNode? Search(int num) {\n    TreeNode? cur = root;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur =\n            cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num)\n            cur = cur.left;\n        // Found target node, exit loop\n        else\n            break;\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.go
/* Search node */\nfunc (bst *binarySearchTree) search(num int) *TreeNode {\n    node := bst.root\n    // Loop search, exit after passing leaf node\n    for node != nil {\n        if node.Val.(int) < num {\n            // Target node is in cur's right subtree\n            node = node.Right\n        } else if node.Val.(int) > num {\n            // Target node is in cur's left subtree\n            node = node.Left\n        } else {\n            // Found target node, exit loop\n            break\n        }\n    }\n    // Return target node\n    return node\n}\n
binary_search_tree.swift
/* Search node */\nfunc search(num: Int) -> TreeNode? {\n    var cur = root\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Target node is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Target node is in cur's left subtree\n        else if cur!.val > num {\n            cur = cur?.left\n        }\n        // Found target node, exit loop\n        else {\n            break\n        }\n    }\n    // Return target node\n    return cur\n}\n
binary_search_tree.js
/* Search node */\nsearch(num) {\n    let cur = this.root;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num) cur = cur.left;\n        // Found target node, exit loop\n        else break;\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.ts
/* Search node */\nsearch(num: number): TreeNode | null {\n    let cur = this.root;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Target node is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Target node is in cur's left subtree\n        else if (cur.val > num) cur = cur.left;\n        // Found target node, exit loop\n        else break;\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.dart
/* Search node */\nTreeNode? search(int _num) {\n  TreeNode? cur = _root;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Target node is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Target node is in cur's left subtree\n    else if (cur.val > _num)\n      cur = cur.left;\n    // Found target node, exit loop\n    else\n      break;\n  }\n  // Return target node\n  return cur;\n}\n
binary_search_tree.rs
/* Search node */\npub fn search(&self, num: i32) -> OptionTreeNodeRc {\n    let mut cur = self.root.clone();\n    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Target node is in cur's right subtree\n            Ordering::Greater => cur = node.borrow().right.clone(),\n            // Target node is in cur's left subtree\n            Ordering::Less => cur = node.borrow().left.clone(),\n            // Found target node, exit loop\n            Ordering::Equal => break,\n        }\n    }\n\n    // Return target node\n    cur\n}\n
binary_search_tree.c
/* Search node */\nTreeNode *search(BinarySearchTree *bst, int num) {\n    TreeNode *cur = bst->root;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        if (cur->val < num) {\n            // Target node is in cur's right subtree\n            cur = cur->right;\n        } else if (cur->val > num) {\n            // Target node is in cur's left subtree\n            cur = cur->left;\n        } else {\n            // Found target node, exit loop\n            break;\n        }\n    }\n    // Return target node\n    return cur;\n}\n
binary_search_tree.kt
/* Search node */\nfun search(num: Int): TreeNode? {\n    var cur = root\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Target node is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Target node is in cur's left subtree\n        else if (cur._val > num)\n            cur.left\n        // Found target node, exit loop\n        else\n            break\n    }\n    // Return target node\n    return cur\n}\n
binary_search_tree.rb
### Search node ###\ndef search(num)\n  cur = @root\n\n  # Loop search, exit after passing leaf node\n  while !cur.nil?\n    # Target node is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Target node is in cur's left subtree\n    elsif cur.val > num\n      cur = cur.left\n    # Found target node, exit loop\n    else\n      break\n    end\n  end\n\n  cur\nend\n
","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#2-inserting-a-node","level":3,"title":"2.   Inserting a Node","text":"

Given an element num to be inserted, in order to maintain the property of the binary search tree \"left subtree < root node < right subtree,\" the insertion process is as shown in Figure 7-18.

  1. Finding the insertion position: Similar to the search operation, start from the root node and loop downward searching according to the size relationship between the current node value and num, until passing the leaf node (traversing to None) and then exit the loop.
  2. Insert the node at that position: Initialize node num and place it at the None position.

Figure 7-18   Inserting a node into a binary search tree

In the code implementation, note the following two points:

  • Binary search trees do not allow duplicate nodes; otherwise, it would violate its definition. Therefore, if the node to be inserted already exists in the tree, the insertion is not performed and it returns directly.
  • To implement the node insertion, we need to use node pre to save the node from the previous loop iteration. This way, when traversing to None, we can obtain its parent node, thereby completing the node insertion operation.
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def insert(self, num: int):\n    \"\"\"Insert node\"\"\"\n    # If tree is empty, initialize root node\n    if self._root is None:\n        self._root = TreeNode(num)\n        return\n    # Loop search, exit after passing leaf node\n    cur, pre = self._root, None\n    while cur is not None:\n        # Found duplicate node, return directly\n        if cur.val == num:\n            return\n        pre = cur\n        # Insertion position is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Insertion position is in cur's left subtree\n        else:\n            cur = cur.left\n    # Insert node\n    node = TreeNode(num)\n    if pre.val < num:\n        pre.right = node\n    else:\n        pre.left = node\n
binary_search_tree.cpp
/* Insert node */\nvoid insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == nullptr) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode *cur = root, *pre = nullptr;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Found duplicate node, return directly\n        if (cur->val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur->left;\n    }\n    // Insert node\n    TreeNode *node = new TreeNode(num);\n    if (pre->val < num)\n        pre->right = node;\n    else\n        pre->left = node;\n}\n
binary_search_tree.java
/* Insert node */\nvoid insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // Insert node\n    TreeNode node = new TreeNode(num);\n    if (pre.val < num)\n        pre.right = node;\n    else\n        pre.left = node;\n}\n
binary_search_tree.cs
/* Insert node */\nvoid Insert(int num) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode? cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n\n    // Insert node\n    TreeNode node = new(num);\n    if (pre != null) {\n        if (pre.val < num)\n            pre.right = node;\n        else\n            pre.left = node;\n    }\n}\n
binary_search_tree.go
/* Insert node */\nfunc (bst *binarySearchTree) insert(num int) {\n    cur := bst.root\n    // If tree is empty, initialize root node\n    if cur == nil {\n        bst.root = NewTreeNode(num)\n        return\n    }\n    // Node position before the node to be inserted\n    var pre *TreeNode = nil\n    // Loop search, exit after passing leaf node\n    for cur != nil {\n        if cur.Val == num {\n            return\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            cur = cur.Right\n        } else {\n            cur = cur.Left\n        }\n    }\n    // Insert node\n    node := NewTreeNode(num)\n    if pre.Val.(int) < num {\n        pre.Right = node\n    } else {\n        pre.Left = node\n    }\n}\n
binary_search_tree.swift
/* Insert node */\nfunc insert(num: Int) {\n    // If tree is empty, initialize root node\n    if root == nil {\n        root = TreeNode(x: num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Found duplicate node, return directly\n        if cur!.val == num {\n            return\n        }\n        pre = cur\n        // Insertion position is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Insertion position is in cur's left subtree\n        else {\n            cur = cur?.left\n        }\n    }\n    // Insert node\n    let node = TreeNode(x: num)\n    if pre!.val < num {\n        pre?.right = node\n    } else {\n        pre?.left = node\n    }\n}\n
binary_search_tree.js
/* Insert node */\ninsert(num) {\n    // If tree is empty, initialize root node\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur = this.root,\n        pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found duplicate node, return directly\n        if (cur.val === num) return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else cur = cur.left;\n    }\n    // Insert node\n    const node = new TreeNode(num);\n    if (pre.val < num) pre.right = node;\n    else pre.left = node;\n}\n
binary_search_tree.ts
/* Insert node */\ninsert(num: number): void {\n    // If tree is empty, initialize root node\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found duplicate node, return directly\n        if (cur.val === num) return;\n        pre = cur;\n        // Insertion position is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Insertion position is in cur's left subtree\n        else cur = cur.left;\n    }\n    // Insert node\n    const node = new TreeNode(num);\n    if (pre!.val < num) pre!.right = node;\n    else pre!.left = node;\n}\n
binary_search_tree.dart
/* Insert node */\nvoid insert(int _num) {\n  // If tree is empty, initialize root node\n  if (_root == null) {\n    _root = TreeNode(_num);\n    return;\n  }\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Found duplicate node, return directly\n    if (cur.val == _num) return;\n    pre = cur;\n    // Insertion position is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Insertion position is in cur's left subtree\n    else\n      cur = cur.left;\n  }\n  // Insert node\n  TreeNode? node = TreeNode(_num);\n  if (pre!.val < _num)\n    pre.right = node;\n  else\n    pre.left = node;\n}\n
binary_search_tree.rs
/* Insert node */\npub fn insert(&mut self, num: i32) {\n    // If tree is empty, initialize root node\n    if self.root.is_none() {\n        self.root = Some(TreeNode::new(num));\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Found duplicate node, return directly\n            Ordering::Equal => return,\n            // Insertion position is in cur's right subtree\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Insertion position is in cur's left subtree\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // Insert node\n    let pre = pre.unwrap();\n    let node = Some(TreeNode::new(num));\n    if num > pre.borrow().val {\n        pre.borrow_mut().right = node;\n    } else {\n        pre.borrow_mut().left = node;\n    }\n}\n
binary_search_tree.c
/* Insert node */\nvoid insert(BinarySearchTree *bst, int num) {\n    // If tree is empty, initialize root node\n    if (bst->root == NULL) {\n        bst->root = newTreeNode(num);\n        return;\n    }\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        // Found duplicate node, return directly\n        if (cur->val == num) {\n            return;\n        }\n        pre = cur;\n        if (cur->val < num) {\n            // Insertion position is in cur's right subtree\n            cur = cur->right;\n        } else {\n            // Insertion position is in cur's left subtree\n            cur = cur->left;\n        }\n    }\n    // Insert node\n    TreeNode *node = newTreeNode(num);\n    if (pre->val < num) {\n        pre->right = node;\n    } else {\n        pre->left = node;\n    }\n}\n
binary_search_tree.kt
/* Insert node */\nfun insert(num: Int) {\n    // If tree is empty, initialize root node\n    if (root == null) {\n        root = TreeNode(num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode? = null\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found duplicate node, return directly\n        if (cur._val == num)\n            return\n        pre = cur\n        // Insertion position is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Insertion position is in cur's left subtree\n        else\n            cur.left\n    }\n    // Insert node\n    val node = TreeNode(num)\n    if (pre?._val!! < num)\n        pre.right = node\n    else\n        pre.left = node\n}\n
binary_search_tree.rb
### Insert node ###\ndef insert(num)\n  # If tree is empty, initialize root node\n  if @root.nil?\n    @root = TreeNode.new(num)\n    return\n  end\n\n  # Loop search, exit after passing leaf node\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Found duplicate node, return directly\n    return if cur.val == num\n\n    pre = cur\n    # Insertion position is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Insertion position is in cur's left subtree\n    else\n      cur = cur.left\n    end\n  end\n\n  # Insert node\n  node = TreeNode.new(num)\n  if pre.val < num\n    pre.right = node\n  else\n    pre.left = node\n  end\nend\n

Similar to searching for a node, inserting a node uses \\(O(\\log n)\\) time.

","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#3-removing-a-node","level":3,"title":"3.   Removing a Node","text":"

First, find the target node in the binary tree, then remove it. Similar to node insertion, we need to ensure that after the removal operation is completed, the binary search tree's property of \"left subtree \\(<\\) root node \\(<\\) right subtree\" is still maintained. Therefore, depending on the number of child nodes the target node has, we divide it into 0, 1, and 2 three cases, and execute the corresponding node removal operations.

As shown in Figure 7-19, when the degree of the node to be removed is \\(0\\), it means the node is a leaf node and can be directly removed.

Figure 7-19   Removing a node in a binary search tree (degree 0)

As shown in Figure 7-20, when the degree of the node to be removed is \\(1\\), replacing the node to be removed with its child node is sufficient.

Figure 7-20   Removing a node in a binary search tree (degree 1)

When the degree of the node to be removed is \\(2\\), we cannot directly remove it; instead, we need to use a node to replace it. To maintain the binary search tree's property of \"left subtree \\(<\\) root node \\(<\\) right subtree,\" this node can be either the smallest node in the right subtree or the largest node in the left subtree.

Assuming we choose the smallest node in the right subtree (the next node in the inorder traversal), the removal process is as shown in Figure 7-21.

  1. Find the next node of the node to be removed in the \"inorder traversal sequence,\" denoted as tmp.
  2. Replace the value of the node to be removed with the value of tmp, and recursively remove node tmp in the tree.
<1><2><3><4>

Figure 7-21   Removing a node in a binary search tree (degree 2)

The node removal operation also uses \\(O(\\log n)\\) time, where finding the node to be removed requires \\(O(\\log n)\\) time, and obtaining the inorder successor node requires \\(O(\\log n)\\) time. Example code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def remove(self, num: int):\n    \"\"\"Delete node\"\"\"\n    # If tree is empty, return directly\n    if self._root is None:\n        return\n    # Loop search, exit after passing leaf node\n    cur, pre = self._root, None\n    while cur is not None:\n        # Found node to delete, exit loop\n        if cur.val == num:\n            break\n        pre = cur\n        # Node to delete is in cur's right subtree\n        if cur.val < num:\n            cur = cur.right\n        # Node to delete is in cur's left subtree\n        else:\n            cur = cur.left\n    # If no node to delete, return directly\n    if cur is None:\n        return\n\n    # Number of child nodes = 0 or 1\n    if cur.left is None or cur.right is None:\n        # When number of child nodes = 0 / 1, child = null / that child node\n        child = cur.left or cur.right\n        # Delete node cur\n        if cur != self._root:\n            if pre.left == cur:\n                pre.left = child\n            else:\n                pre.right = child\n        else:\n            # If deleted node is root node, reassign root node\n            self._root = child\n    # Number of child nodes = 2\n    else:\n        # Get next node of cur in inorder traversal\n        tmp: TreeNode = cur.right\n        while tmp.left is not None:\n            tmp = tmp.left\n        # Recursively delete node tmp\n        self.remove(tmp.val)\n        # Replace cur with tmp\n        cur.val = tmp.val\n
binary_search_tree.cpp
/* Remove node */\nvoid remove(int num) {\n    // If tree is empty, return directly\n    if (root == nullptr)\n        return;\n    TreeNode *cur = root, *pre = nullptr;\n    // Loop search, exit after passing leaf node\n    while (cur != nullptr) {\n        // Found node to delete, exit loop\n        if (cur->val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur->val < num)\n            cur = cur->right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur->left;\n    }\n    // If no node to delete, return directly\n    if (cur == nullptr)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur->left == nullptr || cur->right == nullptr) {\n        // When number of child nodes = 0 / 1, child = nullptr / that child node\n        TreeNode *child = cur->left != nullptr ? cur->left : cur->right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre->left == cur)\n                pre->left = child;\n            else\n                pre->right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n        // Free memory\n        delete cur;\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode *tmp = cur->right;\n        while (tmp->left != nullptr) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Recursively delete node tmp\n        remove(tmp->val);\n        // Replace cur with tmp\n        cur->val = tmpVal;\n    }\n}\n
binary_search_tree.java
/* Remove node */\nvoid remove(int num) {\n    // If tree is empty, return directly\n    if (root == null)\n        return;\n    TreeNode cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        TreeNode child = cur.left != null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        remove(tmp.val);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.cs
/* Remove node */\nvoid Remove(int num) {\n    // If tree is empty, return directly\n    if (root == null)\n        return;\n    TreeNode? cur = root, pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num)\n            cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else\n            cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return;\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        TreeNode? child = cur.left ?? cur.right;\n        // Delete node cur\n        if (cur != root) {\n            if (pre!.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        TreeNode? tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        Remove(tmp.val!.Value);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.go
/* Remove node */\nfunc (bst *binarySearchTree) remove(num int) {\n    cur := bst.root\n    // If tree is empty, return directly\n    if cur == nil {\n        return\n    }\n    // Node position before the node to be removed\n    var pre *TreeNode = nil\n    // Loop search, exit after passing leaf node\n    for cur != nil {\n        if cur.Val == num {\n            break\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            // Node to be removed is in right subtree\n            cur = cur.Right\n        } else {\n            // Node to be removed is in left subtree\n            cur = cur.Left\n        }\n    }\n    // If no node to delete, return directly\n    if cur == nil {\n        return\n    }\n    // Number of child nodes is 0 or 1\n    if cur.Left == nil || cur.Right == nil {\n        var child *TreeNode = nil\n        // Get child node of node to be removed\n        if cur.Left != nil {\n            child = cur.Left\n        } else {\n            child = cur.Right\n        }\n        // Delete node cur\n        if cur != bst.root {\n            if pre.Left == cur {\n                pre.Left = child\n            } else {\n                pre.Right = child\n            }\n        } else {\n            // If deleted node is root node, reassign root node\n            bst.root = child\n        }\n        // Number of child nodes is 2\n    } else {\n        // Get next node of node cur to be removed in in-order traversal\n        tmp := cur.Right\n        for tmp.Left != nil {\n            tmp = tmp.Left\n        }\n        // Recursively delete node tmp\n        bst.remove(tmp.Val.(int))\n        // Replace cur with tmp\n        cur.Val = tmp.Val\n    }\n}\n
binary_search_tree.swift
/* Remove node */\nfunc remove(num: Int) {\n    // If tree is empty, return directly\n    if root == nil {\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // Loop search, exit after passing leaf node\n    while cur != nil {\n        // Found node to delete, exit loop\n        if cur!.val == num {\n            break\n        }\n        pre = cur\n        // Node to delete is in cur's right subtree\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // Node to delete is in cur's left subtree\n        else {\n            cur = cur?.left\n        }\n    }\n    // If no node to delete, return directly\n    if cur == nil {\n        return\n    }\n    // Number of child nodes = 0 or 1\n    if cur?.left == nil || cur?.right == nil {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        let child = cur?.left ?? cur?.right\n        // Delete node cur\n        if cur !== root {\n            if pre?.left === cur {\n                pre?.left = child\n            } else {\n                pre?.right = child\n            }\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        var tmp = cur?.right\n        while tmp?.left != nil {\n            tmp = tmp?.left\n        }\n        // Recursively delete node tmp\n        remove(num: tmp!.val)\n        // Replace cur with tmp\n        cur?.val = tmp!.val\n    }\n}\n
binary_search_tree.js
/* Remove node */\nremove(num) {\n    // If tree is empty, return directly\n    if (this.root === null) return;\n    let cur = this.root,\n        pre = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found node to delete, exit loop\n        if (cur.val === num) break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur === null) return;\n    // Number of child nodes = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        const child = cur.left !== null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur !== this.root) {\n            if (pre.left === cur) pre.left = child;\n            else pre.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            this.root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        let tmp = cur.right;\n        while (tmp.left !== null) {\n            tmp = tmp.left;\n        }\n        // Recursively delete node tmp\n        this.remove(tmp.val);\n        // Replace cur with tmp\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.ts
/* Remove node */\nremove(num: number): void {\n    // If tree is empty, return directly\n    if (this.root === null) return;\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // Loop search, exit after passing leaf node\n    while (cur !== null) {\n        // Found node to delete, exit loop\n        if (cur.val === num) break;\n        pre = cur;\n        // Node to delete is in cur's right subtree\n        if (cur.val < num) cur = cur.right;\n        // Node to delete is in cur's left subtree\n        else cur = cur.left;\n    }\n    // If no node to delete, return directly\n    if (cur === null) return;\n    // Number of child nodes = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        const child: TreeNode | null =\n            cur.left !== null ? cur.left : cur.right;\n        // Delete node cur\n        if (cur !== this.root) {\n            if (pre!.left === cur) pre!.left = child;\n            else pre!.right = child;\n        } else {\n            // If deleted node is root node, reassign root node\n            this.root = child;\n        }\n    }\n    // Number of child nodes = 2\n    else {\n        // Get next node of cur in inorder traversal\n        let tmp: TreeNode | null = cur.right;\n        while (tmp!.left !== null) {\n            tmp = tmp!.left;\n        }\n        // Recursively delete node tmp\n        this.remove(tmp!.val);\n        // Replace cur with tmp\n        cur.val = tmp!.val;\n    }\n}\n
binary_search_tree.dart
/* Remove node */\nvoid remove(int _num) {\n  // If tree is empty, return directly\n  if (_root == null) return;\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // Loop search, exit after passing leaf node\n  while (cur != null) {\n    // Found node to delete, exit loop\n    if (cur.val == _num) break;\n    pre = cur;\n    // Node to delete is in cur's right subtree\n    if (cur.val < _num)\n      cur = cur.right;\n    // Node to delete is in cur's left subtree\n    else\n      cur = cur.left;\n  }\n  // If no node to delete, return directly\n  if (cur == null) return;\n  // Number of child nodes = 0 or 1\n  if (cur.left == null || cur.right == null) {\n    // When number of child nodes = 0 / 1, child = null / that child node\n    TreeNode? child = cur.left ?? cur.right;\n    // Delete node cur\n    if (cur != _root) {\n      if (pre!.left == cur)\n        pre.left = child;\n      else\n        pre.right = child;\n    } else {\n      // If deleted node is root node, reassign root node\n      _root = child;\n    }\n  } else {\n    // Number of child nodes = 2\n    // Get next node of cur in inorder traversal\n    TreeNode? tmp = cur.right;\n    while (tmp!.left != null) {\n      tmp = tmp.left;\n    }\n    // Recursively delete node tmp\n    remove(tmp.val);\n    // Replace cur with tmp\n    cur.val = tmp.val;\n  }\n}\n
binary_search_tree.rs
/* Remove node */\npub fn remove(&mut self, num: i32) {\n    // If tree is empty, return directly\n    if self.root.is_none() {\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // Loop search, exit after passing leaf node\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // Found node to delete, exit loop\n            Ordering::Equal => break,\n            // Node to delete is in cur's right subtree\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // Node to delete is in cur's left subtree\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // If no node to delete, return directly\n    if cur.is_none() {\n        return;\n    }\n    let cur = cur.unwrap();\n    let (left_child, right_child) = (cur.borrow().left.clone(), cur.borrow().right.clone());\n    match (left_child.clone(), right_child.clone()) {\n        // Number of child nodes = 0 or 1\n        (None, None) | (Some(_), None) | (None, Some(_)) => {\n            // When number of child nodes = 0 / 1, child = nullptr / that child node\n            let child = left_child.or(right_child);\n            let pre = pre.unwrap();\n            // Delete node cur\n            if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) {\n                let left = pre.borrow().left.clone();\n                if left.is_some() && Rc::ptr_eq(left.as_ref().unwrap(), &cur) {\n                    pre.borrow_mut().left = child;\n                } else {\n                    pre.borrow_mut().right = child;\n                }\n            } else {\n                // If deleted node is root node, reassign root node\n                self.root = child;\n            }\n        }\n        // Number of child nodes = 2\n        (Some(_), Some(_)) => {\n            // Get next node of cur in inorder traversal\n            let mut tmp = cur.borrow().right.clone();\n            while let Some(node) = tmp.clone() {\n                if node.borrow().left.is_some() {\n                    tmp = node.borrow().left.clone();\n                } else {\n                    break;\n                }\n            }\n            let tmp_val = tmp.unwrap().borrow().val;\n            // Recursively delete node tmp\n            self.remove(tmp_val);\n            // Replace cur with tmp\n            cur.borrow_mut().val = tmp_val;\n        }\n    }\n}\n
binary_search_tree.c
/* Remove node */\n// Cannot use remove keyword here due to stdio.h inclusion\nvoid removeItem(BinarySearchTree *bst, int num) {\n    // If tree is empty, return directly\n    if (bst->root == NULL)\n        return;\n    TreeNode *cur = bst->root, *pre = NULL;\n    // Loop search, exit after passing leaf node\n    while (cur != NULL) {\n        // Found node to delete, exit loop\n        if (cur->val == num)\n            break;\n        pre = cur;\n        if (cur->val < num) {\n            // Node to delete is in right subtree of root\n            cur = cur->right;\n        } else {\n            // Node to delete is in left subtree of root\n            cur = cur->left;\n        }\n    }\n    // If no node to delete, return directly\n    if (cur == NULL)\n        return;\n    // Check if node to delete has children\n    if (cur->left == NULL || cur->right == NULL) {\n        /* Number of child nodes = 0 or 1 */\n        // When number of child nodes = 0 / 1, child = nullptr / that child node\n        TreeNode *child = cur->left != NULL ? cur->left : cur->right;\n        // Delete node cur\n        if (pre->left == cur) {\n            pre->left = child;\n        } else {\n            pre->right = child;\n        }\n        // Free memory\n        free(cur);\n    } else {\n        /* Number of child nodes = 2 */\n        // Get next node of cur in inorder traversal\n        TreeNode *tmp = cur->right;\n        while (tmp->left != NULL) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // Recursively delete node tmp\n        removeItem(bst, tmp->val);\n        // Replace cur with tmp\n        cur->val = tmpVal;\n    }\n}\n
binary_search_tree.kt
/* Remove node */\nfun remove(num: Int) {\n    // If tree is empty, return directly\n    if (root == null)\n        return\n    var cur = root\n    var pre: TreeNode? = null\n    // Loop search, exit after passing leaf node\n    while (cur != null) {\n        // Found node to delete, exit loop\n        if (cur._val == num)\n            break\n        pre = cur\n        // Node to delete is in cur's right subtree\n        cur = if (cur._val < num)\n            cur.right\n        // Node to delete is in cur's left subtree\n        else\n            cur.left\n    }\n    // If no node to delete, return directly\n    if (cur == null)\n        return\n    // Number of child nodes = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // When number of child nodes = 0 / 1, child = null / that child node\n        val child = if (cur.left != null)\n            cur.left\n        else\n            cur.right\n        // Delete node cur\n        if (cur != root) {\n            if (pre!!.left == cur)\n                pre.left = child\n            else\n                pre.right = child\n        } else {\n            // If deleted node is root node, reassign root node\n            root = child\n        }\n        // Number of child nodes = 2\n    } else {\n        // Get next node of cur in inorder traversal\n        var tmp = cur.right\n        while (tmp!!.left != null) {\n            tmp = tmp.left\n        }\n        // Recursively delete node tmp\n        remove(tmp._val)\n        // Replace cur with tmp\n        cur._val = tmp._val\n    }\n}\n
binary_search_tree.rb
### Delete node ###\ndef remove(num)\n  # If tree is empty, return directly\n  return if @root.nil?\n\n  # Loop search, exit after passing leaf node\n  cur, pre = @root, nil\n  while !cur.nil?\n    # Found node to delete, exit loop\n    break if cur.val == num\n\n    pre = cur\n    # Node to delete is in cur's right subtree\n    if cur.val < num\n      cur = cur.right\n    # Node to delete is in cur's left subtree\n    else\n      cur = cur.left\n    end\n  end\n  # If no node to delete, return directly\n  return if cur.nil?\n\n  # Number of child nodes = 0 or 1\n  if cur.left.nil? || cur.right.nil?\n    # When number of child nodes = 0 / 1, child = null / that child node\n    child = cur.left || cur.right\n    # Delete node cur\n    if cur != @root\n      if pre.left == cur\n        pre.left = child\n      else\n        pre.right = child\n      end\n    else\n      # If deleted node is root node, reassign root node\n      @root = child\n    end\n  # Number of child nodes = 2\n  else\n    # Get next node of cur in inorder traversal\n    tmp = cur.right\n    while !tmp.left.nil?\n      tmp = tmp.left\n    end\n    # Recursively delete node tmp\n    remove(tmp.val)\n    # Replace cur with tmp\n    cur.val = tmp.val\n  end\nend\n
","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#4-inorder-traversal-is-ordered","level":3,"title":"4.   Inorder Traversal Is Ordered","text":"

As shown in Figure 7-22, the inorder traversal of a binary tree follows the \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\" traversal order, while the binary search tree satisfies the \"left child node \\(<\\) root node \\(<\\) right child node\" size relationship.

This means that when performing an inorder traversal in a binary search tree, the next smallest node is always traversed first, thus yielding an important property: The inorder traversal sequence of a binary search tree is ascending.

Using the property of inorder traversal being ascending, we can obtain ordered data in a binary search tree in only \\(O(n)\\) time, without the need for additional sorting operations, which is very efficient.

Figure 7-22   Inorder traversal sequence of a binary search tree

","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#742-efficiency-of-binary-search-trees","level":2,"title":"7.4.2   Efficiency of Binary Search Trees","text":"

Given a set of data, we consider using an array or a binary search tree for storage. Observing Table 7-2, all operations in a binary search tree have logarithmic time complexity, providing stable and efficient performance. Arrays are more efficient than binary search trees only in scenarios with high-frequency additions and low-frequency searches and deletions.

Table 7-2   Efficiency comparison between arrays and search trees

Unsorted array Binary search tree Search element \\(O(n)\\) \\(O(\\log n)\\) Insert element \\(O(1)\\) \\(O(\\log n)\\) Remove element \\(O(n)\\) \\(O(\\log n)\\)

In the ideal case, a binary search tree is \"balanced,\" such that any node can be found within \\(\\log n\\) loop iterations.

However, if we continuously insert and remove nodes in a binary search tree, it may degenerate into a linked list as shown in Figure 7-23, where the time complexity of various operations also degrades to \\(O(n)\\).

Figure 7-23   Degradation of a binary search tree

","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#743-common-applications-of-binary-search-trees","level":2,"title":"7.4.3   Common Applications of Binary Search Trees","text":"
  • Used as multi-level indexes in systems to implement efficient search, insertion, and removal operations.
  • Serves as the underlying data structure for certain search algorithms.
  • Used to store data streams to maintain their ordered state.
","path":["Chapter 7. Tree","7.4   Binary Search Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/","level":1,"title":"7.1   Binary Tree","text":"

A binary tree is a non-linear data structure that represents the derivation relationship between \"ancestors\" and \"descendants\" and embodies the divide-and-conquer logic of \"one divides into two\". Similar to a linked list, the basic unit of a binary tree is a node, and each node contains a value, a reference to its left child node, and a reference to its right child node.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class TreeNode:\n    \"\"\"Binary tree node\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # Node value\n        self.left: TreeNode | None = None  # Reference to left child node\n        self.right: TreeNode | None = None # Reference to right child node\n
/* Binary tree node */\nstruct TreeNode {\n    int val;          // Node value\n    TreeNode *left;   // Pointer to left child node\n    TreeNode *right;  // Pointer to right child node\n    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}\n};\n
/* Binary tree node */\nclass TreeNode {\n    int val;         // Node value\n    TreeNode left;   // Reference to left child node\n    TreeNode right;  // Reference to right child node\n    TreeNode(int x) { val = x; }\n}\n
/* Binary tree node */\nclass TreeNode(int? x) {\n    public int? val = x;    // Node value\n    public TreeNode? left;  // Reference to left child node\n    public TreeNode? right; // Reference to right child node\n}\n
/* Binary tree node */\ntype TreeNode struct {\n    Val   int\n    Left  *TreeNode\n    Right *TreeNode\n}\n/* Constructor */\nfunc NewTreeNode(v int) *TreeNode {\n    return &TreeNode{\n        Left:  nil, // Pointer to left child node\n        Right: nil, // Pointer to right child node\n        Val:   v,   // Node value\n    }\n}\n
/* Binary tree node */\nclass TreeNode {\n    var val: Int // Node value\n    var left: TreeNode? // Reference to left child node\n    var right: TreeNode? // Reference to right child node\n\n    init(x: Int) {\n        val = x\n    }\n}\n
/* Binary tree node */\nclass TreeNode {\n    val; // Node value\n    left; // Pointer to left child node\n    right; // Pointer to right child node\n    constructor(val, left, right) {\n        this.val = val === undefined ? 0 : val;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* Binary tree node */\nclass TreeNode {\n    val: number;\n    left: TreeNode | null;\n    right: TreeNode | null;\n\n    constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val; // Node value\n        this.left = left === undefined ? null : left; // Reference to left child node\n        this.right = right === undefined ? null : right; // Reference to right child node\n    }\n}\n
/* Binary tree node */\nclass TreeNode {\n  int val;         // Node value\n  TreeNode? left;  // Reference to left child node\n  TreeNode? right; // Reference to right child node\n  TreeNode(this.val, [this.left, this.right]);\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* Binary tree node */\nstruct TreeNode {\n    val: i32,                               // Node value\n    left: Option<Rc<RefCell<TreeNode>>>,    // Reference to left child node\n    right: Option<Rc<RefCell<TreeNode>>>,   // Reference to right child node\n}\n\nimpl TreeNode {\n    /* Constructor */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            left: None,\n            right: None\n        }))\n    }\n}\n
/* Binary tree node */\ntypedef struct TreeNode {\n    int val;                // Node value\n    int height;             // Node height\n    struct TreeNode *left;  // Pointer to left child node\n    struct TreeNode *right; // Pointer to right child node\n} TreeNode;\n\n/* Constructor */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
/* Binary tree node */\nclass TreeNode(val _val: Int) {  // Node value\n    val left: TreeNode? = null   // Reference to left child node\n    val right: TreeNode? = null  // Reference to right child node\n}\n
### Binary tree node class ###\nclass TreeNode\n  attr_accessor :val    # Node value\n  attr_accessor :left   # Reference to left child node\n  attr_accessor :right  # Reference to right child node\n\n  def initialize(val)\n    @val = val\n  end\nend\n

Each node has two references (pointers), pointing respectively to the left-child node and right-child node. This node is called the parent node of these two child nodes. When given a node of a binary tree, we call the tree formed by this node's left child and all nodes below it the left subtree of this node. Similarly, the right subtree can be defined.

In a binary tree, except leaf nodes, all other nodes contain child nodes and non-empty subtrees. As shown in Figure 7-1, if \"Node 2\" is regarded as a parent node, its left and right child nodes are \"Node 4\" and \"Node 5\" respectively. The left subtree is formed by \"Node 4\" and all nodes beneath it, while the right subtree is formed by \"Node 5\" and all nodes beneath it.

Figure 7-1   Parent Node, child Node, subtree

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#711-common-terminology-of-binary-trees","level":2,"title":"7.1.1   Common Terminology of Binary Trees","text":"

The commonly used terminology of binary trees is shown in Figure 7-2.

  • Root node: The node at the top level of a binary tree, which does not have a parent node.
  • Leaf node: A node that does not have any child nodes, with both of its pointers pointing to None.
  • Edge: A line segment that connects two nodes, representing a reference (pointer) between the nodes.
  • The level of a node: It increases from top to bottom, with the root node being at level 1.
  • The degree of a node: The number of child nodes that a node has. In a binary tree, the degree can be 0, 1, or 2.
  • The height of a binary tree: The number of edges from the root node to the farthest leaf node.
  • The depth of a node: The number of edges from the root node to the node.
  • The height of a node: The number of edges from the farthest leaf node to the node.

Figure 7-2   Common Terminology of Binary Trees

Tip

Please note that we usually define \"height\" and \"depth\" as \"the number of edges traversed\", but some questions or textbooks may define them as \"the number of nodes traversed\". In this case, both height and depth need to be incremented by 1.

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#712-basic-operations-of-binary-trees","level":2,"title":"7.1.2   Basic Operations of Binary Trees","text":"","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#1-initializing-a-binary-tree","level":3,"title":"1.   Initializing a Binary Tree","text":"

Similar to a linked list, the initialization of a binary tree involves first creating the nodes and then establishing the references (pointers) between them.

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
# Initializing a binary tree\n# Initializing nodes\nn1 = TreeNode(val=1)\nn2 = TreeNode(val=2)\nn3 = TreeNode(val=3)\nn4 = TreeNode(val=4)\nn5 = TreeNode(val=5)\n# Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.cpp
/* Initializing a binary tree */\n// Initializing nodes\nTreeNode* n1 = new TreeNode(1);\nTreeNode* n2 = new TreeNode(2);\nTreeNode* n3 = new TreeNode(3);\nTreeNode* n4 = new TreeNode(4);\nTreeNode* n5 = new TreeNode(5);\n// Linking references (pointers) between nodes\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
binary_tree.java
// Initializing nodes\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.cs
/* Initializing a binary tree */\n// Initializing nodes\nTreeNode n1 = new(1);\nTreeNode n2 = new(2);\nTreeNode n3 = new(3);\nTreeNode n4 = new(4);\nTreeNode n5 = new(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.go
/* Initializing a binary tree */\n// Initializing nodes\nn1 := NewTreeNode(1)\nn2 := NewTreeNode(2)\nn3 := NewTreeNode(3)\nn4 := NewTreeNode(4)\nn5 := NewTreeNode(5)\n// Linking references (pointers) between nodes\nn1.Left = n2\nn1.Right = n3\nn2.Left = n4\nn2.Right = n5\n
binary_tree.swift
// Initializing nodes\nlet n1 = TreeNode(x: 1)\nlet n2 = TreeNode(x: 2)\nlet n3 = TreeNode(x: 3)\nlet n4 = TreeNode(x: 4)\nlet n5 = TreeNode(x: 5)\n// Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.js
/* Initializing a binary tree */\n// Initializing nodes\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.ts
/* Initializing a binary tree */\n// Initializing nodes\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.dart
/* Initializing a binary tree */\n// Initializing nodes\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// Linking references (pointers) between nodes\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.rs
// Initializing nodes\nlet n1 = TreeNode::new(1);\nlet n2 = TreeNode::new(2);\nlet n3 = TreeNode::new(3);\nlet n4 = TreeNode::new(4);\nlet n5 = TreeNode::new(5);\n// Linking references (pointers) between nodes\nn1.borrow_mut().left = Some(n2.clone());\nn1.borrow_mut().right = Some(n3);\nn2.borrow_mut().left = Some(n4);\nn2.borrow_mut().right = Some(n5);\n
binary_tree.c
/* Initializing a binary tree */\n// Initializing nodes\nTreeNode *n1 = newTreeNode(1);\nTreeNode *n2 = newTreeNode(2);\nTreeNode *n3 = newTreeNode(3);\nTreeNode *n4 = newTreeNode(4);\nTreeNode *n5 = newTreeNode(5);\n// Linking references (pointers) between nodes\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
binary_tree.kt
// Initializing nodes\nval n1 = TreeNode(1)\nval n2 = TreeNode(2)\nval n3 = TreeNode(3)\nval n4 = TreeNode(4)\nval n5 = TreeNode(5)\n// Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.rb
# Initializing a binary tree\n# Initializing nodes\nn1 = TreeNode.new(1)\nn2 = TreeNode.new(2)\nn3 = TreeNode.new(3)\nn4 = TreeNode.new(4)\nn5 = TreeNode.new(5)\n# Linking references (pointers) between nodes\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
Code Visualization

Full Screen >

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#2-inserting-and-removing-nodes","level":3,"title":"2.   Inserting and Removing Nodes","text":"

Similar to a linked list, inserting and removing nodes in a binary tree can be achieved by modifying pointers. Figure 7-3 provides an example.

Figure 7-3   Inserting and removing nodes in a binary tree

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
# Inserting and removing nodes\np = TreeNode(0)\n# Inserting node P between n1 -> n2\nn1.left = p\np.left = n2\n# Removing node P\nn1.left = n2\n
binary_tree.cpp
/* Inserting and removing nodes */\nTreeNode* P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1->left = P;\nP->left = n2;\n// Removing node P\nn1->left = n2;\n
binary_tree.java
TreeNode P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
binary_tree.cs
/* Inserting and removing nodes */\nTreeNode P = new(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
binary_tree.go
/* Inserting and removing nodes */\n// Inserting node P between n1 and n2\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// Removing node P\nn1.Left = n2\n
binary_tree.swift
let P = TreeNode(x: 0)\n// Inserting node P between n1 and n2\nn1.left = P\nP.left = n2\n// Removing node P\nn1.left = n2\n
binary_tree.js
/* Inserting and removing nodes */\nlet P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
binary_tree.ts
/* Inserting and removing nodes */\nconst P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
binary_tree.dart
/* Inserting and removing nodes */\nTreeNode P = new TreeNode(0);\n// Inserting node P between n1 and n2\nn1.left = P;\nP.left = n2;\n// Removing node P\nn1.left = n2;\n
binary_tree.rs
let p = TreeNode::new(0);\n// Inserting node P between n1 and n2\nn1.borrow_mut().left = Some(p.clone());\np.borrow_mut().left = Some(n2.clone());\n// Removing node P\nn1.borrow_mut().left = Some(n2);\n
binary_tree.c
/* Inserting and removing nodes */\nTreeNode *P = newTreeNode(0);\n// Inserting node P between n1 and n2\nn1->left = P;\nP->left = n2;\n// Removing node P\nn1->left = n2;\n
binary_tree.kt
val P = TreeNode(0)\n// Inserting node P between n1 and n2\nn1.left = P\nP.left = n2\n// Removing node P\nn1.left = n2\n
binary_tree.rb
# Inserting and removing nodes\n_p = TreeNode.new(0)\n# Inserting node _p between n1 and n2\nn1.left = _p\n_p.left = n2\n# Removing node _p\nn1.left = n2\n
Code Visualization

Full Screen >

Tip

It should be noted that inserting nodes may change the original logical structure of the binary tree, while removing nodes typically involves removing the node and all its subtrees. Therefore, in a binary tree, insertion and removal are usually performed through a set of operations to achieve meaningful outcomes.

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#713-common-types-of-binary-trees","level":2,"title":"7.1.3   Common Types of Binary Trees","text":"","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#1-perfect-binary-tree","level":3,"title":"1.   Perfect Binary Tree","text":"

As shown in Figure 7-4, a perfect binary tree has all levels completely filled with nodes. In a perfect binary tree, leaf nodes have a degree of \\(0\\), while all other nodes have a degree of \\(2\\). If the tree height is \\(h\\), the total number of nodes is \\(2^{h+1} - 1\\), exhibiting a standard exponential relationship that reflects the common phenomenon of cell division in nature.

Tip

Please note that in the Chinese community, a perfect binary tree is often referred to as a full binary tree.

Figure 7-4   Perfect binary tree

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#2-complete-binary-tree","level":3,"title":"2.   Complete Binary Tree","text":"

As shown in Figure 7-5, a complete binary tree only allows the bottom level to be incompletely filled, and the nodes at the bottom level must be filled continuously from left to right. Note that a perfect binary tree is also a complete binary tree.

Figure 7-5   Complete binary tree

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#3-full-binary-tree","level":3,"title":"3.   Full Binary Tree","text":"

As shown in Figure 7-6, in a full binary tree, all nodes except leaf nodes have two child nodes.

Figure 7-6   Full binary tree

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#4-balanced-binary-tree","level":3,"title":"4.   Balanced Binary Tree","text":"

As shown in Figure 7-7, in a balanced binary tree, the absolute difference between the height of the left and right subtrees of any node does not exceed 1.

Figure 7-7   Balanced binary tree

","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree/#714-degeneration-of-binary-trees","level":2,"title":"7.1.4   Degeneration of Binary Trees","text":"

Figure 7-8 shows the ideal and degenerate structures of binary trees. When every level of a binary tree is filled, it reaches the \"perfect binary tree\" state; when all nodes are biased toward one side, the binary tree degenerates into a \"linked list\".

  • A perfect binary tree is the ideal case, fully leveraging the \"divide and conquer\" advantage of binary trees.
  • A linked list represents the other extreme, where all operations become linear operations with time complexity degrading to \\(O(n)\\).

Figure 7-8   The Best and Worst Structures of Binary Trees

As shown in Table 7-1, in the best and worst structures, the binary tree achieves either maximum or minimum values for leaf node count, total number of nodes, and height.

Table 7-1   The Best and Worst Structures of Binary Trees

Perfect binary tree Linked list Number of nodes at level \\(i\\) \\(2^{i-1}\\) \\(1\\) Number of leaf nodes in a tree with height \\(h\\) \\(2^h\\) \\(1\\) Total number of nodes in a tree with height \\(h\\) \\(2^{h+1} - 1\\) \\(h + 1\\) Height of a tree with \\(n\\) total nodes \\(\\log_2 (n+1) - 1\\) \\(n - 1\\)","path":["Chapter 7. Tree","7.1   Binary Tree"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/","level":1,"title":"7.2   Binary Tree Traversal","text":"

From a physical structure perspective, a tree is a data structure based on linked lists. Hence, its traversal method involves accessing nodes one by one through pointers. However, a tree is a non-linear data structure, which makes traversing a tree more complex than traversing a linked list, requiring the assistance of search algorithms.

The common traversal methods for binary trees include level-order traversal, pre-order traversal, in-order traversal, and post-order traversal.

","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#721-level-order-traversal","level":2,"title":"7.2.1   Level-Order Traversal","text":"

As shown in Figure 7-9, level-order traversal traverses the binary tree from top to bottom, layer by layer. Within each level, it visits nodes from left to right.

Level-order traversal is essentially breadth-first traversal, also known as breadth-first search (BFS), which embodies a \"expanding outward circle by circle\" layer-by-layer traversal method.

Figure 7-9   Level-order traversal of a binary tree

","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1-code-implementation","level":3,"title":"1.   Code Implementation","text":"

Breadth-first traversal is typically implemented with the help of a \"queue\". The queue follows the \"first in, first out\" rule, while breadth-first traversal follows the \"layer-by-layer progression\" rule; the underlying ideas of the two are consistent. The implementation code is as follows:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_bfs.py
def level_order(root: TreeNode | None) -> list[int]:\n    \"\"\"Level-order traversal\"\"\"\n    # Initialize queue, add root node\n    queue: deque[TreeNode] = deque()\n    queue.append(root)\n    # Initialize a list to save the traversal sequence\n    res = []\n    while queue:\n        node: TreeNode = queue.popleft()  # Dequeue\n        res.append(node.val)  # Save node value\n        if node.left is not None:\n            queue.append(node.left)  # Left child node enqueue\n        if node.right is not None:\n            queue.append(node.right)  # Right child node enqueue\n    return res\n
binary_tree_bfs.cpp
/* Level-order traversal */\nvector<int> levelOrder(TreeNode *root) {\n    // Initialize queue, add root node\n    queue<TreeNode *> queue;\n    queue.push(root);\n    // Initialize a list to save the traversal sequence\n    vector<int> vec;\n    while (!queue.empty()) {\n        TreeNode *node = queue.front();\n        queue.pop();              // Dequeue\n        vec.push_back(node->val); // Save node value\n        if (node->left != nullptr)\n            queue.push(node->left); // Left child node enqueue\n        if (node->right != nullptr)\n            queue.push(node->right); // Right child node enqueue\n    }\n    return vec;\n}\n
binary_tree_bfs.java
/* Level-order traversal */\nList<Integer> levelOrder(TreeNode root) {\n    // Initialize queue, add root node\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.add(root);\n    // Initialize a list to save the traversal sequence\n    List<Integer> list = new ArrayList<>();\n    while (!queue.isEmpty()) {\n        TreeNode node = queue.poll(); // Dequeue\n        list.add(node.val);           // Save node value\n        if (node.left != null)\n            queue.offer(node.left);   // Left child node enqueue\n        if (node.right != null)\n            queue.offer(node.right);  // Right child node enqueue\n    }\n    return list;\n}\n
binary_tree_bfs.cs
/* Level-order traversal */\nList<int> LevelOrder(TreeNode root) {\n    // Initialize queue, add root node\n    Queue<TreeNode> queue = new();\n    queue.Enqueue(root);\n    // Initialize a list to save the traversal sequence\n    List<int> list = [];\n    while (queue.Count != 0) {\n        TreeNode node = queue.Dequeue(); // Dequeue\n        list.Add(node.val!.Value);       // Save node value\n        if (node.left != null)\n            queue.Enqueue(node.left);    // Left child node enqueue\n        if (node.right != null)\n            queue.Enqueue(node.right);   // Right child node enqueue\n    }\n    return list;\n}\n
binary_tree_bfs.go
/* Level-order traversal */\nfunc levelOrder(root *TreeNode) []any {\n    // Initialize queue, add root node\n    queue := list.New()\n    queue.PushBack(root)\n    // Initialize a slice to save traversal sequence\n    nums := make([]any, 0)\n    for queue.Len() > 0 {\n        // Dequeue\n        node := queue.Remove(queue.Front()).(*TreeNode)\n        // Save node value\n        nums = append(nums, node.Val)\n        if node.Left != nil {\n            // Left child node enqueue\n            queue.PushBack(node.Left)\n        }\n        if node.Right != nil {\n            // Right child node enqueue\n            queue.PushBack(node.Right)\n        }\n    }\n    return nums\n}\n
binary_tree_bfs.swift
/* Level-order traversal */\nfunc levelOrder(root: TreeNode) -> [Int] {\n    // Initialize queue, add root node\n    var queue: [TreeNode] = [root]\n    // Initialize a list to save the traversal sequence\n    var list: [Int] = []\n    while !queue.isEmpty {\n        let node = queue.removeFirst() // Dequeue\n        list.append(node.val) // Save node value\n        if let left = node.left {\n            queue.append(left) // Left child node enqueue\n        }\n        if let right = node.right {\n            queue.append(right) // Right child node enqueue\n        }\n    }\n    return list\n}\n
binary_tree_bfs.js
/* Level-order traversal */\nfunction levelOrder(root) {\n    // Initialize queue, add root node\n    const queue = [root];\n    // Initialize a list to save the traversal sequence\n    const list = [];\n    while (queue.length) {\n        let node = queue.shift(); // Dequeue\n        list.push(node.val); // Save node value\n        if (node.left) queue.push(node.left); // Left child node enqueue\n        if (node.right) queue.push(node.right); // Right child node enqueue\n    }\n    return list;\n}\n
binary_tree_bfs.ts
/* Level-order traversal */\nfunction levelOrder(root: TreeNode | null): number[] {\n    // Initialize queue, add root node\n    const queue = [root];\n    // Initialize a list to save the traversal sequence\n    const list: number[] = [];\n    while (queue.length) {\n        let node = queue.shift() as TreeNode; // Dequeue\n        list.push(node.val); // Save node value\n        if (node.left) {\n            queue.push(node.left); // Left child node enqueue\n        }\n        if (node.right) {\n            queue.push(node.right); // Right child node enqueue\n        }\n    }\n    return list;\n}\n
binary_tree_bfs.dart
/* Level-order traversal */\nList<int> levelOrder(TreeNode? root) {\n  // Initialize queue, add root node\n  Queue<TreeNode?> queue = Queue();\n  queue.add(root);\n  // Initialize a list to save the traversal sequence\n  List<int> res = [];\n  while (queue.isNotEmpty) {\n    TreeNode? node = queue.removeFirst(); // Dequeue\n    res.add(node!.val); // Save node value\n    if (node.left != null) queue.add(node.left); // Left child node enqueue\n    if (node.right != null) queue.add(node.right); // Right child node enqueue\n  }\n  return res;\n}\n
binary_tree_bfs.rs
/* Level-order traversal */\nfn level_order(root: &Rc<RefCell<TreeNode>>) -> Vec<i32> {\n    // Initialize queue, add root node\n    let mut que = VecDeque::new();\n    que.push_back(root.clone());\n    // Initialize a list to save the traversal sequence\n    let mut vec = Vec::new();\n\n    while let Some(node) = que.pop_front() {\n        // Dequeue\n        vec.push(node.borrow().val); // Save node value\n        if let Some(left) = node.borrow().left.as_ref() {\n            que.push_back(left.clone()); // Left child node enqueue\n        }\n        if let Some(right) = node.borrow().right.as_ref() {\n            que.push_back(right.clone()); // Right child node enqueue\n        };\n    }\n    vec\n}\n
binary_tree_bfs.c
/* Level-order traversal */\nint *levelOrder(TreeNode *root, int *size) {\n    /* Auxiliary queue */\n    int front, rear;\n    int index, *arr;\n    TreeNode *node;\n    TreeNode **queue;\n\n    /* Auxiliary queue */\n    queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE);\n    // Queue pointer\n    front = 0, rear = 0;\n    // Add root node\n    queue[rear++] = root;\n    // Initialize a list to save the traversal sequence\n    /* Auxiliary array */\n    arr = (int *)malloc(sizeof(int) * MAX_SIZE);\n    // Array pointer\n    index = 0;\n    while (front < rear) {\n        // Dequeue\n        node = queue[front++];\n        // Save node value\n        arr[index++] = node->val;\n        if (node->left != NULL) {\n            // Left child node enqueue\n            queue[rear++] = node->left;\n        }\n        if (node->right != NULL) {\n            // Right child node enqueue\n            queue[rear++] = node->right;\n        }\n    }\n    // Update array length value\n    *size = index;\n    arr = realloc(arr, sizeof(int) * (*size));\n\n    // Free auxiliary array space\n    free(queue);\n    return arr;\n}\n
binary_tree_bfs.kt
/* Level-order traversal */\nfun levelOrder(root: TreeNode?): MutableList<Int> {\n    // Initialize queue, add root node\n    val queue = LinkedList<TreeNode?>()\n    queue.add(root)\n    // Initialize a list to save the traversal sequence\n    val list = mutableListOf<Int>()\n    while (queue.isNotEmpty()) {\n        val node = queue.poll()      // Dequeue\n        list.add(node?._val!!)       // Save node value\n        if (node.left != null)\n            queue.offer(node.left)   // Left child node enqueue\n        if (node.right != null)\n            queue.offer(node.right)  // Right child node enqueue\n    }\n    return list\n}\n
binary_tree_bfs.rb
### Level-order traversal ###\ndef level_order(root)\n  # Initialize queue, add root node\n  queue = [root]\n  # Initialize a list to save the traversal sequence\n  res = []\n  while !queue.empty?\n    node = queue.shift # Dequeue\n    res << node.val # Save node value\n    queue << node.left unless node.left.nil? # Left child node enqueue\n    queue << node.right unless node.right.nil? # Right child node enqueue\n  end\n  res\nend\n
","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2-complexity-analysis","level":3,"title":"2.   Complexity Analysis","text":"
  • Time complexity is \\(O(n)\\): All nodes are visited once, using \\(O(n)\\) time, where \\(n\\) is the number of nodes.
  • Space complexity is \\(O(n)\\): In the worst case, i.e., a full binary tree, before traversing to the bottom level, the queue contains at most \\((n + 1) / 2\\) nodes simultaneously, occupying \\(O(n)\\) space.
","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#722-preorder-inorder-and-postorder-traversal","level":2,"title":"7.2.2   Preorder, Inorder, and Postorder Traversal","text":"

Correspondingly, preorder, inorder, and postorder traversals all belong to depth-first traversal, also known as depth-first search (DFS), which embodies a \"first go to the end, then backtrack and continue\" traversal method.

Figure 7-10 shows how depth-first traversal works on a binary tree. Depth-first traversal is like \"walking\" around the perimeter of the entire binary tree, encountering three positions at each node, corresponding to preorder, inorder, and postorder traversal.

Figure 7-10   Preorder, inorder, and postorder traversal of a binary tree

","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1-code-implementation_1","level":3,"title":"1.   Code Implementation","text":"

Depth-first search is usually implemented based on recursion:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_dfs.py
def pre_order(root: TreeNode | None):\n    \"\"\"Preorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: root node -> left subtree -> right subtree\n    res.append(root.val)\n    pre_order(root=root.left)\n    pre_order(root=root.right)\n\ndef in_order(root: TreeNode | None):\n    \"\"\"Inorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: left subtree -> root node -> right subtree\n    in_order(root=root.left)\n    res.append(root.val)\n    in_order(root=root.right)\n\ndef post_order(root: TreeNode | None):\n    \"\"\"Postorder traversal\"\"\"\n    if root is None:\n        return\n    # Visit priority: left subtree -> right subtree -> root node\n    post_order(root=root.left)\n    post_order(root=root.right)\n    res.append(root.val)\n
binary_tree_dfs.cpp
/* Preorder traversal */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    vec.push_back(root->val);\n    preOrder(root->left);\n    preOrder(root->right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root->left);\n    vec.push_back(root->val);\n    inOrder(root->right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root->left);\n    postOrder(root->right);\n    vec.push_back(root->val);\n}\n
binary_tree_dfs.java
/* Preorder traversal */\nvoid preOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.add(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.add(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.add(root.val);\n}\n
binary_tree_dfs.cs
/* Preorder traversal */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.Add(root.val!.Value);\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n\n/* Inorder traversal */\nvoid InOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: left subtree -> root node -> right subtree\n    InOrder(root.left);\n    list.Add(root.val!.Value);\n    InOrder(root.right);\n}\n\n/* Postorder traversal */\nvoid PostOrder(TreeNode? root) {\n    if (root == null) return;\n    // Visit priority: left subtree -> right subtree -> root node\n    PostOrder(root.left);\n    PostOrder(root.right);\n    list.Add(root.val!.Value);\n}\n
binary_tree_dfs.go
/* Preorder traversal */\nfunc preOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    nums = append(nums, node.Val)\n    preOrder(node.Left)\n    preOrder(node.Right)\n}\n\n/* Inorder traversal */\nfunc inOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(node.Left)\n    nums = append(nums, node.Val)\n    inOrder(node.Right)\n}\n\n/* Postorder traversal */\nfunc postOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(node.Left)\n    postOrder(node.Right)\n    nums = append(nums, node.Val)\n}\n
binary_tree_dfs.swift
/* Preorder traversal */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    list.append(root.val)\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n\n/* Inorder traversal */\nfunc inOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root: root.left)\n    list.append(root.val)\n    inOrder(root: root.right)\n}\n\n/* Postorder traversal */\nfunc postOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root: root.left)\n    postOrder(root: root.right)\n    list.append(root.val)\n}\n
binary_tree_dfs.js
/* Preorder traversal */\nfunction preOrder(root) {\n    if (root === null) return;\n    // Visit priority: root node -> left subtree -> right subtree\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nfunction inOrder(root) {\n    if (root === null) return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nfunction postOrder(root) {\n    if (root === null) return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
binary_tree_dfs.ts
/* Preorder traversal */\nfunction preOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: root node -> left subtree -> right subtree\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* Inorder traversal */\nfunction inOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* Postorder traversal */\nfunction postOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
binary_tree_dfs.dart
/* Preorder traversal */\nvoid preOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: root node -> left subtree -> right subtree\n  list.add(node.val);\n  preOrder(node.left);\n  preOrder(node.right);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: left subtree -> root node -> right subtree\n  inOrder(node.left);\n  list.add(node.val);\n  inOrder(node.right);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode? node) {\n  if (node == null) return;\n  // Visit priority: left subtree -> right subtree -> root node\n  postOrder(node.left);\n  postOrder(node.right);\n  list.add(node.val);\n}\n
binary_tree_dfs.rs
/* Preorder traversal */\nfn pre_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Visit priority: root node -> left subtree -> right subtree\n            let node = node.borrow();\n            res.push(node.val);\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* Inorder traversal */\nfn in_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Visit priority: left subtree -> root node -> right subtree\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            res.push(node.val);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* Postorder traversal */\nfn post_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // Visit priority: left subtree -> right subtree -> root node\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n            res.push(node.val);\n        }\n    }\n\n    dfs(root, &mut result);\n\n    result\n}\n
binary_tree_dfs.c
/* Preorder traversal */\nvoid preOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: root node -> left subtree -> right subtree\n    arr[(*size)++] = root->val;\n    preOrder(root->left, size);\n    preOrder(root->right, size);\n}\n\n/* Inorder traversal */\nvoid inOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root->left, size);\n    arr[(*size)++] = root->val;\n    inOrder(root->right, size);\n}\n\n/* Postorder traversal */\nvoid postOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root->left, size);\n    postOrder(root->right, size);\n    arr[(*size)++] = root->val;\n}\n
binary_tree_dfs.kt
/* Preorder traversal */\nfun preOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: root node -> left subtree -> right subtree\n    list.add(root._val)\n    preOrder(root.left)\n    preOrder(root.right)\n}\n\n/* Inorder traversal */\nfun inOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: left subtree -> root node -> right subtree\n    inOrder(root.left)\n    list.add(root._val)\n    inOrder(root.right)\n}\n\n/* Postorder traversal */\nfun postOrder(root: TreeNode?) {\n    if (root == null) return\n    // Visit priority: left subtree -> right subtree -> root node\n    postOrder(root.left)\n    postOrder(root.right)\n    list.add(root._val)\n}\n
binary_tree_dfs.rb
### Pre-order traversal ###\ndef pre_order(root)\n  return if root.nil?\n\n  # Visit priority: root node -> left subtree -> right subtree\n  $res << root.val\n  pre_order(root.left)\n  pre_order(root.right)\nend\n\n### In-order traversal ###\ndef in_order(root)\n  return if root.nil?\n\n  # Visit priority: left subtree -> root node -> right subtree\n  in_order(root.left)\n  $res << root.val\n  in_order(root.right)\nend\n\n### Post-order traversal ###\ndef post_order(root)\n  return if root.nil?\n\n  # Visit priority: left subtree -> right subtree -> root node\n  post_order(root.left)\n  post_order(root.right)\n  $res << root.val\nend\n

Tip

Depth-first search can also be implemented based on iteration, interested readers can study this on their own.

Figure 7-11 shows the recursive process of preorder traversal of a binary tree, which can be divided into two opposite parts: \"recursion\" and \"return\".

  1. \"Recursion\" means opening a new method, where the program accesses the next node in this process.
  2. \"Return\" means the function returns, indicating that the current node has been fully visited.
<1><2><3><4><5><6><7><8><9><10><11>

Figure 7-11   The recursive process of preorder traversal

","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2-complexity-analysis_1","level":3,"title":"2.   Complexity Analysis","text":"
  • Time complexity is \\(O(n)\\): All nodes are visited once, using \\(O(n)\\) time.
  • Space complexity is \\(O(n)\\): In the worst case, i.e., the tree degenerates into a linked list, the recursion depth reaches \\(n\\), and the system occupies \\(O(n)\\) stack frame space.
","path":["Chapter 7. Tree","7.2   Binary Tree Traversal"],"tags":[]},{"location":"chapter_tree/summary/","level":1,"title":"7.6   Summary","text":"","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]},{"location":"chapter_tree/summary/#1-key-review","level":3,"title":"1.   Key Review","text":"
  • A binary tree is a non-linear data structure that embodies the divide-and-conquer logic of \"one divides into two\". Each binary tree node contains a value and two pointers, which respectively point to its left and right child nodes.
  • For a certain node in a binary tree, the tree formed by its left (right) child node and all nodes below is called the left (right) subtree of that node.
  • Related terminology of binary trees includes root node, leaf node, level, degree, edge, height, and depth.
  • The initialization, node insertion, and node removal operations of binary trees are similar to those of linked lists.
  • Common types of binary trees include perfect binary trees, complete binary trees, full binary trees, and balanced binary trees. The perfect binary tree is the ideal state, while the linked list is the worst state after degradation.
  • A binary tree can be represented using an array by arranging node values and empty slots in level-order traversal sequence, and implementing pointers based on the index mapping relationship between parent and child nodes.
  • Level-order traversal of a binary tree is a breadth-first search method, embodying a layer-by-layer traversal approach of \"expanding outward circle by circle\", typically implemented using a queue.
  • Preorder, inorder, and postorder traversals all belong to depth-first search, embodying a traversal approach of \"first go to the end, then backtrack and continue\", typically implemented using recursion.
  • A binary search tree is an efficient data structure for element searching, with search, insertion, and removal operations all having time complexity of \\(O(\\log n)\\). When a binary search tree degenerates into a linked list, all time complexities degrade to \\(O(n)\\).
  • An AVL tree, also known as a balanced binary search tree, ensures the tree remains balanced after continuous node insertions and removals through rotation operations.
  • Rotation operations in AVL trees include right rotation, left rotation, left rotation then right rotation, and right rotation then left rotation. After inserting or removing nodes, AVL trees perform rotation operations from bottom to top to restore the tree to balance.
","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]},{"location":"chapter_tree/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q: For a binary tree with only one node, are both the height of the tree and the depth of the root node \\(0\\)?

Yes, because height and depth are typically defined as \"the number of edges passed.\"

Q: The insertion and removal in a binary tree are generally accomplished by a set of operations. What does \"a set of operations\" refer to here? Does it imply releasing the resources of the child nodes?

Taking the binary search tree as an example, the operation of removing a node needs to be handled in three different scenarios, each requiring multiple steps of node operations.

Q: Why does DFS traversal of binary trees have three orders: preorder, inorder, and postorder, and what are their uses?

Similar to forward and reverse traversal of arrays, preorder, inorder, and postorder traversals are three methods of binary tree traversal that allow us to obtain a traversal result in a specific order. For example, in a binary search tree, since nodes satisfy the relationship left child node value < root node value < right child node value, we only need to traverse the tree with the priority of \"left \\(\\rightarrow\\) root \\(\\rightarrow\\) right\" to obtain an ordered node sequence.

Q: In a right rotation operation handling the relationship between unbalanced nodes node, child, and grand_child, doesn't the connection between node and its parent node get lost after the right rotation?

We need to view this problem from a recursive perspective. The right rotation operation right_rotate(root) passes in the root node of the subtree and eventually returns the root node of the subtree after rotation with return child. The connection between the subtree's root node and its parent node is completed after the function returns, which is not within the maintenance scope of the right rotation operation.

Q: In C++, functions are divided into private and public sections. What considerations are there for this? Why are the height() function and the updateHeight() function placed in public and private, respectively?

It mainly depends on the method's usage scope. If a method is only used within the class, then it is designed as private. For example, calling updateHeight() alone by the user makes no sense, as it is only a step in insertion or removal operations. However, height() is used to access node height, similar to vector.size(), so it is set to public for ease of use.

Q: How do you build a binary search tree from a set of input data? Is the choice of root node very important?

Yes, the method for building a tree is provided in the build_tree() method in the binary search tree code. As for the choice of root node, we typically sort the input data, then select the middle element as the root node, and recursively build the left and right subtrees. This approach maximizes the tree's balance.

Q: In Java, do you always have to use the equals() method for string comparison?

In Java, for primitive data types, == is used to compare whether the values of two variables are equal. For reference types, the working principles of the two symbols are different.

  • ==: Used to compare whether two variables point to the same object, i.e., whether their positions in memory are the same.
  • equals(): Used to compare whether the values of two objects are equal.

Therefore, if we want to compare values, we should use equals(). However, strings initialized via String a = \"hi\"; String b = \"hi\"; are stored in the string constant pool and point to the same object, so a == b can also be used to compare the contents of the two strings.

Q: Before reaching the bottom level, is the number of nodes in the queue \\(2^h\\) in breadth-first traversal?

Yes, for example, a full binary tree with height \\(h = 2\\) has a total of \\(n = 7\\) nodes, then the bottom level has \\(4 = 2^h = (n + 1) / 2\\) nodes.

","path":["Chapter 7. Tree","7.6   Summary"],"tags":[]}]} \ No newline at end of file diff --git a/en/stylesheets/extra.css b/en/stylesheets/extra.css index 57becd0d3..00a71c3b1 100644 --- a/en/stylesheets/extra.css +++ b/en/stylesheets/extra.css @@ -1,4 +1,4 @@ -/* Color Settings */ +/* Theme tokens */ /* https://github.com/squidfunk/mkdocs-material/blob/6b5035f5580f97532d664e3d1babf5f320e88ee9/src/assets/stylesheets/main/_colors.scss */ /* https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#custom-colors */ :root>* { @@ -14,21 +14,23 @@ --md-code-fg-color: #1d1d20; --md-code-bg-color: #f5f5f5; - --md-accent-fg-color: #999; - --md-typeset-color: #1d1d20; --md-typeset-a-color: #349890; + --md-accent-fg-color: var(--md-typeset-a-color); + --md-typeset-btn-color: #55aea6; --md-typeset-btn-hover-color: #52bbb1; --md-admonition-icon--pythontutor: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-pythontutor-color: #eee; + --md-admonition-pythontutor-color: var(--md-code-bg-color); + + --hello-algo-sidebar-width: 13rem; } [data-md-color-scheme="slate"] { - --theme-dark-base: #1E1E1E; - --theme-dark-mantle: #1A1A1A; + --theme-dark-base: #1e1e1e; + --theme-dark-mantle: #1a1a1a; --theme-dark-crust: #171717; --hero-starfield-bg-color: var(--theme-dark-base); @@ -37,25 +39,25 @@ --md-default-fg-color: #adbac7; --md-default-bg-color: var(--theme-dark-base); + --md-default-bg-color--light: rgb(30 30 30 / 0.8); - --md-body-bg-color: var(--theme-dark-mantle); + --md-body-bg-color: var(--md-default-bg-color); --md-header-bg-color: rgba(26, 26, 26, 0.8); --md-code-fg-color: #adbac7; --md-code-bg-color: var(--theme-dark-crust); - --md-accent-fg-color: #aaa; - - --md-footer-fg-color: #adbac7; - --md-footer-bg-color: var(--theme-dark-mantle); - --md-typeset-color: #adbac7; --md-typeset-a-color: #52bbb1; + --md-accent-fg-color: var(--md-typeset-a-color); --md-typeset-btn-color: #52bbb1; --md-typeset-btn-hover-color: #55aea6; - --md-admonition-pythontutor-color: var(--theme-dark-crust); + --md-footer-fg-color: #adbac7; + --md-footer-bg-color: var(--theme-dark-mantle); + + --md-admonition-pythontutor-color: var(--md-code-bg-color); } [data-md-color-scheme="slate"][data-md-color-primary="black"], @@ -63,24 +65,82 @@ --md-typeset-a-color: #52bbb1; } +/* Base layout */ +body { + background-color: var(--md-default-bg-color); + --md-text-font-family: -apple-system, BlinkMacSystemFont, + var(--md-text-font, _), Helvetica, Arial, sans-serif; + --md-code-font-family: var(--md-code-font, _), SFMono-Regular, Consolas, Menlo, + -apple-system, BlinkMacSystemFont, var(--md-text-font, _), monospace; +} + +html:has(body[data-md-color-scheme="slate"]) { + background-color: #1e1e1e; +} + +html:has(body[data-md-color-scheme="default"]) { + background-color: #ffffff; +} + +@media screen and (min-width: 76.25em) { + .md-grid { + max-width: calc(61rem + 2 * (var(--hello-algo-sidebar-width) - 12.1rem)); + } + + .md-sidebar--primary, + .md-sidebar--secondary { + width: var(--hello-algo-sidebar-width); + } + + [dir="ltr"] .md-sidebar__inner { + padding-right: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); + } + + [dir="rtl"] .md-sidebar__inner { + padding-left: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); + } +} + +.md-sidebar--primary .md-sidebar__scrollwrap { + scrollbar-color: var(--md-default-fg-color--lighter) transparent; +} + +.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb, +.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: var(--md-default-fg-color--lighter); +} + +/* Banner and footer */ +.md-banner { + background-color: var(--md-default-bg-color); + color: var(--md-default-fg-color); + font-size: 0.75rem; +} + +.md-banner .banner-svg svg { + margin-right: 0.3rem; + height: 0.63rem; + fill: var(--md-default-fg-color); +} + +.md-footer, +.md-footer__inner, +.md-footer-meta, +.md-footer__link, +.md-footer__link:hover { + background-color: var(--md-default-bg-color); +} + +.md-footer { + border-top: 0.05rem solid var(--md-default-fg-color--lightest); +} + [data-md-color-scheme="slate"] .md-footer, -[data-md-color-scheme="slate"] .md-footer__inner { - background-color: var(--theme-dark-mantle); - color: var(--md-footer-fg-color); -} - -[data-md-color-scheme="slate"] .md-footer-meta { - background-color: var(--theme-dark-crust); - color: var(--md-footer-fg-color); -} - -[data-md-color-scheme="slate"] .md-footer__link { - background-color: var(--theme-dark-crust); - color: var(--md-footer-fg-color); -} - +[data-md-color-scheme="slate"] .md-footer__inner, +[data-md-color-scheme="slate"] .md-footer-meta, +[data-md-color-scheme="slate"] .md-footer__link, [data-md-color-scheme="slate"] .md-footer__link:hover { - background-color: var(--theme-dark-base); + color: var(--md-footer-fg-color); } [data-md-color-scheme="slate"] .md-footer__title, @@ -93,40 +153,31 @@ color: var(--md-footer-fg-color); } -/* https://github.com/squidfunk/mkdocs-material/issues/4832#issuecomment-1374891676 */ -.md-nav__link[for] { - color: var(--md-default-fg-color) !important; -} - -/* Figure class */ +/* Shared content elements */ .animation-figure { border-radius: 0.3rem; display: block; margin: 0 auto; - box-shadow: var(--md-shadow-z2); + box-shadow: 0 0.03rem 0.16rem rgb(0 0 0 / 0.07); } -/* Cover image class */ .cover-image { width: 28rem; height: auto; border-radius: 0.3rem; display: block; margin: 0 auto; - box-shadow: var(--md-shadow-z2); + box-shadow: 0 0.03rem 0.16rem rgb(0 0 0 / 0.07); } -/* Center Markdown Tables (requires md_in_html extension) */ .center-table { text-align: center; } -/* Reset alignment for table cells */ .md-typeset .center-table :is(td, th):not([align]) { text-align: initial; } -/* Font size */ .md-typeset { font-size: 0.75rem; line-height: 1.5; @@ -136,7 +187,6 @@ font-size: 0.95em; } -/* Markdown Header */ /* https://github.com/squidfunk/mkdocs-material/blob/dcab57dd1cced4b77875c1aa1b53467c62709d31/src/assets/stylesheets/main/_typeset.scss */ .md-typeset h1 { font-weight: 400; @@ -155,11 +205,6 @@ text-transform: none; } -.md-typeset a:hover { - color: var(--md-typeset-a-color); - text-decoration: underline; -} - .md-typeset code { border-radius: 0.2rem; } @@ -168,21 +213,11 @@ font-weight: normal; } -/* font-family setting for Win10 */ -body { - --md-text-font-family: -apple-system, BlinkMacSystemFont, - var(--md-text-font, _), Helvetica, Arial, sans-serif; - --md-code-font-family: var(--md-code-font, _), SFMono-Regular, Consolas, Menlo, - -apple-system, BlinkMacSystemFont, var(--md-text-font, _), monospace; -} - -/* max height of code block */ /* https://github.com/squidfunk/mkdocs-material/issues/3444 */ .md-typeset pre>code { max-height: 25rem; } -/* Keep code block scrollbar hover neutral instead of accent-colored */ .md-typeset pre>code:hover { scrollbar-color: var(--md-default-fg-color--lighter) transparent; } @@ -191,29 +226,48 @@ body { background-color: var(--md-default-fg-color--lighter); } -/* Make the picture not glare in dark theme */ [data-md-color-scheme="slate"] .md-typeset img, [data-md-color-scheme="slate"] .md-typeset svg, [data-md-color-scheme="slate"] .md-typeset video { filter: brightness(0.85) invert(0.05); } -/* landing page */ -.header-img-div { - display: flex; - align-items: center; - justify-content: center; - margin: 0 auto; - width: 100%; - /* Default to full width */ +.md-typeset a:not(.md-button) { + text-decoration: none; +} + +.md-typeset a:not(.md-button):hover, +.md-typeset a:not(.md-button):focus-visible { + color: var(--md-typeset-a-color); + text-decoration: underline; +} + +/* Admonitions and tabs */ +.md-typeset .admonition-title:before, +.md-typeset summary:before, +.md-typeset summary:after { + top: 50%; +} + +.md-typeset .admonition-title:before, +.md-typeset summary:before { + transform: translateY(-50%); +} + +.md-typeset summary:after { + transform: translateY(-50%) rotate(0deg); +} + +.md-typeset details[open]>summary:after { + transform: translateY(-50%) rotate(90deg); } -/* Admonition for python tutor */ .md-typeset .admonition.pythontutor, .md-typeset details.pythontutor { border-color: var(--md-default-fg-color--lightest); margin-top: 0; margin-bottom: 1.5625em; + background-color: var(--md-code-bg-color); } .md-typeset .pythontutor>.admonition-title, @@ -228,26 +282,18 @@ body { mask-image: var(--md-admonition-icon--pythontutor); } -/* code block tabs */ +[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary), +[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary) :is(p, li, strong, em, sub, sup, code, a) { + background-color: #f5f5f5; + color: #1d1d20; +} + .md-typeset .tabbed-labels>label { font-size: 0.61rem; } .md-typeset .tabbed-labels--linked>label>a { - padding: .78125em 1.0em .625em; -} - -/* header banner */ -.md-banner { - background-color: var(--md-code-bg-color); - color: var(--md-default-fg-color); - font-size: 0.75rem; -} - -.md-banner .banner-svg svg { - margin-right: 0.3rem; - height: 0.63rem; - fill: var(--md-default-fg-color); + padding: 0.78125em 1em 0.625em; } .pythontutor-iframe { @@ -260,115 +306,55 @@ body { border: none; } -/* landing page container */ +/* Landing page layout */ +.header-img-div { + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; + width: 100%; +} + .home-div { width: 100%; height: auto; display: flex; justify-content: center; align-items: center; + padding: 3em 2em; background-color: var(--md-default-bg-color); color: var(--md-default-fg-color); font-size: 0.9rem; - padding: 3em 2em; text-align: center; } +.home-div[data-md-color-scheme="default"], +.home-div[data-md-color-scheme="default"] h1, +.home-div[data-md-color-scheme="default"] h2, +.home-div[data-md-color-scheme="default"] h3, +.home-div[data-md-color-scheme="default"] h4, +.home-div[data-md-color-scheme="default"] h5, +.home-div[data-md-color-scheme="default"] h6, +.home-div[data-md-color-scheme="slate"], +.home-div[data-md-color-scheme="slate"] h1, +.home-div[data-md-color-scheme="slate"] h2, +.home-div[data-md-color-scheme="slate"] h3, +.home-div[data-md-color-scheme="slate"] h4, +.home-div[data-md-color-scheme="slate"] h5, +.home-div[data-md-color-scheme="slate"] h6 { + color: var(--md-default-fg-color); +} + .section-content { width: 100%; height: auto; max-width: 70vw; } -/* rounded button */ -.rounded-button { - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 10em; - margin: 0 0.1em; - padding: 0.6em 1.3em; - border: none; - background-color: var(--md-typeset-btn-color); - color: var(--md-primary-fg-color) !important; - text-align: center; - text-decoration: none; - cursor: pointer; -} - -.rounded-button:hover { - background-color: var(--md-typeset-btn-hover-color); -} - -.rounded-button span { - margin: 0; - margin-bottom: 0.07em; - white-space: nowrap; -} - -.rounded-button svg { - fill: var(--md-primary-fg-color); - width: auto; - height: 1.2em; - margin-right: 0.5em; -} - -/* device image */ -.device-on-hover { - width: auto; - transition: transform 0.3s ease-in-out, filter 0.3s ease-in-out; -} - -a:hover .device-on-hover { - filter: drop-shadow(0 0 0.2rem rgba(0, 0, 0, 0.15)); - transform: scale(1.01); -} - -/* text button */ -.reading-media { - display: flex; - justify-content: center; - align-items: flex-end; - height: 32vw; -} - -.media-block { - height: 100%; - margin: 0 0.2em; -} - -.text-button { - width: auto; - color: var(--md-typeset-btn-color); - text-decoration: none; - text-align: center; - margin: 2.7em auto; -} - -.text-button span { - white-space: nowrap; -} - -.text-button svg { - display: inline-block; - fill: var(--md-typeset-btn-color); - width: auto; - height: 0.9em; - background-size: cover; - padding-top: 0.17em; - margin-left: 0.15em; -} - -a:hover .text-button span { - text-decoration: underline; -} - -/* hero image */ .hero-div { height: min(84vh, 75vw); width: min(112vh, 100vw); - margin: 0 auto; - margin-top: -2.4rem; + margin: -2.4rem auto 0; padding: 0; position: relative; font-size: min(1.8vh, 2.5vw); @@ -384,13 +370,12 @@ a:hover .text-button span { } .hero-bg { - height: 100%; width: 100%; + height: 100%; object-fit: cover; position: absolute; } -/* hover on the planets */ .hero-div>a>img { width: auto; position: absolute; @@ -402,7 +387,6 @@ a:hover .text-button span { position: absolute; transform: translateX(-50%) translateY(-50%); white-space: nowrap; - /* prevent line breaks */ color: white; } @@ -412,21 +396,105 @@ a:hover .text-button span { } .hero-div>a:hover>span { - text-decoration: underline; color: var(--md-typeset-btn-color); + text-decoration: underline; } .heading-div { width: 100%; position: absolute; - transform: translateX(-50%); left: 50%; bottom: min(2vh, 3vw); + transform: translateX(-50%); pointer-events: none; color: #fff; } -/* code badge */ +/* Landing page CTAs */ +.rounded-button { + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0 0.1em; + padding: 0.6em 1.3em; + border: none; + border-radius: 10em; + background-color: var(--md-typeset-btn-color); + color: var(--md-primary-fg-color) !important; + text-align: center; + text-decoration: none; + cursor: pointer; +} + +.rounded-button:hover { + background-color: var(--md-typeset-btn-hover-color); +} + +.rounded-button span { + margin: 0 0 0.07em; + white-space: nowrap; +} + +.rounded-button svg { + width: auto; + height: 1.2em; + margin-right: 0.5em; + fill: var(--md-primary-fg-color); +} + +.reading-media { + display: flex; + justify-content: center; + align-items: flex-end; + height: 32vw; +} + +.reading-media+p { + margin-top: 1em !important; +} + +.media-block { + height: 100%; + margin: 0 0.2em; +} + +.text-button { + width: auto; + margin: 2.7em auto; + color: var(--md-typeset-btn-color); + text-align: center; + text-decoration: none; +} + +.text-button span { + white-space: nowrap; +} + +.text-button svg { + display: inline-block; + width: auto; + height: 0.9em; + margin-left: 0.15em; + padding-top: 0.17em; + fill: var(--md-typeset-btn-color); + background-size: cover; +} + +a:hover .text-button span { + text-decoration: underline; +} + +.device-on-hover { + width: auto; + transition: transform 0.3s ease-in-out, filter 0.3s ease-in-out; +} + +a:hover .device-on-hover { + filter: drop-shadow(0 0 0.2rem rgba(0, 0, 0, 0.15)); + transform: scale(1.01); +} + +/* Landing page content blocks */ .code-badge { width: 100%; height: auto; @@ -434,11 +502,10 @@ a:hover .text-button span { } .code-badge img { - height: 1.07em; width: auto; + height: 1.07em; } -/* brief intro */ .intro-container { display: flex; align-items: center; @@ -455,14 +522,13 @@ a:hover .text-button span { .intro-text { flex-grow: 1; - /* fill the space */ display: flex; flex-direction: column; justify-content: center; - text-align: left; align-items: flex-start; width: fit-content; margin: 2em; + text-align: left; } .intro-text>div { @@ -471,6 +537,10 @@ a:hover .text-button span { margin: 0 auto; } +.intro-text svg path { + fill: #3b3b3b; +} + .endor-text { width: 50%; } @@ -480,7 +550,10 @@ a:hover .text-button span { font-weight: bold; } -/* contributors table */ +.home-div .intro-quote { + color: var(--md-default-fg-color--light) !important; +} + .profile-div { display: flex; flex-wrap: wrap; @@ -495,6 +568,11 @@ a:hover .text-button span { text-align: center; } +.profile-cell a:hover b, +.profile-cell a:focus-visible b { + text-decoration: underline; +} + .profile-img { width: 5em; border-radius: 50%; @@ -517,7 +595,37 @@ a:hover .text-button span { margin: 0 auto; } -/* Hide navigation */ +/* Embedded media */ +.video-container { + position: relative; + padding-bottom: 56.25%; + height: 0; +} + +.video-container iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.starfield { + position: absolute; + width: 100%; + height: 100%; + z-index: 0; + background-color: var(--hero-starfield-bg-color, transparent); +} + +.starfield-origin { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +/* Responsive adjustments */ @media screen and (max-width: 76.25em) { .section-content { max-width: 95vw; @@ -532,7 +640,6 @@ a:hover .text-button span { } } -/* mobile devices */ @media screen and (max-width: 60em) { .home-div { font-size: 0.75rem; @@ -571,209 +678,4 @@ a:hover .text-button span { flex: 1 1 30%; } } - -.video-container { - position: relative; - padding-bottom: 56.25%; - /* 16:9 */ - height: 0; -} - -.video-container iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -/* starfield */ -.starfield { - position: absolute; - width: 100%; - height: 100%; - z-index: 0; - background-color: var(--hero-starfield-bg-color, transparent); -} - -.starfield-origin { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -/* Zensical-specific adjustments merged into the main stylesheet. */ -:root>* { - --md-accent-fg-color: var(--md-typeset-a-color); - --md-admonition-pythontutor-color: var(--md-code-bg-color); - --hello-algo-sidebar-width: 13rem; -} - -[data-md-color-scheme="slate"] { - --md-accent-fg-color: var(--md-typeset-a-color); - --md-admonition-pythontutor-color: var(--md-code-bg-color); - --md-body-bg-color: var(--md-default-bg-color); - --md-default-bg-color--light: rgb(30 30 30 / 0.8); -} - -[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary), -[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary) :is(p, li, strong, em, sub, sup, code, a) { - background-color: #f5f5f5; - color: #1d1d20; -} - -body { - background-color: var(--md-default-bg-color); -} - -html:has(body[data-md-color-scheme="slate"]) { - background-color: #1e1e1e; -} - -html:has(body[data-md-color-scheme="default"]) { - background-color: #ffffff; -} - -.home-div[data-md-color-scheme="default"], -.home-div[data-md-color-scheme="default"] h1, -.home-div[data-md-color-scheme="default"] h2, -.home-div[data-md-color-scheme="default"] h3, -.home-div[data-md-color-scheme="default"] h4, -.home-div[data-md-color-scheme="default"] h5, -.home-div[data-md-color-scheme="default"] h6 { - color: var(--md-default-fg-color); -} - -.home-div[data-md-color-scheme="slate"], -.home-div[data-md-color-scheme="slate"] h1, -.home-div[data-md-color-scheme="slate"] h2, -.home-div[data-md-color-scheme="slate"] h3, -.home-div[data-md-color-scheme="slate"] h4, -.home-div[data-md-color-scheme="slate"] h5, -.home-div[data-md-color-scheme="slate"] h6 { - color: var(--md-default-fg-color); -} - -.home-div .intro-quote { - color: var(--md-default-fg-color--light) !important; -} - -.reading-media+p { - margin-top: 1em !important; -} - -.md-typeset .admonition-title:before, -.md-typeset summary:before, -.md-typeset summary:after { - top: 50%; -} - -.md-typeset .admonition-title:before, -.md-typeset summary:before { - transform: translateY(-50%); -} - -.md-typeset summary:after { - transform: translateY(-50%) rotate(0deg); -} - -.md-typeset details[open]>summary:after { - transform: translateY(-50%) rotate(90deg); -} - -.md-nav__link[for] { - color: inherit !important; -} - -.md-nav__link[for].md-nav__link--active { - color: var(--md-accent-fg-color) !important; -} - -@media screen and (min-width: 76.25em) { - .md-grid { - max-width: calc(61rem + 2 * (var(--hello-algo-sidebar-width) - 12.1rem)); - } - - .md-sidebar--primary, - .md-sidebar--secondary { - width: var(--hello-algo-sidebar-width); - } - - [dir="ltr"] .md-sidebar__inner { - padding-right: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); - } - - [dir="rtl"] .md-sidebar__inner { - padding-left: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); - } -} - -.md-sidebar--primary .md-sidebar__scrollwrap { - scrollbar-color: var(--md-default-fg-color--lighter) transparent; -} - -.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb, -.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { - background-color: var(--md-default-fg-color--lighter); -} - -.md-footer, -.md-footer__inner, -.md-footer-meta, -.md-footer__link, -.md-footer__link:hover { - background-color: var(--md-default-bg-color); -} - -.md-footer { - border-top: 0.05rem solid var(--md-default-fg-color--lightest); -} - -[data-md-color-scheme="slate"] .md-footer, -[data-md-color-scheme="slate"] .md-footer__inner, -[data-md-color-scheme="slate"] .md-footer-meta, -[data-md-color-scheme="slate"] .md-footer__link, -[data-md-color-scheme="slate"] .md-footer__link:hover { - background-color: var(--md-default-bg-color); -} - -.md-banner { - background-color: var(--md-default-bg-color); -} - -.md-typeset .admonition.pythontutor, -.md-typeset details.pythontutor, -.md-typeset .pythontutor>.admonition-title, -.md-typeset .pythontutor>summary { - background-color: var(--md-code-bg-color); -} - -.md-typeset .pythontutor>.admonition-title::before, -.md-typeset .pythontutor>summary::before, -.md-typeset .pythontutor>summary::after { - top: 50%; -} - -.md-typeset .pythontutor>.admonition-title::before, -.md-typeset .pythontutor>summary::before { - transform: translateY(-50%); -} - -.md-typeset .pythontutor>summary::after { - transform: translateY(-50%) rotate(0deg); -} - -.md-typeset details[open].pythontutor>summary::after { - transform: translateY(-50%) rotate(90deg); -} - -.md-typeset a:not(.md-button) { - text-decoration: none; -} - -.md-typeset a:not(.md-button):hover, -.md-typeset a:not(.md-button):focus-visible { - text-decoration: underline; -} -/*! update cache: 20260331044513 */ +/*! update cache: 20260331053206 */ diff --git a/ja/assets/covers/chapter_appendix.jpg b/ja/assets/covers/chapter_appendix.jpg index 01e4dd552..1c4cacacb 100644 Binary files a/ja/assets/covers/chapter_appendix.jpg and b/ja/assets/covers/chapter_appendix.jpg differ diff --git a/ja/assets/covers/chapter_array_and_linkedlist.jpg b/ja/assets/covers/chapter_array_and_linkedlist.jpg index 4cf13ceb3..5174b2035 100644 Binary files a/ja/assets/covers/chapter_array_and_linkedlist.jpg and b/ja/assets/covers/chapter_array_and_linkedlist.jpg differ diff --git a/ja/assets/covers/chapter_backtracking.jpg b/ja/assets/covers/chapter_backtracking.jpg index 6aa3208d2..4ea0ffc10 100644 Binary files a/ja/assets/covers/chapter_backtracking.jpg and b/ja/assets/covers/chapter_backtracking.jpg differ diff --git a/ja/assets/covers/chapter_complexity_analysis.jpg b/ja/assets/covers/chapter_complexity_analysis.jpg index 8c5a9d2e9..d78da9bea 100644 Binary files a/ja/assets/covers/chapter_complexity_analysis.jpg and b/ja/assets/covers/chapter_complexity_analysis.jpg differ diff --git a/ja/assets/covers/chapter_data_structure.jpg b/ja/assets/covers/chapter_data_structure.jpg index 7c1a4a69c..5e4751531 100644 Binary files a/ja/assets/covers/chapter_data_structure.jpg and b/ja/assets/covers/chapter_data_structure.jpg differ diff --git a/ja/assets/covers/chapter_divide_and_conquer.jpg b/ja/assets/covers/chapter_divide_and_conquer.jpg index 0b3b90eea..e95d52aec 100644 Binary files a/ja/assets/covers/chapter_divide_and_conquer.jpg and b/ja/assets/covers/chapter_divide_and_conquer.jpg differ diff --git a/ja/assets/covers/chapter_dynamic_programming.jpg b/ja/assets/covers/chapter_dynamic_programming.jpg index 98db1e161..69f0977b4 100644 Binary files a/ja/assets/covers/chapter_dynamic_programming.jpg and b/ja/assets/covers/chapter_dynamic_programming.jpg differ diff --git a/ja/assets/covers/chapter_graph.jpg b/ja/assets/covers/chapter_graph.jpg index 0062d7b4c..4b5fef52b 100644 Binary files a/ja/assets/covers/chapter_graph.jpg and b/ja/assets/covers/chapter_graph.jpg differ diff --git a/ja/assets/covers/chapter_greedy.jpg b/ja/assets/covers/chapter_greedy.jpg index 4508d88e6..7f72a307a 100644 Binary files a/ja/assets/covers/chapter_greedy.jpg and b/ja/assets/covers/chapter_greedy.jpg differ diff --git a/ja/assets/covers/chapter_hashing.jpg b/ja/assets/covers/chapter_hashing.jpg index 7fedeea75..18077867f 100644 Binary files a/ja/assets/covers/chapter_hashing.jpg and b/ja/assets/covers/chapter_hashing.jpg differ diff --git a/ja/assets/covers/chapter_heap.jpg b/ja/assets/covers/chapter_heap.jpg index 57e2c6c92..8484ab2b1 100644 Binary files a/ja/assets/covers/chapter_heap.jpg and b/ja/assets/covers/chapter_heap.jpg differ diff --git a/ja/assets/covers/chapter_introduction.jpg b/ja/assets/covers/chapter_introduction.jpg index d3dea8d22..6949c4115 100644 Binary files a/ja/assets/covers/chapter_introduction.jpg and b/ja/assets/covers/chapter_introduction.jpg differ diff --git a/ja/assets/covers/chapter_preface.jpg b/ja/assets/covers/chapter_preface.jpg index 9a89f9e09..22227fc9b 100644 Binary files a/ja/assets/covers/chapter_preface.jpg and b/ja/assets/covers/chapter_preface.jpg differ diff --git a/ja/assets/covers/chapter_searching.jpg b/ja/assets/covers/chapter_searching.jpg index 60e2bfe47..151e91079 100644 Binary files a/ja/assets/covers/chapter_searching.jpg and b/ja/assets/covers/chapter_searching.jpg differ diff --git a/ja/assets/covers/chapter_sorting.jpg b/ja/assets/covers/chapter_sorting.jpg index eadf95d40..b7539856a 100644 Binary files a/ja/assets/covers/chapter_sorting.jpg and b/ja/assets/covers/chapter_sorting.jpg differ diff --git a/ja/assets/covers/chapter_stack_and_queue.jpg b/ja/assets/covers/chapter_stack_and_queue.jpg index 7b164e31f..ca6e24fb1 100644 Binary files a/ja/assets/covers/chapter_stack_and_queue.jpg and b/ja/assets/covers/chapter_stack_and_queue.jpg differ diff --git a/ja/assets/covers/chapter_tree.jpg b/ja/assets/covers/chapter_tree.jpg index 78fc852c9..e47306dbb 100644 Binary files a/ja/assets/covers/chapter_tree.jpg and b/ja/assets/covers/chapter_tree.jpg differ diff --git a/ja/assets/javascripts/bundle.c2b142ea.min.js b/ja/assets/javascripts/bundle.c2b142ea.min.js index 8fa527bf3..9a524e732 100644 --- a/ja/assets/javascripts/bundle.c2b142ea.min.js +++ b/ja/assets/javascripts/bundle.c2b142ea.min.js @@ -1,4 +1,4 @@ "use strict";(()=>{var xc=Object.create;var kn=Object.defineProperty,wc=Object.defineProperties,Ec=Object.getOwnPropertyDescriptor,Tc=Object.getOwnPropertyDescriptors,Sc=Object.getOwnPropertyNames,Dr=Object.getOwnPropertySymbols,Oc=Object.getPrototypeOf,An=Object.prototype.hasOwnProperty,Fo=Object.prototype.propertyIsEnumerable;var jo=(e,t,r)=>t in e?kn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,H=(e,t)=>{for(var r in t||(t={}))An.call(t,r)&&jo(e,r,t[r]);if(Dr)for(var r of Dr(t))Fo.call(t,r)&&jo(e,r,t[r]);return e},He=(e,t)=>wc(e,Tc(t));var gr=(e,t)=>{var r={};for(var n in e)An.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Dr)for(var n of Dr(e))t.indexOf(n)<0&&Fo.call(e,n)&&(r[n]=e[n]);return r};var Cn=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Lc=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Sc(t))!An.call(e,o)&&o!==r&&kn(e,o,{get:()=>t[o],enumerable:!(n=Ec(t,o))||n.enumerable});return e};var _r=(e,t,r)=>(r=e!=null?xc(Oc(e)):{},Lc(t||!e||!e.__esModule?kn(r,"default",{value:e,enumerable:!0}):r,e));var Uo=(e,t,r)=>new Promise((n,o)=>{var i=c=>{try{s(r.next(c))}catch(l){o(l)}},a=c=>{try{s(r.throw(c))}catch(l){o(l)}},s=c=>c.done?n(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var Do=Cn((Hn,No)=>{(function(e,t){typeof Hn=="object"&&typeof No!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Hn,(function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(_){return!!(_&&_!==document&&_.nodeName!=="HTML"&&_.nodeName!=="BODY"&&"classList"in _&&"contains"in _.classList)}function c(_){var de=_.type,be=_.tagName;return!!(be==="INPUT"&&a[de]&&!_.readOnly||be==="TEXTAREA"&&!_.readOnly||_.isContentEditable)}function l(_){_.classList.contains("focus-visible")||(_.classList.add("focus-visible"),_.setAttribute("data-focus-visible-added",""))}function u(_){_.hasAttribute("data-focus-visible-added")&&(_.classList.remove("focus-visible"),_.removeAttribute("data-focus-visible-added"))}function p(_){_.metaKey||_.altKey||_.ctrlKey||(s(r.activeElement)&&l(r.activeElement),n=!0)}function d(_){n=!1}function m(_){s(_.target)&&(n||c(_.target))&&l(_.target)}function h(_){s(_.target)&&(_.target.classList.contains("focus-visible")||_.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(_.target))}function v(_){document.visibilityState==="hidden"&&(o&&(n=!0),x())}function x(){document.addEventListener("mousemove",E),document.addEventListener("mousedown",E),document.addEventListener("mouseup",E),document.addEventListener("pointermove",E),document.addEventListener("pointerdown",E),document.addEventListener("pointerup",E),document.addEventListener("touchmove",E),document.addEventListener("touchstart",E),document.addEventListener("touchend",E)}function w(){document.removeEventListener("mousemove",E),document.removeEventListener("mousedown",E),document.removeEventListener("mouseup",E),document.removeEventListener("pointermove",E),document.removeEventListener("pointerdown",E),document.removeEventListener("pointerup",E),document.removeEventListener("touchmove",E),document.removeEventListener("touchstart",E),document.removeEventListener("touchend",E)}function E(_){_.target.nodeName&&_.target.nodeName.toLowerCase()==="html"||(n=!1,w())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",d,!0),document.addEventListener("pointerdown",d,!0),document.addEventListener("touchstart",d,!0),document.addEventListener("visibilitychange",v,!0),x(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var So=Cn((M0,vs)=>{"use strict";var Gu=/["'&<>]/;vs.exports=Ju;function Ju(e){var t=""+e,r=Gu.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i{(function(t,r){typeof jr=="object"&&typeof Lo=="object"?Lo.exports=r():typeof define=="function"&&define.amd?define([],r):typeof jr=="object"?jr.ClipboardJS=r():t.ClipboardJS=r()})(jr,function(){return(function(){var e={686:(function(n,o,i){"use strict";i.d(o,{default:function(){return vr}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),p=i.n(u);function d(B){try{return document.execCommand(B)}catch(C){return!1}}var m=function(C){var k=p()(C);return d("cut"),k},h=m;function v(B){var C=document.documentElement.getAttribute("dir")==="rtl",k=document.createElement("textarea");k.style.fontSize="12pt",k.style.border="0",k.style.padding="0",k.style.margin="0",k.style.position="absolute",k.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return k.style.top="".concat(D,"px"),k.setAttribute("readonly",""),k.value=B,k}var x=function(C,k){var D=v(C);k.container.appendChild(D);var W=p()(D);return d("copy"),D.remove(),W},w=function(C){var k=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=x(C,k):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=x(C.value,k):(D=p()(C),d("copy")),D},E=w;function _(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?_=function(k){return typeof k}:_=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},_(B)}var de=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},k=C.action,D=k===void 0?"copy":k,W=C.container,Z=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Z!==void 0)if(Z&&_(Z)==="object"&&Z.nodeType===1){if(D==="copy"&&Z.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(Z.hasAttribute("readonly")||Z.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return E(We,{container:W});if(Z)return D==="cut"?h(Z):E(Z,{container:W})},be=de;function M(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?M=function(k){return typeof k}:M=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},M(B)}function O(B,C){if(!(B instanceof C))throw new TypeError("Cannot call a class as a function")}function N(B,C){for(var k=0;k0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof W.action=="function"?W.action:this.defaultAction,this.target=typeof W.target=="function"?W.target:this.defaultTarget,this.text=typeof W.text=="function"?W.text:this.defaultText,this.container=M(W.container)==="object"?W.container:document.body}},{key:"listenClick",value:function(W){var Z=this;this.listener=l()(W,"click",function(We){return Z.onClick(We)})}},{key:"onClick",value:function(W){var Z=W.delegateTarget||W.currentTarget,We=this.action(Z)||"copy",Gt=be({action:We,container:this.container,target:this.target(Z),text:this.text(Z)});this.emit(Gt?"success":"error",{action:We,text:Gt,trigger:Z,clearSelection:function(){Z&&Z.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(W){return Yt("action",W)}},{key:"defaultTarget",value:function(W){var Z=Yt("target",W);if(Z)return document.querySelector(Z)}},{key:"defaultText",value:function(W){return Yt("text",W)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(W){var Z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return E(W,Z)}},{key:"cut",value:function(W){return h(W)}},{key:"isSupported",value:function(){var W=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Z=typeof W=="string"?[W]:W,We=!!document.queryCommandSupported;return Z.forEach(function(Gt){We=We&&!!document.queryCommandSupported(Gt)}),We}}]),k})(s()),vr=Mt}),828:(function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a}),438:(function(n,o,i){var a=i(828);function s(u,p,d,m,h){var v=l.apply(this,arguments);return u.addEventListener(d,v,h),{destroy:function(){u.removeEventListener(d,v,h)}}}function c(u,p,d,m,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof d=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,d,m,h)}))}function l(u,p,d,m){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&m.call(u,h)}}n.exports=c}),879:(function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}}),370:(function(n,o,i){var a=i(879),s=i(438);function c(d,m,h){if(!d&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(d))return l(d,m,h);if(a.nodeList(d))return u(d,m,h);if(a.string(d))return p(d,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(d,m,h){return d.addEventListener(m,h),{destroy:function(){d.removeEventListener(m,h)}}}function u(d,m,h){return Array.prototype.forEach.call(d,function(v){v.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(d,function(v){v.removeEventListener(m,h)})}}}function p(d,m,h){return s(document.body,d,m,h)}n.exports=c}),817:(function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}n.exports=o}),279:(function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c0&&i[i.length-1])&&(l[0]===6||l[0]===2)){r=0;continue}if(l[0]===3&&(!i||l[1]>i[0]&&l[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function te(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function ne(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||c(m,v)})},h&&(o[m]=h(o[m])))}function c(m,h){try{l(n[m](h))}catch(v){d(i[0][3],v)}}function l(m){m.value instanceof kt?Promise.resolve(m.value.v).then(u,p):d(i[0][2],m)}function u(m){c("next",m)}function p(m){c("throw",m)}function d(m,h){m(h),i.shift(),i.length&&c(i[0][0],i[0][1])}}function zo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof $e=="function"?$e(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),o(s,c,a.done,a.value)})}}function o(i,a,s,c){Promise.resolve(c).then(function(l){i({value:l,done:s})},a)}}function F(e){return typeof e=="function"}function Jt(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Vr=Jt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: `+r.map(function(n,o){return o+1+") "+n.toString()}).join(` `):"",this.name="UnsubscriptionError",this.errors=r}});function ct(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var rt=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=$e(a),c=s.next();!c.done;c=s.next()){var l=c.value;l.remove(this)}}catch(v){t={error:v}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(F(u))try{u()}catch(v){i=v instanceof Vr?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var d=$e(p),m=d.next();!m.done;m=d.next()){var h=m.value;try{qo(h)}catch(v){i=i!=null?i:[],v instanceof Vr?i=ne(ne([],te(i)),te(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{m&&!m.done&&(o=d.return)&&o.call(d)}finally{if(n)throw n.error}}}if(i)throw new Vr(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)qo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&ct(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&ct(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var Pn=rt.EMPTY;function zr(e){return e instanceof rt||e&&"closed"in e&&F(e.remove)&&F(e.add)&&F(e.unsubscribe)}function qo(e){F(e)?e():e.unsubscribe()}var Je={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Xt={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Pn:(this.currentObservers=null,s.push(r),new rt(function(){n.currentObservers=null,ct(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new Qo(r,n)},t})(U);var Qo=(function(e){ue(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Pn},t})(I);var Un=(function(e){ue(t,e);function t(r){var n=e.call(this)||this;return n._value=r,n}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var n=e.prototype._subscribe.call(this,r);return!n.closed&&r.next(this._value),n},t.prototype.getValue=function(){var r=this,n=r.hasError,o=r.thrownError,i=r._value;if(n)throw o;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(I);var xr={now:function(){return(xr.delegate||Date).now()},delegate:void 0};var wr=(function(e){ue(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=xr);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.schedule.call(this,r,n):(this.delay=n,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,n){return n>0||this.closed?e.prototype.execute.call(this,r,n):this._execute(r,n)},t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.flush(this),0)},t})(tr);var ri=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(rr);var Wn=new ri(ti);var ni=(function(e){ue(t,e);function t(r,n){var o=e.call(this,r,n)||this;return o.scheduler=r,o.work=n,o}return t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!==null&&o>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=er.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&n===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(er.cancelAnimationFrame(n),r._scheduled=void 0)},t})(tr);var oi=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n;r?n=r.id:(n=this._scheduled,this._scheduled=void 0);var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t})(rr);var je=new oi(ni);var y=new U(function(e){return e.complete()});function Br(e){return e&&F(e.schedule)}function Vn(e){return e[e.length-1]}function _t(e){return F(Vn(e))?e.pop():void 0}function qe(e){return Br(Vn(e))?e.pop():void 0}function Yr(e,t){return typeof Vn(e)=="number"?e.pop():t}var nr=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function Gr(e){return F(e==null?void 0:e.then)}function Jr(e){return F(e[Qt])}function Xr(e){return Symbol.asyncIterator&&F(e==null?void 0:e[Symbol.asyncIterator])}function Zr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Rc(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qr=Rc();function en(e){return F(e==null?void 0:e[Qr])}function tn(e){return Vo(this,arguments,function(){var r,n,o,i;return Wr(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,kt(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,kt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,kt(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function rn(e){return F(e==null?void 0:e.getReader)}function q(e){if(e instanceof U)return e;if(e!=null){if(Jr(e))return jc(e);if(nr(e))return Fc(e);if(Gr(e))return Uc(e);if(Xr(e))return ii(e);if(en(e))return Nc(e);if(rn(e))return Dc(e)}throw Zr(e)}function jc(e){return new U(function(t){var r=e[Qt]();if(F(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Fc(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?L(function(o,i){return e(o,i,n)}):Oe,Me(1),r?ot(t):wi(function(){return new on}))}}function Gn(e){return e<=0?function(){return y}:S(function(t,r){var n=[];t.subscribe(T(r,function(o){n.push(o),e=2,!0))}function xe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new I}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u,p,d,m=0,h=!1,v=!1,x=function(){p==null||p.unsubscribe(),p=void 0},w=function(){x(),u=d=void 0,h=v=!1},E=function(){var _=u;w(),_==null||_.unsubscribe()};return S(function(_,de){m++,!v&&!h&&x();var be=d=d!=null?d:r();de.add(function(){m--,m===0&&!v&&!h&&(p=Jn(E,c))}),be.subscribe(de),!u&&m>0&&(u=new Ct({next:function(M){return be.next(M)},error:function(M){v=!0,x(),p=Jn(w,o,M),be.error(M)},complete:function(){h=!0,x(),p=Jn(w,a),be.complete()}}),q(_).subscribe(u))})(l)}}function Jn(e,t){for(var r=[],n=2;ne.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function G(e,t=document){let r=Le(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function Le(e,t=document){return t.querySelector(e)||void 0}function xt(){var e,t,r,n;return(n=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?n:void 0}var il=R(b(document.body,"focusin"),b(document.body,"focusout")).pipe(Be(1),J(void 0),f(()=>xt()||document.body),se(1));function ir(e){return il.pipe(f(t=>e.contains(t)),ie())}function Ft(e,t){let{matches:r}=matchMedia("(hover)");return j(()=>(r?R(b(e,"mouseenter").pipe(f(()=>!0)),b(e,"mouseleave").pipe(f(()=>!1))):R(b(e,"touchstart").pipe(f(()=>!0)),b(e,"touchend").pipe(f(()=>!1)),b(e,"touchcancel").pipe(f(()=>!1)))).pipe(t?Tr(o=>Ve(+!o*t)):Oe,J(!0,e.matches(":hover"))))}function Oi(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Oi(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Oi(n,o);return n}function Li(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function ar(e){let t=A("script",{src:e});return j(()=>(document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(g(()=>zn(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(f(()=>{}),V(()=>document.head.removeChild(t)),Me(1))))}var Mi=new I,al=j(()=>typeof ResizeObserver=="undefined"?ar("https://unpkg.com/resize-observer-polyfill"):Y(void 0)).pipe(f(()=>new ResizeObserver(e=>e.forEach(t=>Mi.next(t)))),g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Ae(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Re(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return al.pipe($(r=>r.observe(t)),g(r=>Mi.pipe(L(n=>n.target===t),V(()=>r.unobserve(t)))),f(()=>Ae(e)),J(Ae(e)))}function Mr(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ki(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Ai(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function wt(e){return{x:e.offsetLeft,y:e.offsetTop}}function Ci(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Hi(e){return R(b(window,"load"),b(window,"resize")).pipe(Xe(0,je),f(()=>wt(e)),J(wt(e)))}function ln(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ut(e){return R(b(e,"scroll"),b(window,"scroll"),b(window,"resize")).pipe(Xe(0,je),f(()=>ln(e)),J(ln(e)))}var $i=new I,sl=j(()=>Y(new IntersectionObserver(e=>{for(let t of e)$i.next(t)},{threshold:0}))).pipe(g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Et(e){return sl.pipe($(t=>t.observe(e)),g(t=>$i.pipe(L(({target:r})=>r===e),V(()=>t.unobserve(e)),f(({isIntersecting:r})=>r))))}var cl=Object.create,la=Object.defineProperty,ll=Object.getOwnPropertyDescriptor,ul=Object.getOwnPropertyNames,pl=Object.getPrototypeOf,fl=Object.prototype.hasOwnProperty,ml=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),dl=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ul(t))!fl.call(e,o)&&o!==r&&la(e,o,{get:()=>t[o],enumerable:!(n=ll(t,o))||n.enumerable});return e},hl=(e,t,r)=>(r=e!=null?cl(pl(e)):{},dl(t||!e||!e.__esModule?la(r,"default",{value:e,enumerable:!0}):r,e)),vl=ml((e,t)=>{var r="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,c=/^0o[0-7]+$/i,l=parseInt,u=typeof global=="object"&&global&&global.Object===Object&&global,p=typeof self=="object"&&self&&self.Object===Object&&self,d=u||p||Function("return this")(),m=Object.prototype,h=m.toString,v=Math.max,x=Math.min,w=function(){return d.Date.now()};function E(O,N,ee){var le,ce,Ne,bt,De,st,tt=0,Yt=!1,Mt=!1,vr=!0;if(typeof O!="function")throw new TypeError(r);N=M(N)||0,_(ee)&&(Yt=!!ee.leading,Mt="maxWait"in ee,Ne=Mt?v(M(ee.maxWait)||0,N):Ne,vr="trailing"in ee?!!ee.trailing:vr);function B(Te){var gt=le,br=ce;return le=ce=void 0,tt=Te,bt=O.apply(br,gt),bt}function C(Te){return tt=Te,De=setTimeout(W,N),Yt?B(Te):bt}function k(Te){var gt=Te-st,br=Te-tt,Ro=N-gt;return Mt?x(Ro,Ne-br):Ro}function D(Te){var gt=Te-st,br=Te-tt;return st===void 0||gt>=N||gt<0||Mt&&br>=Ne}function W(){var Te=w();if(D(Te))return Z(Te);De=setTimeout(W,k(Te))}function Z(Te){return De=void 0,vr&&le?B(Te):(le=ce=void 0,bt)}function We(){De!==void 0&&clearTimeout(De),tt=0,le=st=ce=De=void 0}function Gt(){return De===void 0?bt:Z(w())}function Nr(){var Te=w(),gt=D(Te);if(le=arguments,ce=this,st=Te,gt){if(De===void 0)return C(st);if(Mt)return De=setTimeout(W,N),B(st)}return De===void 0&&(De=setTimeout(W,N)),bt}return Nr.cancel=We,Nr.flush=Gt,Nr}function _(O){var N=typeof O;return!!O&&(N=="object"||N=="function")}function de(O){return!!O&&typeof O=="object"}function be(O){return typeof O=="symbol"||de(O)&&h.call(O)==o}function M(O){if(typeof O=="number")return O;if(be(O))return n;if(_(O)){var N=typeof O.valueOf=="function"?O.valueOf():O;O=_(N)?N+"":N}if(typeof O!="string")return O===0?O:+O;O=O.replace(i,"");var ee=s.test(O);return ee||c.test(O)?l(O.slice(2),ee?2:8):a.test(O)?n:+O}t.exports=E}),yn,K,ua,pa,Nt,Pi,fa,ma,da,lo,to,ro,bl,Ar={},ha=[],gl=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Pr=Array.isArray;function pt(e,t){for(var r in t)e[r]=t[r];return e}function uo(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function Wt(e,t,r){var n,o,i,a={};for(i in t)i=="key"?n=t[i]:i=="ref"?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?yn.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return fn(e,a,n,o,null)}function fn(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o!=null?o:++ua,__i:-1,__u:0};return o==null&&K.vnode!=null&&K.vnode(i),i}function ft(e){return e.children}function at(e,t){this.props=e,this.context=t}function cr(e,t){if(t==null)return e.__?cr(e.__,e.__i+1):null;for(var r;ts&&Nt.sort(ma),e=Nt.shift(),s=Nt.length,e.__d&&(r=void 0,n=void 0,o=(n=(t=e).__v).__e,i=[],a=[],t.__P&&((r=pt({},n)).__v=n.__v+1,K.vnode&&K.vnode(r),po(t.__P,r,n,t.__n,t.__P.namespaceURI,32&n.__u?[o]:null,i,o!=null?o:cr(n),!!(32&n.__u),a),r.__v=n.__v,r.__.__k[r.__i]=r,_a(i,r,a),n.__e=n.__=null,r.__e!=o&&va(r)));vn.__r=0}function ba(e,t,r,n,o,i,a,s,c,l,u){var p,d,m,h,v,x,w,E=n&&n.__k||ha,_=t.length;for(c=_l(r,t,E,c,_),p=0;p<_;p++)(m=r.__k[p])!=null&&(d=m.__i==-1?Ar:E[m.__i]||Ar,m.__i=p,x=po(e,m,d,o,i,a,s,c,l,u),h=m.__e,m.ref&&d.ref!=m.ref&&(d.ref&&fo(d.ref,null,m),u.push(m.ref,m.__c||h,m)),v==null&&h!=null&&(v=h),(w=!!(4&m.__u))||d.__k===m.__k?c=ga(m,c,e,w):typeof m.type=="function"&&x!==void 0?c=x:h&&(c=h.nextSibling),m.__u&=-7);return r.__e=v,c}function _l(e,t,r,n,o){var i,a,s,c,l,u=r.length,p=u,d=0;for(e.__k=new Array(o),i=0;i0?fn(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):a).__=e,a.__b=e.__b+1,s=null,(l=a.__i=yl(a,r,c,p))!=-1&&(p--,(s=r[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(o>u?d--:oc?d--:d++,a.__u|=4))):e.__k[i]=null;if(p)for(i=0;i(u?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return a}return-1}function Ri(e,t,r){t[0]=="-"?e.setProperty(t,r!=null?r:""):e[t]=r==null?"":typeof r!="number"||gl.test(t)?r:r+"px"}function un(e,t,r,n,o){var i,a;e:if(t=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)r&&t in r||Ri(e.style,t,"");if(r)for(t in r)n&&r[t]==n[t]||Ri(e.style,t,r[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(da,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=r,r?n?r.u=n.u:(r.u=lo,e.addEventListener(t,i?ro:to,i)):e.removeEventListener(t,i?ro:to,i);else{if(o=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=r!=null?r:"";break e}catch(s){}typeof r=="function"||(r==null||r===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&r==1?"":r))}}function ji(e){return function(t){if(this.l){var r=this.l[t.type+e];if(t.t==null)t.t=lo++;else if(t.t0?e:Pr(e)?e.map(ya):pt({},e)}function xl(e,t,r,n,o,i,a,s,c){var l,u,p,d,m,h,v,x=r.props,w=t.props,E=t.type;if(E=="svg"?o="http://www.w3.org/2000/svg":E=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(l=0;l=r.__.length&&r.__.push({}),r.__[e]}function bn(e){return $r=1,Tl(Ta,e)}function Tl(e,t,r){var n=mo(Hr++,2);if(n.t=e,!n.__c&&(n.__=[r?r(t):Ta(void 0,t),function(s){var c=n.__N?n.__N[0]:n.__[0],l=n.t(c,s);c!==l&&(n.__N=[l,n.__[1]],n.__c.setState({}))}],n.__c=ve,!ve.__f)){var o=function(s,c,l){if(!n.__c.__H)return!0;var u=n.__c.__H.__.filter(function(d){return!!d.__c});if(u.every(function(d){return!d.__N}))return!i||i.call(this,s,c,l);var p=n.__c.props!==s;return u.forEach(function(d){if(d.__N){var m=d.__[0];d.__=d.__N,d.__N=void 0,m!==d.__[0]&&(p=!0)}}),i&&i.call(this,s,c,l)||p};ve.__f=!0;var i=ve.shouldComponentUpdate,a=ve.componentWillUpdate;ve.componentWillUpdate=function(s,c,l){if(this.__e){var u=i;i=void 0,o(s,c,l),i=u}a&&a.call(this,s,c,l)},ve.shouldComponentUpdate=o}return n.__N||n.__}function mt(e,t){var r=mo(Hr++,3);!we.__s&&Ea(r.__H,t)&&(r.__=e,r.u=t,ve.__H.__h.push(r))}function Vt(e){return $r=5,ur(function(){return{current:e}},[])}function ur(e,t){var r=mo(Hr++,7);return Ea(r.__H,t)&&(r.__=e(),r.__H=t,r.__h=e),r.__}function Sl(e,t){return $r=8,ur(function(){return e},t)}function Ol(){for(var e;e=wa.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(mn),e.__H.__h.forEach(oo),e.__H.__h=[]}catch(t){e.__H.__h=[],we.__e(t,e.__v)}}we.__b=function(e){ve=null,Ui&&Ui(e)},we.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),zi&&zi(e,t)},we.__r=function(e){Ni&&Ni(e),Hr=0;var t=(ve=e.__c).__H;t&&(Zn===ve?(t.__h=[],ve.__h=[],t.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(t.__h.forEach(mn),t.__h.forEach(oo),t.__h=[],Hr=0)),Zn=ve},we.diffed=function(e){Di&&Di(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(wa.push(t)!==1&&Fi===we.requestAnimationFrame||((Fi=we.requestAnimationFrame)||Ll)(Ol)),t.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),Zn=ve=null},we.__c=function(e,t){t.some(function(r){try{r.__h.forEach(mn),r.__h=r.__h.filter(function(n){return!n.__||oo(n)})}catch(n){t.some(function(o){o.__h&&(o.__h=[])}),t=[],we.__e(n,r.__v)}}),Wi&&Wi(e,t)},we.unmount=function(e){Vi&&Vi(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(n){try{mn(n)}catch(o){t=o}}),r.__H=void 0,t&&we.__e(t,r.__v))};var qi=typeof requestAnimationFrame=="function";function Ll(e){var t,r=function(){clearTimeout(n),qi&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,35);qi&&(t=requestAnimationFrame(r))}function mn(e){var t=ve,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),ve=t}function oo(e){var t=ve;e.__c=e.__(),ve=t}function Ea(e,t){return!e||e.length!==t.length||t.some(function(r,n){return r!==e[n]})}function Ta(e,t){return typeof t=="function"?t(e):t}function Ml(e,t){for(var r in t)e[r]=t[r];return e}function Ki(e,t){for(var r in e)if(r!=="__source"&&!(r in t))return!0;for(var n in t)if(n!=="__source"&&e[n]!==t[n])return!0;return!1}function Bi(e,t){this.props=e,this.context=t}(Bi.prototype=new at).isPureReactComponent=!0,Bi.prototype.shouldComponentUpdate=function(e,t){return Ki(this.props,e)||Ki(this.state,t)};var Yi=K.__b;K.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),Yi&&Yi(e)};var Yx=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,kl=K.__e;K.__e=function(e,t,r,n){if(e.then){for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return t.__e==null&&(t.__e=r.__e,t.__k=r.__k),o.__c(e,t)}kl(e,t,r,n)};var Gi=K.unmount;function Sa(e,t,r){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=Ml({},e)).__c!=null&&(e.__c.__P===r&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return Sa(n,t,r)})),e}function Oa(e,t,r){return e&&r&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return Oa(n,t,r)}),e.__c&&e.__c.__P===t&&(e.__e&&r.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=r)),e}function Qn(){this.__u=0,this.o=null,this.__b=null}function La(e){var t=e.__.__c;return t&&t.__a&&t.__a(e)}function pn(){this.i=null,this.l=null}K.unmount=function(e){var t=e.__c;t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),Gi&&Gi(e)},(Qn.prototype=new at).__c=function(e,t){var r=t.__c,n=this;n.o==null&&(n.o=[]),n.o.push(r);var o=La(n.__v),i=!1,a=function(){i||(i=!0,r.__R=null,o?o(s):s())};r.__R=a;var s=function(){if(!--n.__u){if(n.state.__a){var c=n.state.__a;n.__v.__k[0]=Oa(c,c.__c.__P,c.__c.__O)}var l;for(n.setState({__a:n.__b=null});l=n.o.pop();)l.forceUpdate()}};n.__u++||32&t.__u||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(a,a)},Qn.prototype.componentWillUnmount=function(){this.o=[]},Qn.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=Sa(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__a&&Wt(ft,null,e.fallback);return o&&(o.__u&=-33),[Wt(ft,null,t.__a?null:e.children),o]};var Ji=function(e,t,r){if(++r[1]===r[0]&&e.l.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.l.size))for(r=e.i;r;){for(;r.length>3;)r.pop()();if(r[1]Object.freeze({get current(){return t.current}}),[])}var Nl=typeof globalThis<"u"&&typeof navigator<"u"&&typeof document<"u";function Dl(e,...t){var r;(r=e==null?void 0:e.addEventListener)==null||r.call(e,...t)}function Wl(e,...t){var r;(r=e==null?void 0:e.removeEventListener)==null||r.call(e,...t)}var Vl=(e,t)=>Object.hasOwn(e,t),zl=()=>!0,ql=()=>!1;function Kl(e=!1){let t=Vt(e),r=Sl(()=>t.current,[]);return mt(()=>(t.current=!0,()=>{t.current=!1}),[]),r}function Bl(e,...t){let r=Kl(),n=ka(t[1]),o=ur(()=>function(...i){r()&&(typeof n.current=="function"?n.current.apply(this,i):typeof n.current.handleEvent=="function"&&n.current.handleEvent.apply(this,i))},[]);mt(()=>{let i=Yl(e)?e.current:e;if(!i)return;let a=t.slice(2);return Dl(i,t[0],o,...a),()=>{Wl(i,t[0],o,...a)}},[e,t[0]])}function Yl(e){return e!==null&&typeof e=="object"&&Vl(e,"current")}var Gl=e=>typeof e=="function"?e:typeof e=="string"?t=>t.key===e:e?zl:ql,Jl=Nl?globalThis:null;function Aa(e,t,r=[],n={}){let{event:o="keydown",target:i=Jl,eventOptions:a}=n,s=ka(t),c=ur(()=>{let l=Gl(e);return function(u){l(u)&&s.current.call(this,u)}},r);Bl(i,o,c,a)}function Ca(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t1)St--;else{for(var e,t=!1;kr!==void 0;){var r=kr;for(kr=void 0,io++;r!==void 0;){var n=r.o;if(r.o=void 0,r.f&=-3,!(8&r.f)&&Pa(r))try{r.c()}catch(o){t||(e=o,t=!0)}r=n}}if(io=0,St--,t)throw e}}function Ql(e){if(St>0)return e();St++;try{return e()}finally{xn()}}var ae=void 0;function Ha(e){var t=ae;ae=void 0;try{return e()}finally{ae=t}}var kr=void 0,St=0,io=0,gn=0;function $a(e){if(ae!==void 0){var t=e.n;if(t===void 0||t.t!==ae)return t={i:0,S:e,p:ae.s,n:void 0,t:ae,e:void 0,x:void 0,r:t},ae.s!==void 0&&(ae.s.n=t),ae.s=t,e.n=t,32&ae.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=ae.s,t.n=void 0,ae.s.n=t,ae.s=t),t}}function Ce(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Ce.prototype.brand=Zl;Ce.prototype.h=function(){return!0};Ce.prototype.S=function(e){var t=this,r=this.t;r!==e&&e.e===void 0&&(e.x=r,this.t=e,r!==void 0?r.e=e:Ha(function(){var n;(n=t.W)==null||n.call(t)}))};Ce.prototype.U=function(e){var t=this;if(this.t!==void 0){var r=e.e,n=e.x;r!==void 0&&(r.x=n,e.e=void 0),n!==void 0&&(n.e=r,e.x=void 0),e===this.t&&(this.t=n,n===void 0&&Ha(function(){var o;(o=t.Z)==null||o.call(t)}))}};Ce.prototype.subscribe=function(e){var t=this;return qt(function(){var r=t.value,n=ae;ae=void 0;try{e(r)}finally{ae=n}},{name:"sub"})};Ce.prototype.valueOf=function(){return this.value};Ce.prototype.toString=function(){return this.value+""};Ce.prototype.toJSON=function(){return this.value};Ce.prototype.peek=function(){var e=ae;ae=void 0;try{return this.value}finally{ae=e}};Object.defineProperty(Ce.prototype,"value",{get:function(){var e=$a(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(io>100)throw new Error("Cycle detected");this.v=e,this.i++,gn++,St++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{xn()}}}});function Ot(e,t){return new Ce(e,t)}function Pa(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function Ia(e){for(var t=e.s;t!==void 0;t=t.n){var r=t.S.n;if(r!==void 0&&(t.r=r),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function Ra(e){for(var t=e.s,r=void 0;t!==void 0;){var n=t.p;t.i===-1?(t.S.U(t),n!==void 0&&(n.n=t.n),t.n!==void 0&&(t.n.p=n)):r=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=n}e.s=r}function Kt(e,t){Ce.call(this,void 0),this.x=e,this.s=void 0,this.g=gn-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Kt.prototype=new Ce;Kt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===gn))return!0;if(this.g=gn,this.f|=1,this.i>0&&!Pa(this))return this.f&=-2,!0;var e=ae;try{Ia(this),ae=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(r){this.v=r,this.f|=16,this.i++}return ae=e,Ra(this),this.f&=-2,!0};Kt.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}Ce.prototype.S.call(this,e)};Kt.prototype.U=function(e){if(this.t!==void 0&&(Ce.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};Kt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(Kt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=$a(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function ta(e,t){return new Kt(e,t)}function ja(e){var t=e.u;if(e.u=void 0,typeof t=="function"){St++;var r=ae;ae=void 0;try{t()}catch(n){throw e.f&=-2,e.f|=8,ho(e),n}finally{ae=r,xn()}}}function ho(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,ja(e)}function eu(e){if(ae!==this)throw new Error("Out-of-order effect");Ra(this),ae=e,this.f&=-2,8&this.f&&ho(this),xn()}function pr(e,t){this.x=e,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32,this.name=t==null?void 0:t.name}pr.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.u=t)}finally{e()}};pr.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,ja(this),Ia(this),St++;var e=ae;return ae=this,eu.bind(this,e)};pr.prototype.N=function(){2&this.f||(this.f|=2,this.o=kr,kr=this)};pr.prototype.d=function(){this.f|=8,1&this.f||ho(this)};pr.prototype.dispose=function(){this.d()};function qt(e,t){var r=new pr(e,t);try{r.c()}catch(o){throw r.d(),o}var n=r.d.bind(r);return n[Symbol.dispose]=n,n}var Fa,vo,eo,Ua=[];qt(function(){Fa=this.N})();function fr(e,t){K[e]=t.bind(null,K[e]||function(){})}function _n(e){eo&&eo(),eo=e&&e.S()}function Na(e){var t=this,r=e.data,n=ru(r);n.value=r;var o=ur(function(){for(var s=t,c=t.__v;c=c.__;)if(c.__c){c.__c.__$f|=4;break}var l=ta(function(){var m=n.value.value;return m===0?0:m===!0?"":m||""}),u=ta(function(){return!Array.isArray(l.value)&&!pa(l.value)}),p=qt(function(){if(this.N=Da,u.value){var m=l.value;s.__v&&s.__v.__e&&s.__v.__e.nodeType===3&&(s.__v.__e.data=m)}}),d=t.__$u.d;return t.__$u.d=function(){p(),d.call(this)},[u,l]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}Na.displayName="ReactiveTextNode";Object.defineProperties(Ce.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Na},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});fr("__b",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),typeof t.type=="string"){var r,n=t.props;for(var o in n)if(o!=="children"){var i=n[o];i instanceof Ce&&(r||(t.__np=r={}),r[o]=i,n[o]=i.peek())}}e(t)});fr("__r",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(t),t.type!==ft){_n();var r,n=t.__c;n&&(n.__$f&=-2,(r=n.__$u)===void 0&&(n.__$u=r=(function(o){var i;return qt(function(){i=this}),i.c=function(){n.__$f|=1,n.setState({})},i})())),vo=n,_n(r)}e(t)});fr("__e",function(e,t,r,n){typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0,e(t,r,n)});fr("diffed",function(e,t){typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0;var r;if(typeof t.type=="string"&&(r=t.__e)){var n=t.__np,o=t.props;if(n){var i=r.U;if(i)for(var a in i){var s=i[a];s!==void 0&&!(a in n)&&(s.d(),i[a]=void 0)}else i={},r.U=i;for(var c in n){var l=i[c],u=n[c];l===void 0?(l=tu(r,c,u,o),i[c]=l):l.o(u,o)}}}e(t)});function tu(e,t,r,n){var o=t in e&&e.ownerSVGElement===void 0,i=Ot(r);return{o:function(a,s){i.value=a,n=s},d:qt(function(){this.N=Da;var a=i.value.value;n[t]!==a&&(n[t]=a,o?e[t]=a:a?e.setAttribute(t,a):e.removeAttribute(t))})}}fr("unmount",function(e,t){if(typeof t.type=="string"){var r=t.__e;if(r){var n=r.U;if(n){r.U=void 0;for(var o in n){var i=n[o];i&&i.d()}}}}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)});fr("__h",function(e,t,r,n){(n<3||n===9)&&(t.__$f|=2),e(t,r,n)});at.prototype.shouldComponentUpdate=function(e,t){var r=this.__$u,n=r&&r.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){var i=2&this.__$f;if(!(n||i||4&this.__$f)||1&this.__$f)return!0}else if(!(n||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};function ru(e,t){return bn(function(){return Ot(e,t)})[0]}var nu=function(e){queueMicrotask(function(){queueMicrotask(e)})};function ou(){Ql(function(){for(var e;e=Ua.shift();)Fa.call(e)})}function Da(){Ua.push(this)===1&&(K.requestAnimationFrame||nu)(ou)}var ao=[0];for(let e=0;e<32;e++)ao.push(ao[e]|1<>>5]>>>e&1}set(e){this.data[e>>>5]|=1<<(e&31)}forEach(e){let t=this.size&31;for(let r=0;r{var r;return(r=t.tags)==null?void 0:r.length})&&(matchMedia("(max-width: 768px)").matches||Wa())}function Dt(){Qe.value=He(H({},Qe.value),{hideSearch:!Qe.value.hideSearch})}function Wa(){Qe.value=He(H({},Qe.value),{hideFilters:!Qe.value.hideFilters})}function dn(){return Qe.value.selectedItem}function so(e){Qe.value=He(H({},Qe.value),{selectedItem:e})}function su(){var e,t;return(t=(e=lr.value)==null?void 0:e.items)!=null?t:[]}function wn(){return typeof Se.value.input=="string"?Se.value.input:""}function Va(e){let t=za();e.length&&!t.length?Se.value=He(H({},Se.value),{page:void 0,input:e}):!e.length&&t.length?Se.value=He(H({},Se.value),{page:void 0,input:{type:"operator",data:{operator:"not",operands:[]}}}):Se.value=He(H({},Se.value),{page:void 0,input:e})}function cu(){typeof it.value.pagination.next<"u"&&(Se.value=He(H({},Se.value),{page:it.value.pagination.next}))}function lu(e){let t=Se.value.filter.input;if("type"in t&&t.type==="operator"){for(let r of t.data.operands)if("type"in r&&r.type==="value"&&typeof r.data.value=="string"&&r.data.value===e)return!0}return!1}function za(){let e=Se.value.filter.input,t=[];if("type"in e&&e.type==="operator")for(let r of e.data.operands)"type"in r&&r.type==="value"&&typeof r.data.value=="string"&&t.push(r.data.value);return t}function uu(e){let t=Se.value.filter.input,r=[];if("type"in t&&t.type==="operator")for(let n of t.data.operands)"type"in n&&n.type==="value"&&typeof n.data.value=="string"&&r.push(n.data.value);if(r.includes(e)){let n=r.indexOf(e);n>-1&&r.splice(n,1)}else r.push(e);Se.value=He(H({},Se.value),{page:void 0,filter:He(H({},Se.value.filter),{input:{type:"operator",data:{operator:"and",operands:r.map(n=>({type:"value",data:{field:"tags",value:n}}))}}})}),Va(wn())}function pu(){return it.value.items}function fu(){return it.value.total}function mu(){var e;for(let t of(e=it.value.aggregations)!=null?e:[])if(t.type==="term")return t.data.value;return[]}function sr(){return Qe.value.hideSearch}function du(){return Qe.value.hideFilters}function qa(){var e;return(e=Ka.value.highlight)!=null?e:!1}var Qe=Ot({hideSearch:!0,hideFilters:!0,selectedItem:0}),Ka=Ot({}),lr=Ot(),na=Ot(),Se=Ot({input:"",filter:{input:{type:"operator",data:{operator:"and",operands:[]}},aggregation:{input:[{type:"term",data:{field:"tags"}}]}}}),it=Ot({items:[],query:{select:{documents:new ra(0),terms:new ra(0)},values:[]},pagination:{total:0}});function hu(e,t,r){for(let n=0;tr&&t(0,o,r,r=i);continue;case 62:e.charCodeAt(r+1)===47?t(2,--o,r,r=i+1):hu(e,r,n)?t(3,o,r,r=i+1):t(1,o++,r,r=i+1)}i>r&&t(0,o,r,i)}function bu(e,t=0,r=e.length){let n=++t;e:for(let l=0;n{let i=[],a=[],{onElement:s,onText:c=gu}=typeof r=="function"?{onElement:r}:r,l=0,u=0;return e(t,(p,d,m,h)=>{if(p===0)i[l++]=c(t,m,h),a[u++]={value:null,depth:d};else if(p&1&&(a[u++]={value:bu(t,m,h),depth:d}),p&2)for(let v=0;u>=0;v++){let{value:x,depth:w}=a[--u];if(w>d)continue;let E=i.slice(l-=v,l+v);i[l++]=s(x,E),u++;break}},n,o),i.slice(0,l)}}function yu(e){return e.replace(/[&<>]/g,t=>{switch(t.charCodeAt(0)){case 38:return"&";case 60:return"<";case 62:return">"}})}function hn(e){return e.replace(/&(amp|[lg]t);/g,t=>{switch(t.charCodeAt(1)){case 97:return"&";case 108:return"<";case 103:return">"}})}function xu(e,t){return{start:e.start+t,end:e.end+t,value:e.value}}function wu(e,t,r){return e.slice(t,r)}function Eu(e){let{onHighlight:t,onText:r=wu}=typeof e=="function"?{onHighlight:e}:e;return(n,o,i=0,a=n.length)=>{var l;let s=[],c=(l=o==null?void 0:o.ranges)!=null?l:[];for(let u=0,p=i;ua)break;let m=c[u].end;if(mi&&s.push(r(n,i,d));let{value:h}=c[u];s.push(t(n,{start:d,end:i=m,value:h}))}return i{let o=n.data;switch(o.type){case 1:na.value=!0;break;case 3:typeof o.data.pagination.prev<"u"?it.value=He(H({},it.value),{pagination:o.data.pagination,items:[...it.value.items,...o.data.items]}):(it.value=o.data,so(0));break}},qt(()=>{lr.value&&r.postMessage({type:0,data:lr.value})}),qt(()=>{na.value&&r.postMessage({type:2,data:Se.value})})}var oa={container:"p",hidden:"m"};function ku(e){return z("div",{class:zt(oa.container,{[oa.hidden]:e.hidden}),onClick:()=>Dt()})}var ia={container:"r",disabled:"c"};function co(e){return z("button",{class:zt(ia.container,{[ia.disabled]:!e.onClick}),onClick:e.onClick,children:e.children})}var aa=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Au=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),sa=e=>{let t=Au(e);return t.charAt(0).toUpperCase()+t.slice(1)},Cu=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),Hu={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},$u=c=>{var l=c,{color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,children:o,iconNode:i,class:a=""}=l,s=gr(l,["color","size","strokeWidth","absoluteStrokeWidth","children","iconNode","class"]);return Wt("svg",H(He(H({},Hu),{width:String(t),height:t,stroke:e,"stroke-width":n?Number(r)*24/Number(t):r,class:["lucide",a].join(" ")}),s),[...i.map(([u,p])=>Wt(u,p)),...Cr(o)])},bo=(e,t)=>{let r=a=>{var s=a,{class:n="",children:o}=s,i=gr(s,["class","children"]);return Wt($u,He(H({},i),{iconNode:t,class:Cu(`lucide-${aa(sa(e))}`,`lucide-${aa(e)}`,n)}),o)};return r.displayName=sa(e),r},Pu=bo("corner-down-left",[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4",key:"6o5b7l"}],["path",{d:"m9 10-5 5 5 5",key:"1kshq7"}]]),Iu=bo("list-filter",[["path",{d:"M2 5h20",key:"1fs1ex"}],["path",{d:"M6 12h12",key:"8npq4p"}],["path",{d:"M9 19h6",key:"456am0"}]]),Ru=bo("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Gx=hl(vl(),1);function ju({threshold:e=0,root:t=null,rootMargin:r="0%",freezeOnceVisible:n=!1,initialIsIntersecting:o=!1,onChange:i}={}){var a;let[s,c]=bn(null),[l,u]=bn(()=>({isIntersecting:o,entry:void 0})),p=Vt();p.current=i;let d=((a=l.entry)==null?void 0:a.isIntersecting)&&n;mt(()=>{if(!s||!("IntersectionObserver"in window)||d)return;let v,x=new IntersectionObserver(w=>{let E=Array.isArray(x.thresholds)?x.thresholds:[x.thresholds];w.forEach(_=>{let de=_.isIntersecting&&E.some(be=>_.intersectionRatio>=be);u({isIntersecting:de,entry:_}),p.current&&p.current(de,_),de&&n&&v&&(v(),v=void 0)})},{threshold:e,root:t,rootMargin:r});return x.observe(s),()=>{x.disconnect()}},[s,JSON.stringify(e),t,r,d,n]);let m=Vt(null);mt(()=>{var v;!s&&(v=l.entry)!=null&&v.target&&!n&&!d&&m.current!==l.entry.target&&(m.current=l.entry.target,u({isIntersecting:o,entry:void 0}))},[s,l.entry,n,d,o]);let h=[c,!!l.isIntersecting,l.entry];return h.ref=h[0],h.isIntersecting=h[1],h.entry=h[2],h}var lt={container:"n",hidden:"l",content:"u",pop:"d",badge:"y",sidebar:"i",controls:"w",results:"k",loadmore:"z"};function Fu(e){let{isIntersecting:t,ref:r}=ju({threshold:0});mt(()=>{t&&cu()},[t]);let n=Vt(null);mt(()=>{n.current&&typeof Se.value.page>"u"&&n.current.scrollTo({top:0,behavior:"smooth"})},[Se.value]);let o=za();return z("div",{class:zt(lt.container,{[lt.hidden]:e.hidden}),children:[z("div",{class:lt.content,children:[z("div",{class:lt.controls,children:[z(co,{onClick:Dt,children:z(Ru,{})}),z(Nu,{focus:!e.hidden}),z(co,{onClick:Wa,children:[z(Iu,{}),o.length>0&&z("span",{class:lt.badge,children:o.length})]})]}),z("div",{class:lt.results,ref:n,children:[z(Du,{keyboard:!e.hidden}),z("div",{class:lt.loadmore,ref:r})]})]}),z("div",{class:zt(lt.sidebar,{[lt.hidden]:du()}),children:z(Uu,{})})]})}var Tt={container:"X",list:"j",heading:"F",title:"I",item:"o",active:"g",value:"R",count:"q"};function Uu(e){let t=mu();return t.sort((r,n)=>n.node.count-r.node.count),z("div",{class:Tt.container,children:[z("h3",{class:Tt.heading,children:"Filters"}),z("h4",{class:Tt.title,children:"Tags"}),z("ol",{class:Tt.list,children:t.map(r=>z("li",{class:zt(Tt.item,{[Tt.active]:lu(r.node.value)}),onClick:()=>uu(r.node.value),children:[z("span",{class:Tt.value,children:r.node.value}),z("span",{class:Tt.count,children:r.node.count})]}))})]})}var ca={container:"f"};function Nu(e){let t=Vt(null);return mt(()=>{var r,n;e.focus?(r=t.current)==null||r.focus():(n=t.current)==null||n.blur()},[e.focus]),z("div",{class:ca.container,children:z("input",{ref:t,type:"text",class:ca.content,value:hn(wn()),onInput:r=>Va(yu(r.currentTarget.value)),autocapitalize:"off",autocomplete:"off",autocorrect:"off",placeholder:"Search",spellcheck:!1,role:"combobox"})})}var ut={container:"b",heading:"A",item:"a",active:"h",wrapper:"B",actions:"s",title:"x",path:"t"};function Ga(){let[e,t]=bn(!1);return mt(()=>{let r=()=>t(!0),n=()=>t(!1);return document.addEventListener("compositionstart",r),document.addEventListener("compositionend",n),()=>{document.removeEventListener("compositionstart",r),document.removeEventListener("compositionend",n)}},[]),e}function Du(e){var s;let t=su(),r=pu(),n=dn(),o=Vt([]),i=Ga();mt(()=>{let c=o.current[n];c&&c.scrollIntoView({block:"center",behavior:"smooth"})},[n]),Aa(e.keyboard,c=>{if(i)return;let l=dn();c.key==="ArrowDown"?(c.preventDefault(),so(Math.min(l+1,r.length-1))):c.key==="ArrowUp"&&(c.preventDefault(),so(Math.max(l-1,0)))},[e.keyboard,i]);let a=(s=fu())!=null?s:0;return z(ft,{children:[r.length>0&&z("h3",{class:ut.heading,children:[z("span",{class:ut.bubble,children:new Intl.NumberFormat("en-US").format(a)})," ","results"]}),z("ol",{class:ut.container,children:r.map((c,l)=>{var m;let u=Ba(t[c.id].title,c.matches.find(({field:h})=>h==="title")),p=Mu((m=t[c.id].path)!=null?m:[],c.matches.find(({field:h})=>h==="path")),d=t[c.id].location;if(qa()){let h=encodeURIComponent(wn()),[v,x]=d.split("#",2);d=`${v}?h=${h.replace(/%20/g,"+")}`,typeof x<"u"&&(d+=`#${x}`)}return z("li",{children:z("a",{ref:h=>{o.current[l]=h},href:d,onClick:()=>Dt(),class:zt(ut.item,{[ut.active]:l===dn()}),children:[z("div",{class:ut.wrapper,children:[z("h2",{class:ut.title,children:u}),z("menu",{class:ut.path,children:p.map(h=>z("li",{children:h}))})]}),z("nav",{class:ut.actions,children:z(co,{children:z(Pu,{})})})]})})})})]})}var Wu={container:"e"};function Vu(e){let t=Ga();return Aa(!0,r=>{var n,o,i,a,s;if(!t)if((r.metaKey||r.ctrlKey)&&r.key==="k")r.preventDefault(),Dt();else if((r.metaKey||r.ctrlKey)&&r.key==="j")document.body.classList.toggle("dark");else if(r.key==="Enter"&&!sr()){r.preventDefault();let c=dn(),l=(o=(n=it.value)==null?void 0:n.items[c])==null?void 0:o.id;if((a=(i=lr.value)==null?void 0:i.items[l])!=null&&a.location){Dt();let u=(s=lr.value)==null?void 0:s.items[l].location;if(qa()){let p=encodeURIComponent(wn()),[d,m]=u.split("#",2);u=`${d}?h=${p.replace(/%20/g,"+")}`,typeof m<"u"&&(u+=`#${m}`)}window.location.href=u}}else r.key==="Escape"&&!sr()&&(r.preventDefault(),Dt())},[t]),z("div",{class:Wu.container,children:[z(ku,{hidden:sr()}),z(Fu,{hidden:sr()})]})}function Ja(e,t){au(e),El(z(Vu,{}),t)}function go(){Dt()}function zu(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function qu(){return R(b(window,"compositionstart").pipe(f(()=>!0)),b(window,"compositionend").pipe(f(()=>!1))).pipe(J(!1))}function Xa(){let e=b(window,"keydown").pipe(f(t=>({mode:sr()?"global":"search",type:t.key,meta:t.ctrlKey||t.metaKey,claim(){t.preventDefault(),t.stopPropagation()}})),L(({mode:t,type:r})=>{if(t==="global"){let n=xt();if(typeof n!="undefined")return!zu(n,r)}return!0}),xe());return qu().pipe(g(t=>t?y:e))}function Ye(){return new URL(location.href)}function dt(e,t=!1){if(X("navigation.instant")&&!t){let r=A("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function Za(){return new I}function Qa(){return location.hash.slice(1)}function es(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function _o(e){return R(b(window,"hashchange"),e).pipe(f(Qa),J(Qa()),L(t=>t.length>0),se(1))}function ts(e){return _o(e).pipe(f(t=>Le(`[id="${t}"]`)),L(t=>typeof t!="undefined"))}function Ir(e){let t=matchMedia(e);return an(r=>t.addListener(()=>r(t.matches))).pipe(J(t.matches))}function rs(){let e=matchMedia("print");return R(b(window,"beforeprint").pipe(f(()=>!0)),b(window,"afterprint").pipe(f(()=>!1))).pipe(J(e.matches))}function yo(e,t){return e.pipe(g(r=>r?t():y))}function xo(e,t){return new U(r=>{let n=new XMLHttpRequest;return n.open("GET",`${e}`),n.responseType="blob",n.addEventListener("load",()=>{n.status>=200&&n.status<300?(r.next(n.response),r.complete()):r.error(new Error(n.statusText))}),n.addEventListener("error",()=>{r.error(new Error("Network error"))}),n.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(n.addEventListener("progress",o=>{var i;if(o.lengthComputable)t.progress$.next(o.loaded/o.total*100);else{let a=(i=n.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(o.loaded/+a*100)}}),t.progress$.next(5)),n.send(),()=>n.abort()})}function et(e,t){return xo(e,t).pipe(g(r=>r.text()),f(r=>JSON.parse(r)),se(1))}function En(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/html")),se(1))}function ns(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/xml")),se(1))}var wo={drawer:G("[data-md-toggle=drawer]"),search:G("[data-md-toggle=search]")};function Eo(e,t){wo[e].checked!==t&&wo[e].click()}function Tn(e){let t=wo[e];return b(t,"change").pipe(f(()=>t.checked),J(t.checked))}function os(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function is(){return R(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(f(os),J(os()))}function as(){return{width:innerWidth,height:innerHeight}}function ss(){return b(window,"resize",{passive:!0}).pipe(f(as),J(as()))}function cs(){return re([is(),ss()]).pipe(f(([e,t])=>({offset:e,size:t})),se(1))}function Sn(e,{viewport$:t,header$:r}){let n=t.pipe(fe("size")),o=re([n,r]).pipe(f(()=>wt(e)));return re([r,t,o]).pipe(f(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}var Ku=G("#__config"),mr=JSON.parse(Ku.textContent);mr.base=`${new URL(mr.base,Ye())}`;function Ue(){return mr}function X(e){return mr.features.includes(e)}function Bt(e,t){return typeof t!="undefined"?mr.translations[e].replace("#",t.toString()):mr.translations[e]}function ht(e,t=document){return G(`[data-md-component=${e}]`,t)}function Ee(e,t=document){return P(`[data-md-component=${e}]`,t)}function Bu(e){let t=G(".md-typeset > :first-child",e);return b(t,"click",{once:!0}).pipe(f(()=>G(".md-typeset",e)),f(r=>({hash:__md_hash(r.innerHTML)})))}function ls(e){if(!X("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=G(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return j(()=>{let t=new I;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Bu(e).pipe($(r=>t.next(r)),V(()=>t.complete()),f(r=>H({ref:e},r)))})}function Yu(e,{target$:t}){return t.pipe(f(r=>({hidden:r!==e})))}function us(e,t){let r=new I;return r.subscribe(({hidden:n})=>{e.hidden=n}),Yu(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))}function To(e,t){return t==="inline"?A("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})):A("div",{class:"md-tooltip",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"}))}function On(...e){return A("div",{class:"md-tooltip2",role:"dialog"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function ps(...e){return A("div",{class:"md-tooltip2",role:"tooltip"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function fs(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("a",{href:r,class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}else return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("span",{class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}function ms(e){return A("button",{class:"md-code__button",title:Bt("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function ds(){return A("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function hs(){return A("nav",{class:"md-code__nav"})}var Xu=_r(So());function bs(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Li(r):r)))}function Oo(e){let t=`tabbed-control tabbed-control--${e}`;return A("div",{class:t,hidden:!0},A("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function gs(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function Zu(e){var n;let t=Ue(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:`${r}`,class:"md-version__link"},e.title,((n=t.version)==null?void 0:n.alias)&&e.aliases.length>0&&A("span",{class:"md-version__alias"},e.aliases[0])))}function _s(e,t){var n;let r=Ue();return e=e.filter(o=>{var i;return!((i=o.properties)!=null&&i.hidden)}),A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":Bt("select.version")},t.title,((n=r.version)==null?void 0:n.alias)&&t.aliases.length>0&&A("span",{class:"md-version__alias"},t.aliases[0])),A("ul",{class:"md-version__list"},e.map(Zu)))}var Qu=0;function ep(e,t=250){let r=re([ir(e),Ft(e,t)]).pipe(f(([o,i])=>o||i),ie()),n=j(()=>Ai(e)).pipe(oe(Ut),Lr(1),Ze(r),f(()=>Ci(e)));return r.pipe(Sr(o=>o),g(()=>re([r,n])),f(([o,i])=>({active:o,offset:i})),xe())}function Rr(e,t,r=250){let{content$:n,viewport$:o}=t,i=`__tooltip2_${Qu++}`;return j(()=>{let a=new I,s=new Un(!1);a.pipe(he(),ye(!1)).subscribe(s);let c=s.pipe(Tr(u=>Ve(+!u*250,Wn)),ie(),g(u=>u?n:y),$(u=>u.id=i),xe());re([a.pipe(f(({active:u})=>u)),c.pipe(g(u=>Ft(u,250)),J(!1))]).pipe(f(u=>u.some(p=>p))).subscribe(s);let l=s.pipe(L(u=>u),pe(c,o),f(([u,p,{size:d}])=>{let m=e.getBoundingClientRect(),h=m.width/2;if(p.role==="tooltip")return{x:h,y:8+m.height};if(m.y>=d.height/2){let{height:v}=Ae(p);return{x:h,y:-16-v}}else return{x:h,y:16+m.height}}));return re([c,a,l]).subscribe(([u,{offset:p},d])=>{u.style.setProperty("--md-tooltip-host-x",`${p.x}px`),u.style.setProperty("--md-tooltip-host-y",`${p.y}px`),u.style.setProperty("--md-tooltip-x",`${d.x}px`),u.style.setProperty("--md-tooltip-y",`${d.y}px`),u.classList.toggle("md-tooltip2--top",d.y<0),u.classList.toggle("md-tooltip2--bottom",d.y>=0)}),s.pipe(L(u=>u),pe(c,(u,p)=>p),L(u=>u.role==="tooltip")).subscribe(u=>{let p=Ae(G(":scope > *",u));u.style.setProperty("--md-tooltip-width",`${p.width}px`),u.style.setProperty("--md-tooltip-tail","0px")}),s.pipe(ie(),Ie(je),pe(c)).subscribe(([u,p])=>{p.classList.toggle("md-tooltip2--active",u)}),re([s.pipe(L(u=>u)),c]).subscribe(([u,p])=>{p.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),s.pipe(L(u=>!u)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ep(e,r).pipe($(u=>a.next(u)),V(()=>a.complete()),f(u=>H({ref:e},u)))})}function Ge(e,{viewport$:t},r=document.body){return Rr(e,{content$:new U(n=>{let o=e.title,i=ps(o);return n.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",o)}}),viewport$:t},0)}function tp(e,t){let r=j(()=>re([Hi(e),Ut(t)])).pipe(f(([{x:n,y:o},i])=>{let{width:a,height:s}=Ae(e);return{x:n-i.x+a/2,y:o-i.y+s/2}}));return ir(e).pipe(g(n=>r.pipe(f(o=>({active:n,offset:o})),Me(+!n||1/0))))}function ys(e,t,{target$:r}){let[n,o]=Array.from(e.children);return j(()=>{let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),Et(e).pipe(Q(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),R(i.pipe(L(({active:s})=>s)),i.pipe(Be(250),L(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Xe(16,je)).subscribe(({active:s})=>{n.classList.toggle("md-tooltip--active",s)}),i.pipe(Lr(125,je),L(()=>!!e.offsetParent),f(()=>e.offsetParent.getBoundingClientRect()),f(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),b(o,"click").pipe(Q(a),L(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),b(o,"mousedown").pipe(Q(a),pe(i)).subscribe(([s,{active:c}])=>{var l;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(l=xt())==null||l.blur()}}),r.pipe(Q(a),L(s=>s===n),It(125)).subscribe(()=>e.focus()),tp(e,t).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function rp(e){let t=Ue();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate){let n=e.closest("[class|=language]");if(n)for(let o of Array.from(n.classList)){if(!o.startsWith("language-"))continue;let[,i]=o.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return P(r.join(", "),e)}function np(e){let t=[];for(let r of rp(e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let l=i.splitText(a.index);i=l.splitText(s.length),t.push(l)}else{i.textContent=s,t.push(i);break}}}}return t}function xs(e,t){t.append(...Array.from(e.childNodes))}function Ln(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let s of np(t)){let[,c]=s.textContent.match(/\((\d+)\)/);Le(`:scope > li:nth-child(${c})`,e)&&(a.set(c,fs(c,i)),s.replaceWith(a.get(c)))}return a.size===0?y:j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=[];for(let[u,p]of a)l.push([G(".md-typeset",p),G(`:scope > li:nth-child(${u})`,e)]);return n.pipe(Q(c)).subscribe(u=>{e.hidden=!u,e.classList.toggle("md-annotation-list",u);for(let[p,d]of l)u?xs(p,d):xs(d,p)}),R(...[...a].map(([,u])=>ys(u,t,{target$:r}))).pipe(V(()=>s.complete()),xe())})}function ws(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return ws(t)}}function Es(e,t){return j(()=>{let r=ws(e);return typeof r!="undefined"?Ln(r,e,t):y})}var Ss=_r(Mo());var op=0,Ts=R(b(window,"keydown").pipe(f(()=>!0)),R(b(window,"keyup"),b(window,"contextmenu")).pipe(f(()=>!1))).pipe(J(!1),se(1));function Os(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Os(t)}}function ip(e){return Re(e).pipe(f(({width:t})=>({scrollable:Mr(e).width>t})),fe("scrollable"))}function Ls(e,t){let{matches:r}=matchMedia("(hover)"),n=j(()=>{let o=new I,i=o.pipe(Gn(1));o.subscribe(({scrollable:m})=>{m&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[],s=e.closest("pre"),c=s.closest("[id]"),l=c?c.id:op++;s.id=`__code_${l}`;let u=[],p=e.closest(".highlight");if(p instanceof HTMLElement){let m=Os(p);if(typeof m!="undefined"&&(p.classList.contains("annotate")||X("content.code.annotate"))){let h=Ln(m,e,t);u.push(Re(p).pipe(Q(i),f(({width:v,height:x})=>v&&x),ie(),g(v=>v?h:y)))}}let d=P(":scope > span[id]",e);if(d.length&&(e.classList.add("md-code__content"),e.closest(".select")||X("content.code.select")&&!e.closest(".no-select"))){let m=+d[0].id.split("-").pop(),h=ds();a.push(h),X("content.tooltips")&&u.push(Ge(h,{viewport$}));let v=b(h,"click").pipe(Or(M=>!M,!1),$(()=>h.blur()),xe());v.subscribe(M=>{h.classList.toggle("md-code__button--active",M)});let x=me(d).pipe(oe(M=>Ft(M).pipe(f(O=>[M,O]))));v.pipe(g(M=>M?x:y)).subscribe(([M,O])=>{let N=Le(".hll.select",M);if(N&&!O)N.replaceWith(...Array.from(N.childNodes));else if(!N&&O){let ee=document.createElement("span");ee.className="hll select",ee.append(...Array.from(M.childNodes).slice(1)),M.append(ee)}});let w=me(d).pipe(oe(M=>b(M,"mousedown").pipe($(O=>O.preventDefault()),f(()=>M)))),E=v.pipe(g(M=>M?w:y),pe(Ts),f(([M,O])=>{var ee;let N=d.indexOf(M)+m;if(O===!1)return[N,N];{let le=P(".hll",e).map(ce=>d.indexOf(ce.parentElement)+m);return(ee=window.getSelection())==null||ee.removeAllRanges(),[Math.min(N,...le),Math.max(N,...le)]}})),_=_o(y).pipe(L(M=>M.startsWith(`__codelineno-${l}-`)));_.subscribe(M=>{let[,,O]=M.split("-"),N=O.split(":").map(le=>+le-m+1);N.length===1&&N.push(N[0]);for(let le of P(".hll:not(.select)",e))le.replaceWith(...Array.from(le.childNodes));let ee=d.slice(N[0]-1,N[1]);for(let le of ee){let ce=document.createElement("span");ce.className="hll",ce.append(...Array.from(le.childNodes).slice(1)),le.append(ce)}}),_.pipe(Me(1),Ie(ge)).subscribe(M=>{if(M.includes(":")){let O=document.getElementById(M.split(":")[0]);O&&setTimeout(()=>{let N=O,ee=-64;for(;N!==document.body;)ee+=N.offsetTop,N=N.offsetParent;window.scrollTo({top:ee})},1)}});let be=me(P('a[href^="#__codelineno"]',p)).pipe(oe(M=>b(M,"click").pipe($(O=>O.preventDefault()),f(()=>M)))).pipe(Q(i),pe(Ts),f(([M,O])=>{let ee=+G(`[id="${M.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(O===!1)return[ee,ee];{let le=P(".hll",e).map(ce=>+ce.parentElement.id.split("-").pop());return[Math.min(ee,...le),Math.max(ee,...le)]}}));R(E,be).subscribe(M=>{let O=`#__codelineno-${l}-`;M[0]===M[1]?O+=M[0]:O+=`${M[0]}:${M[1]}`,history.replaceState({},"",O),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+O,oldURL:window.location.href}))})}if(Ss.default.isSupported()&&(e.closest(".copy")||X("content.code.copy")&&!e.closest(".no-copy"))){let m=ms(s.id);a.push(m),X("content.tooltips")&&u.push(Ge(m,{viewport$}))}if(a.length){let m=hs();m.append(...a),s.insertBefore(m,e)}return ip(e).pipe($(m=>o.next(m)),V(()=>o.complete()),f(m=>H({ref:e},m)),Rt(R(...u).pipe(Q(i))))});return X("content.lazy")?Et(e).pipe(L(o=>o),Me(1),g(()=>n)):n}function ap(e,{target$:t,print$:r}){let n=!0;return R(t.pipe(f(o=>o.closest("details:not([open])")),L(o=>e===o),f(()=>({action:"open",reveal:!0}))),r.pipe(L(o=>o||!n),$(()=>n=e.open),f(o=>({action:o?"open":"close"}))))}function Ms(e,t){return j(()=>{let r=new I;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),ap(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}var ks=0,As=new Map;function sp(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],n=e.nextElementSibling;for(;n&&!(n instanceof HTMLHeadingElement);)r.push(n.cloneNode(!0)),n=n.nextElementSibling;return r}function cp(e,t){for(let r of P("[href], [src]",e))for(let n of["href","src"]){let o=r.getAttribute(n);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){r[n]=new URL(r.getAttribute(n),t).toString();break}}for(let r of P("[name^=__], [for]",e))for(let n of["id","for","name"]){let o=r.getAttribute(n);o&&r.setAttribute(n,`${o}$preview_${ks}`)}return ks++,Y(e)}function lp(e){let t=As.get(e.toString());return t?Y(t):En(e).pipe(g(r=>cp(r,e)),f(r=>(As.set(e.toString(),r),r)))}function Cs(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(X("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let n=re([ir(e),Ft(e).pipe(ke(1))]).pipe(f(([i,a])=>i||a),ie(),L(i=>i));return $t([r,n]).pipe(g(([i])=>{let a=new URL(e.href);return a.search=a.hash="",i.has(`${a}`)?Y(a):y}),g(i=>lp(i)),g(i=>{let a=e.hash?`article [id="${decodeURIComponent(e.hash.slice(1))}"]`:"article h1",s=Le(a,i);return typeof s=="undefined"?y:Y(sp(s))})).pipe(g(i=>{let a=new U(s=>{let c=On(...i);return s.next(c),document.body.append(c),()=>c.remove()});return Rr(e,H({content$:a},t))}))}var Hs=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var ko,pp=0;function fp(){return typeof mermaid=="undefined"||mermaid instanceof Element?ar("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):Y(void 0)}function $s(e){return e.classList.remove("mermaid"),ko||(ko=fp().pipe($(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Hs,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),f(()=>{}),se(1))),ko.subscribe(()=>Uo(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${pp++}`,r=A("div",{class:"mermaid"}),n=e.textContent,{svg:o,fn:i}=yield mermaid.render(t,n),a=r.attachShadow({mode:"closed"});a.innerHTML=o,e.replaceWith(r),i==null||i(a)})),ko.pipe(f(()=>({ref:e})))}var Ps=A("table");function Is(e){return e.replaceWith(Ps),Ps.replaceWith(gs(e)),Y({ref:e})}function mp(e){let t=e.find(r=>r.checked)||e[0];return R(...e.map(r=>b(r,"change").pipe(f(()=>G(`label[for="${r.id}"]`))))).pipe(J(G(`label[for="${t.id}"]`)),f(r=>({active:r})))}function Rs(e,{viewport$:t,target$:r}){let n=G(".tabbed-labels",e),o=P(":scope > input",e),i=Oo("prev");e.append(i);let a=Oo("next");return e.append(a),j(()=>{let s=new I,c=s.pipe(he(),ye(!0));re([s,Re(e),Et(e)]).pipe(Q(c),Xe(1,je)).subscribe({next([{active:l},u]){let p=wt(l),{width:d}=Ae(l);e.style.setProperty("--md-indicator-x",`${p.x}px`),e.style.setProperty("--md-indicator-width",`${d}px`);let m=ln(n);(p.xm.x+u.width)&&n.scrollTo({left:Math.max(0,p.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),re([Ut(n),Re(n)]).pipe(Q(c)).subscribe(([l,u])=>{let p=Mr(n);i.hidden=l.x<16,a.hidden=l.x>p.width-u.width-16}),R(b(i,"click").pipe(f(()=>-1)),b(a,"click").pipe(f(()=>1))).pipe(Q(c)).subscribe(l=>{let{width:u}=Ae(n);n.scrollBy({left:u*l,behavior:"smooth"})}),r.pipe(Q(c),L(l=>o.includes(l))).subscribe(l=>l.click()),n.classList.add("tabbed-labels--linked");for(let l of o){let u=G(`label[for="${l.id}"]`);u.replaceChildren(A("a",{href:`#${u.htmlFor}`,tabIndex:-1},...Array.from(u.childNodes))),b(u.firstElementChild,"click").pipe(Q(c),L(p=>!(p.metaKey||p.ctrlKey)),$(p=>{p.preventDefault(),p.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${u.htmlFor}`),u.click()})}return X("content.tabs.link")&&s.pipe(ke(1),pe(t)).subscribe(([{active:l},{offset:u}])=>{let p=l.innerText.trim();if(l.hasAttribute("data-md-switching"))l.removeAttribute("data-md-switching");else{let d=e.offsetTop-u.y;for(let h of P("[data-tabs]"))for(let v of P(":scope > input",h)){let x=G(`label[for="${v.id}"]`);if(x!==l&&x.innerText.trim()===p){x.setAttribute("data-md-switching",""),v.click();break}}window.scrollTo({top:e.offsetTop-d});let m=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([p,...m])])}}),s.pipe(Q(c)).subscribe(()=>{for(let l of P("audio, video",e))l.offsetWidth&&l.autoplay?l.play().catch(()=>{}):l.pause()}),mp(o).pipe($(l=>s.next(l)),V(()=>s.complete()),f(l=>H({ref:e},l)))}).pipe(Ht(ge))}function js(e,t){let{viewport$:r,target$:n,print$:o}=t;return R(...P(".annotate:not(.highlight)",e).map(i=>Es(i,{target$:n,print$:o})),...P("pre:not(.mermaid) > code",e).map(i=>Ls(i,{target$:n,print$:o})),...P("a",e).map(i=>Cs(i,t)),...P("pre.mermaid",e).map(i=>$s(i)),...P("table:not([class])",e).map(i=>Is(i)),...P("details",e).map(i=>Ms(i,{target$:n,print$:o})),...P("[data-tabs]",e).map(i=>Rs(i,{viewport$:r,target$:n})),...P("[title]:not([data-preview])",e).filter(()=>X("content.tooltips")).map(i=>Ge(i,{viewport$:r})),...P(".footnote-ref",e).filter(()=>X("content.footnote.tooltips")).map(i=>Rr(i,{content$:new U(a=>{let s=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(s).cloneNode(!0).children),l=On(...c);return a.next(l),document.body.append(l),()=>l.remove()}),viewport$:r})))}function dp(e,{alert$:t}){return t.pipe(g(r=>R(Y(!0),Y(!1).pipe(It(2e3))).pipe(f(n=>({message:r,active:n})))))}function Fs(e,t){let r=G(".md-typeset",e);return j(()=>{let n=new I;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),dp(e,t).pipe($(o=>n.next(o)),V(()=>n.complete()),f(o=>H({ref:e},o)))})}function hp({viewport$:e}){if(!X("header.autohide"))return Y(!1);let t=e.pipe(f(({offset:{y:o}})=>o),Pt(2,1),f(([o,i])=>[oMath.abs(i-o.y)>100),f(([,[o]])=>o),ie()),n=Tn("search");return re([e,n]).pipe(f(([{offset:o},i])=>o.y>400&&!i),ie(),g(o=>o?r:Y(!1)),J(!1))}function Us(e,t){return j(()=>re([Re(e),hp(t)])).pipe(f(([{height:r},n])=>({height:r,hidden:n})),ie((r,n)=>r.height===n.height&&r.hidden===n.hidden),se(1))}function Ns(e,{viewport$:t,header$:r,main$:n}){return j(()=>{let o=new I,i=o.pipe(he(),ye(!0));o.pipe(fe("active"),Ze(r)).subscribe(([{active:s},{hidden:c}])=>{e.classList.toggle("md-header--shadow",s&&!c),e.hidden=c});let a=me(P("[title]",e)).pipe(L(()=>X("content.tooltips")),oe(s=>Ge(s,{viewport$:t})));return n.subscribe(o),r.pipe(Q(i),f(s=>H({ref:e},s)),Rt(a.pipe(Q(i))))})}function vp(e,{viewport$:t,header$:r}){return Sn(e,{viewport$:t,header$:r}).pipe(f(({offset:{y:n}})=>{let{height:o}=Ae(e);return{active:o>0&&n>=o}}),fe("active"))}function Ds(e,t){return j(()=>{let r=new I;r.subscribe({next({active:o}){e.classList.toggle("md-header__title--active",o)},complete(){e.classList.remove("md-header__title--active")}});let n=Le(".md-content h1");return typeof n=="undefined"?y:vp(n,t).pipe($(o=>r.next(o)),V(()=>r.complete()),f(o=>H({ref:e},o)))})}function Ws(e,{viewport$:t,header$:r}){let n=r.pipe(f(({height:i})=>i),ie()),o=n.pipe(g(()=>Re(e).pipe(f(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),fe("bottom"))));return re([n,o,t]).pipe(f(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),ie((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function bp(e){let t=__md_get("__palette")||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return Y(...e).pipe(oe(n=>b(n,"change").pipe(f(()=>n))),J(e[r]),f(n=>({index:e.indexOf(n),color:{media:n.getAttribute("data-md-color-media"),scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),se(1))}function Vs(e){let t=P("input",e),r=A("meta",{name:"theme-color"});document.head.appendChild(r);let n=A("meta",{name:"color-scheme"});document.head.appendChild(n);let o=Ir("(prefers-color-scheme: light)");return j(()=>{let i=new I;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),pe(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(f(()=>{let a=ht("header"),s=window.getComputedStyle(a);return n.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Ie(ge)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),bp(t).pipe(Q(o.pipe(ke(1))),jt(),$(a=>i.next(a)),V(()=>i.complete()),f(a=>H({ref:e},a)))})}function zs(e,{progress$:t}){return j(()=>{let r=new I;return r.subscribe(({value:n})=>{e.style.setProperty("--md-progress-value",`${n}`)}),t.pipe($(n=>r.next({value:n})),V(()=>r.complete()),f(n=>({ref:e,value:n})))})}var qs='.v u{text-decoration:underline!important;text-decoration-style:wavy!important;text-decoration-thickness:1px!important}.p{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-backdrop)/var(--alpha-lighter));cursor:pointer;height:100%;pointer-events:auto;position:absolute;transition:opacity .25s;width:100%}.p.m{opacity:0;pointer-events:none;transition:opacity .35s}.r{align-items:center;background-color:initial;border:none;border-radius:var(--space-2);cursor:pointer;display:flex;flex-shrink:0;font-family:var(--font-family);height:36px;justify-content:center;outline:none;padding:0;position:relative;transition:background-color .25s,color .25s;width:36px;z-index:1}.r svg{stroke:rgb(var(--color-foreground));height:18px;opacity:.5;width:18px}.r:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.r:hover:before{opacity:1;transform:scale(1)}.r.c{cursor:auto}.r.c:before{display:none}.n{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background)/var(--alpha-light));border-radius:var(--space-3);box-shadow:0 0 60px #0000000d;display:flex;height:480px;overflow:hidden;pointer-events:auto;position:absolute;transition:transform .25s cubic-bezier(.16,1,.3,1),opacity .25s;width:640px}.n.l{opacity:0;pointer-events:none;transform:scale(1.1);transition:transform .25s .15s,opacity .15s}@media (max-width:680px){.n{border-radius:0;height:100%;width:100%}}.u{display:flex;flex-basis:min-content;flex-direction:column;flex-grow:1;flex-shrink:0}@keyframes d{0%{transform:scale(0)}50%{transform:scale(1.2)}to{transform:scale(1)}}.y{animation:d .25s ease-in-out;background:var(--color-highlight);border-radius:100%;color:#fff;font-size:8px;font-weight:700;height:12px;padding-top:1px;position:absolute;right:4px;top:4px;width:12px}.i{background-color:rgb(var(--color-background-subtle)/var(--alpha-lighter));flex-shrink:0;overflow:scroll;position:relative;transition:width .35s cubic-bezier(.16,1,.3,1),opacity .25s;width:200px}.i>*{transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.i.l{opacity:0;width:0}.i.l>*{transform:translate(-48px)}@media (max-width:680px){.i{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background-subtle)/var(--alpha-light));box-shadow:0 0 60px #00000026;height:100%;position:absolute;right:0;top:0}}.w{border-bottom:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));display:flex;gap:var(--space-1);padding:var(--space-2)}.k{-webkit-overflow-scrolling:touch;overflow:auto;overscroll-behavior:contain}.z{padding:8px 10px}.X{color:rgb(var(--color-foreground)/var(--alpha-light));padding:var(--space-2);position:absolute;width:200px}.X,.j{display:flex;flex-direction:column}.j{gap:2px;list-style:none;padding:0}.F,.j{margin:0}.F{font-size:16px;font-weight:400}.F,.I{padding:8px}.I{font-size:14px;margin:4px 0 0;opacity:.5}.I,.o{font-size:12px}.o{cursor:pointer;display:flex;padding:4px 8px;position:relative}.o:before{background-color:var(--color-highlight-transparent);border-radius:var(--space-1);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.o.g:before,.o:hover:before{opacity:1;transform:scale(1)}.o.g,.o:hover{color:var(--color-highlight)}.R{flex-grow:1}.R,.q{position:relative}.q{font-weight:700}.f{flex-grow:1}.f input{background:#0000;border:none;color:rgb(var(--color-foreground));font-family:var(--font-family);font-size:16px;height:100%;letter-spacing:-.25px;outline:none;width:100%}.b{color:rgb(var(--color-foreground)/var(--alpha-light));display:flex;flex-direction:column;gap:2px;line-height:1.3;list-style:none;margin:var(--space-2);margin-top:0;padding:0}.A,.b li{margin:0}.A{color:rgb(var(--color-foreground)/var(--alpha-lighter));font-size:12px;margin-top:var(--space-2);padding:0 18px}.a{border-radius:var(--space-2);color:inherit;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;padding:8px 10px;position:relative;text-decoration:none}.a:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";display:block;inset:0;opacity:0;position:absolute;transform:scale(.9);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.a.h:before,.a:hover:before{opacity:1;transform:scale(1)}}.a mark{background:#0000;color:var(--color-highlight)}.a u{background-color:var(--color-highlight-transparent);border-radius:2px;box-shadow:0 0 0 1px var(--color-highlight-transparent);text-decoration:none}.B{flex-grow:1}.s{margin-right:-8px;opacity:0;position:relative;transform:translate(-2px);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.h>.s,:hover>.s{opacity:1;transform:none}}.x{font-size:14px;margin:0;position:relative}.x code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);font-size:13px;padding:2px 4px}.t{color:rgb(var(--color-foreground)/var(--alpha-lighter));display:inline-flex;flex-wrap:wrap;font-size:12px;gap:var(--space-1);list-style:none;margin:0;padding:0;position:relative}.t li{white-space:nowrap}.t li:after{content:"/";display:inline;margin-left:var(--space-1)}.t li:last-child:after{content:"";display:none}.e{--space-1:4px;--space-2:calc(var(--space-1)*2);--space-3:calc(var(--space-2)*2);--space-4:calc(var(--space-3)*2);--space-5:calc(var(--space-4)*2);--alpha-light:.7;--alpha-lighter:.54;--alpha-lightest:.1;--color-highlight:var(--md-accent-fg-color,#526cfe);--color-highlight-transparent:var(--md-accent-fg-color--transparent,#526cfe1a);--border-radius-1:var(--space-1);--border-radius-2:var(--space-2);--border-radius-3:calc(var(--space-1) + var(--space-2));--font-family:var(--md-text-font-family,Inter,Roboto Flex,system-ui,sans-serif);--font-size:16px;--line-height:1.5;--letter-spacing:-.5px;-webkit-font-smoothing:antialiased;align-items:center;display:flex;font-family:var(--font-family);font-size:var(--font-size);height:100vh;justify-content:center;letter-spacing:var(--letter-spacing);line-height:var(--line-height);pointer-events:none;position:absolute;width:100vw}@media (pointer:coarse){.e{height:-webkit-fill-available}}.e *,.e :after,.e :before{box-sizing:border-box}';function Ks(e,{index$:t}){let r=Ue(),n=document.createElement("div");document.body.appendChild(n),n.style.position="fixed",n.style.height="100%",n.style.top="0",n.style.zIndex="4";let o=n.attachShadow({mode:"open"});o.appendChild(A("style",{},qs.toString()));try{Ya(r.search,{highlight:r.features.includes("search.highlight")}),me(t).subscribe(i=>{for(let a of i.items)a.location=new URL(a.location,r.base).toString();Ja(i,o)}),b(e,"click").subscribe(()=>{go()}),Tn("search").pipe(ke(1)).subscribe(()=>go())}catch(i){e.hidden=!0;let a=G("label[for=__search]");a.hidden=!0}return Ke}var Bs=_r(So());function Ys(e,{index$:t,location$:r}){return re([t,r.pipe(J(Ye()),L(n=>!!n.searchParams.get("h")))]).pipe(f(([n,o])=>_p(n.config)(o.searchParams.get("h"))),f(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,l=n(c);l.length>c.length&&o.set(s,l)}for(let[s,c]of o){let{childNodes:l}=A("span",null,c);s.replaceWith(...Array.from(l))}return{ref:e,nodes:o}}))}function _p(e){let t=e.separator.split("|").map(o=>o.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":o).join("|"),r=new RegExp(t,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/\s+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${o.split(r).map(a=>a.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&")).filter(a=>a.length>0).join("|")})`,"img");return a=>(0,Bs.default)(a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function yp(e,{viewport$:t,main$:r}){let n=e.closest(".md-grid"),o=n.offsetTop-n.parentElement.offsetTop;return re([r,t]).pipe(f(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),ie((i,a)=>i.height===a.height&&i.locked===a.locked))}function Ao(e,n){var o=n,{header$:t}=o,r=gr(o,["header$"]);let i=G(".md-sidebar__scrollwrap",e),{y:a}=wt(i);return j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=s.pipe(Xe(0,je));return l.pipe(pe(t)).subscribe({next([{height:u},{height:p}]){i.style.height=`${u-2*a}px`,e.style.top=`${p}px`},complete(){i.style.height="",e.style.top=""}}),l.pipe(Sr()).subscribe(()=>{for(let u of P(".md-nav__link--active[href]",e)){if(!u.clientHeight)continue;let p=u.closest(".md-sidebar__scrollwrap");if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2})}}}),me(P("label[tabindex]",e)).pipe(oe(u=>b(u,"click").pipe(Ie(ge),f(()=>u),Q(c)))).subscribe(u=>{let p=G(`[id="${u.htmlFor}"]`);G(`[aria-labelledby="${u.id}"]`).setAttribute("aria-expanded",`${p.checked}`)}),X("content.tooltips")&&me(P("abbr[title]",e)).pipe(oe(u=>Ge(u,{viewport$})),Q(c)).subscribe(),yp(e,r).pipe($(u=>s.next(u)),V(()=>s.complete()),f(u=>H({ref:e},u)))})}function Gs(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return $t(et(`${r}/releases/latest`).pipe(_e(()=>y),f(n=>({version:n.tag_name})),ot({})),et(r).pipe(_e(()=>y),f(n=>({stars:n.stargazers_count,forks:n.forks_count})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return et(r).pipe(f(n=>({repositories:n.public_repos})),ot({}))}}function Js(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return $t(et(`${r}/releases/permalink/latest`).pipe(_e(()=>y),f(({tag_name:n})=>({version:n})),ot({})),et(r).pipe(_e(()=>y),f(({star_count:n,forks_count:o})=>({stars:n,forks:o})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}function Xs(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,n]=t;return Gs(r,n)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,n]=t;return Js(r,n)}return y}var xp;function wp(e){return xp||(xp=j(()=>{let t=__md_get("__source",sessionStorage);if(t)return Y(t);if(Ee("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return y}return Xs(e.href).pipe($(n=>__md_set("__source",n,sessionStorage)))}).pipe(_e(()=>y),L(t=>Object.keys(t).length>0),f(t=>({facts:t})),se(1)))}function Zs(e){let t=G(":scope > :last-child",e);return j(()=>{let r=new I;return r.subscribe(({facts:n})=>{t.appendChild(bs(n)),t.classList.add("md-source__repository--active")}),wp(e).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Ep(e,{viewport$:t,header$:r}){return Re(document.body).pipe(g(()=>Sn(e,{header$:r,viewport$:t})),f(({offset:{y:n}})=>({hidden:n>=10})),fe("hidden"))}function Qs(e,t){return j(()=>{let r=new I;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(X("navigation.tabs.sticky")?Y({hidden:!1}):Ep(e,t)).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Tp(e,{viewport$:t,header$:r}){let n=new Map,o=P(".md-nav__link",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),l=Le(`[id="${c}"]`);typeof l!="undefined"&&n.set(s,l)}let i=r.pipe(fe("height"),f(({height:s})=>{let c=ht("main"),l=G(":scope > :first-child",c);return s+.9*(l.offsetTop-c.offsetTop)}),xe());return Re(document.body).pipe(fe("height"),g(s=>j(()=>{let c=[];return Y([...n].reduce((l,[u,p])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=p.tagName;)c.pop();let d=p.offsetTop;for(;!d&&p.parentElement;)p=p.parentElement,d=p.offsetTop;let m=p.offsetParent;for(;m;m=m.offsetParent)d+=m.offsetTop;return l.set([...c=[...c,u]].reverse(),d)},new Map))}).pipe(f(c=>new Map([...c].sort(([,l],[,u])=>l-u))),Ze(i),g(([c,l])=>t.pipe(Or(([u,p],{offset:{y:d},size:m})=>{let h=d+m.height>=Math.floor(s.height);for(;p.length;){let[,v]=p[0];if(v-l=d&&!h)p=[u.pop(),...p];else break}return[u,p]},[[],[...c]]),ie((u,p)=>u[0]===p[0]&&u[1]===p[1])))))).pipe(f(([s,c])=>({prev:s.map(([l])=>l),next:c.map(([l])=>l)})),J({prev:[],next:[]}),Pt(2,1),f(([s,c])=>s.prev.length{let i=new I,a=i.pipe(he(),ye(!0));if(i.subscribe(({prev:s,next:c})=>{for(let[l]of c)l.classList.remove("md-nav__link--passed"),l.classList.remove("md-nav__link--active");for(let[l,[u]]of s.entries())u.classList.add("md-nav__link--passed"),u.classList.toggle("md-nav__link--active",l===s.length-1)}),X("toc.follow")){let s=R(t.pipe(Be(1),f(()=>{})),t.pipe(Be(250),f(()=>"smooth")));i.pipe(L(({prev:c})=>c.length>0),Ze(n.pipe(Ie(ge))),pe(s)).subscribe(([[{prev:c}],l])=>{let[u]=c[c.length-1];if(u.offsetHeight){let p=ki(u);if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2,behavior:l})}}})}return X("navigation.tracking")&&t.pipe(Q(a),fe("offset"),Be(250),ke(1),Q(o.pipe(ke(1))),jt({delay:250}),pe(i)).subscribe(([,{prev:s}])=>{let c=Ye(),l=s[s.length-1];if(l&&l.length){let[u]=l,{hash:p}=new URL(u.href);c.hash!==p&&(c.hash=p,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),Tp(e,{viewport$:t,header$:r}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function Sp(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(f(({offset:{y:a}})=>a),Pt(2,1),f(([a,s])=>a>s&&s>0),ie()),i=r.pipe(f(({active:a})=>a));return re([i,o]).pipe(f(([a,s])=>!(a&&s)),ie(),Q(n.pipe(ke(1))),ye(!0),jt({delay:250}),f(a=>({hidden:a})))}function tc(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(Q(a),fe("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),b(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Sp(e,{viewport$:t,main$:n,target$:o}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))}function rc(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,t.port&&(e.port=t.port),e}function Op(e,t){let r=new Map;for(let n of P("url",e)){let o=G("loc",n),i=[rc(new URL(o.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",n)){let s=a.getAttribute("href");s!=null&&i.push(rc(new URL(s),t))}}return r}function dr(e){return ns(new URL("sitemap.xml",e)).pipe(f(t=>Op(t,new URL(e))),_e(()=>Y(new Map)),xe())}function nc({document$:e}){let t=new Map;e.pipe(g(()=>P("link[rel=alternate]")),f(r=>new URL(r.href)),L(r=>!t.has(r.toString())),oe(r=>dr(r).pipe(f(n=>[r,n]),_e(()=>y)))).subscribe(([r,n])=>{t.set(r.toString().replace(/\/$/,""),n)}),b(document.body,"click").pipe(L(r=>!r.metaKey&&!r.ctrlKey),g(r=>{if(r.target instanceof Element){let n=r.target.closest("a");if(n&&!n.target){let o=[...t].find(([p])=>n.href.startsWith(`${p}/`));if(typeof o=="undefined")return y;let[i,a]=o,s=Ye();if(s.href.startsWith(i))return y;let c=Ue(),l=s.href.replace(c.base,"");l=`${i}/${l}`;let u=a.has(l.split("#")[0])?new URL(l,c.base):new URL(i);return r.preventDefault(),Y(u)}}return y})).subscribe(r=>dt(r,!0))}var Co=_r(Mo());function Lp(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function oc({alert$:e}){Co.default.isSupported()&&new U(t=>{new Co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Lp(G(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe($(t=>{t.trigger.focus()}),f(()=>Bt("clipboard.copied"))).subscribe(e)}function ic(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let n=new URL(r.href);return n.search=n.hash="",t.has(`${n}`)?(e.preventDefault(),Y(r)):y}function ac(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function sc(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let n=t.getAttribute(r);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){t[r]=t[r];break}}return Y(e)}function Mp(e){for(let n of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...X("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let o=Le(n),i=Le(n,e);typeof o!="undefined"&&typeof i!="undefined"&&o.replaceWith(i)}let t=ac(document);for(let[n,o]of ac(e))t.has(n)?t.delete(n):document.head.appendChild(o);for(let n of t.values()){let o=n.getAttribute("name");o!=="theme-color"&&o!=="color-scheme"&&n.remove()}let r=ht("container");return nt(P("script",r)).pipe(g(n=>{let o=e.createElement("script");if(n.src){for(let i of n.getAttributeNames())o.setAttribute(i,n.getAttribute(i));return n.replaceWith(o),new U(i=>{o.onload=()=>i.complete()})}else return o.textContent=n.textContent,n.replaceWith(o),y}),he(),ye(document))}function cc({sitemap$:e,location$:t,viewport$:r,progress$:n}){if(location.protocol==="file:")return Ke;Y(document).subscribe(sc);let o=b(document.body,"click").pipe(Ze(e),g(([s,c])=>ic(s,c)),f(({href:s})=>new URL(s)),xe()),i=b(window,"popstate").pipe(f(Ye),xe());o.pipe(pe(r)).subscribe(([s,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",s)}),R(o,i).subscribe(t);let a=t.pipe(fe("pathname"),g(s=>En(s,{progress$:n}).pipe(_e(()=>(dt(s,!0),y)))),g(sc),g(Mp),xe());return R(a.pipe(pe(t,(s,c)=>c)),a.pipe(g(()=>t),fe("hash")),t.pipe(ie((s,c)=>s.pathname===c.pathname&&s.hash===c.hash),g(()=>o),$(()=>history.back()))).subscribe(s=>{var c,l;history.state!==null||!s.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",es(s.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),b(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(fe("offset"),Be(100)).subscribe(({offset:s})=>{history.replaceState(s,"")}),X("navigation.instant.prefetch")&&R(b(document.body,"mousemove"),b(document.body,"focusin")).pipe(Ze(e),g(([s,c])=>ic(s,c)),Be(25),Yn(({href:s})=>s),cn(s=>{let c=document.createElement("link");return c.rel="prefetch",c.href=s.toString(),document.head.appendChild(c),b(c,"load").pipe(f(()=>c),Me(1))})).subscribe(s=>s.remove()),a}function lc(e){var u;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:n,currentBaseURL:o}=e,i=(u=Ho(o))==null?void 0:u.pathname;if(i===void 0)return;let a=kp(n.pathname,i);if(a===void 0)return;let s=Cp(t.keys());if(!t.has(s))return;let c=Ho(a,s);if(!c||!t.has(c.href))return;let l=Ho(a,r);if(l)return l.hash=n.hash,l.search=n.search,l}function Ho(e,t){try{return new URL(e,t)}catch(r){return}}function kp(e,t){if(e.startsWith(t))return e.slice(t.length)}function Ap(e,t){let r=Math.min(e.length,t.length),n;for(n=0;ny)),n=r.pipe(f(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));r.pipe(f(o=>new Map(o.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),g(o=>b(document.body,"click").pipe(L(i=>!i.metaKey&&!i.ctrlKey),pe(n),g(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&o.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&o.get(c)===a?y:(i.preventDefault(),Y(new URL(c)))}}return y}),g(i=>dr(i).pipe(f(a=>{var s;return(s=lc({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:Ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(o=>dt(o,!0)),re([r,n]).subscribe(([o,i])=>{G(".md-header__topic").appendChild(_s(o,i))}),e.pipe(g(()=>n)).subscribe(o=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let c=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let l of c)for(let u of o.aliases.concat(o.version))if(new RegExp(l,"i").test(u)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let c of Ee("outdated"))c.hidden=!1})}function pc({document$:e,viewport$:t}){e.pipe(g(()=>P(".md-ellipsis")),oe(r=>Et(r).pipe(Q(e.pipe(ke(1))),L(n=>n),f(()=>r),Me(1))),L(r=>r.offsetWidth{let n=r.innerText,o=r.closest("a")||r;return o.title=n,X("content.tooltips")?Ge(o,{viewport$:t}).pipe(Q(e.pipe(ke(1))),V(()=>o.removeAttribute("title"))):y})).subscribe(),X("content.tooltips")&&e.pipe(g(()=>P(".md-status")),oe(r=>Ge(r,{viewport$:t}))).subscribe()}function fc({document$:e,tablet$:t}){e.pipe(g(()=>P(".md-toggle--indeterminate")),$(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>b(r,"change").pipe(Xn(()=>r.classList.contains("md-toggle--indeterminate")),f(()=>r))),pe(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function Hp(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function mc({document$:e}){e.pipe(g(()=>P("[data-md-scrollfix]")),$(t=>t.removeAttribute("data-md-scrollfix")),L(Hp),oe(t=>b(t,"touchstart").pipe(f(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));function $p(){return location.protocol==="file:"?ar(`${new URL("search.js",Mn.base)}`).pipe(f(()=>__index),_e(()=>Ke),se(1)):et(new URL("search.json",Mn.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var vt=Si(),Ur=Za(),hr=ts(Ur),hc=Xa(),ze=cs(),$o=Ir("(min-width: 60em)"),vc=Ir("(min-width: 76.25em)"),bc=rs(),Mn=Ue(),gc=Le(".md-search")?$p():Ke,Po=new I;oc({alert$:Po});nc({document$:vt});var Io=new I,_c=dr(Mn.base);X("navigation.instant")&&cc({sitemap$:_c,location$:Ur,viewport$:ze,progress$:Io}).subscribe(vt);var dc;((dc=Mn.version)==null?void 0:dc.provider)==="mike"&&uc({document$:vt});R(Ur,hr).pipe(It(125)).subscribe(()=>{Eo("drawer",!1),Eo("search",!1)});hc.pipe(L(({mode:e,meta:t})=>e==="global"&&!t)).subscribe(e=>{switch(e.type){case",":case"p":let t=document.querySelector("link[rel=prev]");t instanceof HTMLLinkElement&&dt(t);break;case".":case"n":let r=document.querySelector("link[rel=next]");r instanceof HTMLLinkElement&&dt(r);break;case"/":let n=document.querySelector("[data-md-component=search] button");n instanceof HTMLButtonElement&&n.click();break;case"Enter":let o=xt();o instanceof HTMLLabelElement&&o.click()}});pc({viewport$:ze,document$:vt});fc({document$:vt,tablet$:$o});mc({document$:vt});var Lt=Us(ht("header"),{viewport$:ze}),Fr=vt.pipe(f(()=>ht("main")),g(e=>Ws(e,{viewport$:ze,header$:Lt})),se(1)),Pp=R(...Ee("consent").map(e=>us(e,{target$:hr})),...Ee("dialog").map(e=>Fs(e,{alert$:Po})),...Ee("palette").map(e=>Vs(e)),...Ee("progress").map(e=>zs(e,{progress$:Io})),...Ee("search").map(e=>Ks(e,{index$:gc})),...Ee("source").map(e=>Zs(e))),Ip=j(()=>R(...Ee("announce").map(e=>ls(e)),...Ee("content").map(e=>js(e,{sitemap$:_c,viewport$:ze,target$:hr,print$:bc})),...Ee("content").map(e=>X("search.highlight")?Ys(e,{index$:gc,location$:Ur}):y),...Ee("header").map(e=>Ns(e,{viewport$:ze,header$:Lt,main$:Fr})),...Ee("header-title").map(e=>Ds(e,{viewport$:ze,header$:Lt})),...Ee("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?yo(vc,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr})):yo($o,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr}))),...Ee("tabs").map(e=>Qs(e,{viewport$:ze,header$:Lt})),...Ee("toc").map(e=>ec(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})),...Ee("top").map(e=>tc(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})))),yc=vt.pipe(g(()=>Ip),Rt(Pp),se(1));yc.subscribe();window.document$=vt;window.location$=Ur;window.target$=hr;window.keyboard$=hc;window.viewport$=ze;window.tablet$=$o;window.screen$=vc;window.print$=bc;window.alert$=Po;window.progress$=Io;window.component$=yc;})(); -/*! update cache: 20260331044524 */ +/*! update cache: 20260331053217 */ diff --git a/ja/chapter_stack_and_queue/queue/index.html b/ja/chapter_stack_and_queue/queue/index.html index 0a236b028..81dec591c 100644 --- a/ja/chapter_stack_and_queue/queue/index.html +++ b/ja/chapter_stack_and_queue/queue/index.html @@ -6445,7 +6445,7 @@ typedef struct { int *nums; // キュー要素を格納する配列 int front; // 先頭ポインタ。先頭要素を指す - int queSize; // 末尾ポインタ。キューの末尾 + 1 を指す + int queSize; // 現在のキュー内の要素数 int queCapacity; // キューの容量 } ArrayQueue; diff --git a/ja/javascripts/katex.js b/ja/javascripts/katex.js index 4953d6994..bdee3e6b8 100644 --- a/ja/javascripts/katex.js +++ b/ja/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260331044524 */ +/*! update cache: 20260331053217 */ diff --git a/ja/javascripts/mathjax.js b/ja/javascripts/mathjax.js index 08685f45f..e756f0f29 100644 --- a/ja/javascripts/mathjax.js +++ b/ja/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260331044524 */ +/*! update cache: 20260331053217 */ diff --git a/ja/javascripts/starfield.js b/ja/javascripts/starfield.js index ff79c29cb..5569a232c 100644 --- a/ja/javascripts/starfield.js +++ b/ja/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260331044524 */ +/*! update cache: 20260331053217 */ diff --git a/ja/search.json b/ja/search.json index 42bdcc1ec..cfddf1df2 100644 --- a/ja/search.json +++ b/ja/search.json @@ -1 +1 @@ -{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"第 16 章   付録","text":"","path":["第 16 章   付録"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"章の内容","text":"
  • 16.1   プログラミング環境のインストール
  • 16.2   一緒に制作に参加しましょう
  • 16.3   用語集
","path":["第 16 章   付録"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   一緒に制作に参加しましょう","text":"

著者の力には限りがあるため、本書にはどうしても一部の漏れや誤りが含まれる可能性があります。ご了承ください。誤字、リンク切れ、内容の欠落、表現の曖昧さ、説明の不明瞭さ、文章構成の不適切さなどの問題を見つけた場合は、ぜひ修正にご協力ください。読者により良い学習リソースを提供できます。

すべての寄稿者の GitHub ID は、本書のリポジトリ、Web 版、PDF 版のホームページに掲載され、オープンソースコミュニティへの惜しみない貢献に感謝を表します。

オープンソースの魅力

紙の書籍では、2 回の増刷の間隔が長くなりがちで、内容更新は非常に不便です。

一方、このオープンソース書籍では、内容更新のサイクルは数日、場合によっては数時間にまで短縮されています。

","path":["第 16 章   付録","16.2   一緒に制作に参加しましょう"],"tags":[]},{"location":"chapter_appendix/contribution/#1","level":3,"title":"1.   内容の微調整","text":"

以下の図のように、各ページの右上には「編集アイコン」があります。次の手順で本文やコードを修正できます。

  1. 「編集アイコン」をクリックし、「このリポジトリを Fork する必要があります」と表示された場合は、その操作を承認してください。
  2. Markdown のソースファイルを修正し、内容が正しいことを確認したうえで、できるだけ書式の統一を保ってください。
  3. ページ下部に修正内容の説明を入力し、その後「Propose file change」ボタンをクリックします。ページ遷移後、「Create pull request」ボタンをクリックするとプルリクエストを作成できます。

図 16-3   ページ編集ボタン

画像は直接修正できないため、新しい Issue を作成するかコメントで問題を説明してください。できるだけ早く描き直して差し替えます。

","path":["第 16 章   付録","16.2   一緒に制作に参加しましょう"],"tags":[]},{"location":"chapter_appendix/contribution/#2","level":3,"title":"2.   コンテンツ制作","text":"

コードを他のプログラミング言語へ翻訳することや、記事内容を拡充することなど、このオープンソースプロジェクトへの参加に興味がある場合は、以下の Pull Request ワークフローに従ってください。

  1. GitHub にログインし、本書のコードリポジトリを個人アカウントに Fork します。
  2. Fork したリポジトリのページに入り、git clone コマンドを使ってリポジトリをローカルにクローンします。
  3. ローカルでコンテンツを作成し、完全なテストを行ってコードの正しさを確認します。
  4. ローカルで行った変更を Commit し、その後リモートリポジトリへ Push します。
  5. リポジトリのページを更新し、「Create pull request」ボタンをクリックするとプルリクエストを作成できます。
","path":["第 16 章   付録","16.2   一緒に制作に参加しましょう"],"tags":[]},{"location":"chapter_appendix/contribution/#3-docker","level":3,"title":"3.   Docker デプロイ","text":"

hello-algo のルートディレクトリで以下の Docker スクリプトを実行すると、http://localhost:8000 で本プロジェクトにアクセスできます。

docker-compose up -d\n

以下のコマンドでデプロイを削除できます。

docker-compose down\n
","path":["第 16 章   付録","16.2   一緒に制作に参加しましょう"],"tags":[]},{"location":"chapter_appendix/installation/","level":1,"title":"16.1   プログラミング環境のインストール","text":"","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#1611-ide","level":2,"title":"16.1.1   IDE のインストール","text":"

オープンソースで軽量な VS Code をローカルの統合開発環境(IDE)として使用することを推奨します。VS Code 公式サイト にアクセスし、使用している OS に応じたバージョンの VS Code をダウンロードしてインストールしてください。

図 16-1   公式サイトから VS Code をダウンロード

VS Code には強力な拡張機能のエコシステムがあり、ほとんどのプログラミング言語の実行とデバッグをサポートしています。Python を例にすると、「Python Extension Pack」拡張機能をインストールした後、Python コードをデバッグできるようになります。インストール手順を以下に示します。

図 16-2   VS Code 拡張機能のインストール

","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#1612","level":2,"title":"16.1.2   言語環境のインストール","text":"","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#1-python","level":3,"title":"1.   Python 環境","text":"
  1. Miniconda3 をダウンロードしてインストールします。Python 3.10 以降が必要です。
  2. VS Code の拡張機能マーケットプレイスで python を検索し、Python Extension Pack をインストールします。
  3. (任意)コマンドラインで pip install black を入力し、コード整形ツールをインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#2-cc","level":3,"title":"2.   C/C++ 環境","text":"
  1. Windows システムでは MinGW をインストールする必要があります(設定チュートリアル)。MacOS には Clang が標準搭載されているため、追加インストールは不要です。
  2. VS Code の拡張機能マーケットプレイスで c++ を検索し、C/C++ Extension Pack をインストールします。
  3. (任意)Settings ページを開き、コード整形オプション Clang_format_fallback Style を検索して、{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach } に設定します。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#3-java","level":3,"title":"3.   Java 環境","text":"
  1. OpenJDK をダウンロードしてインストールします(バージョンは JDK 9 より新しい必要があります)。
  2. VS Code の拡張機能マーケットプレイスで java を検索し、Extension Pack for Java をインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#4-c","level":3,"title":"4.   C# 環境","text":"
  1. .Net 8.0 をダウンロードしてインストールします。
  2. VS Code の拡張機能マーケットプレイスで C# Dev Kit を検索し、C# Dev Kit をインストールします(設定チュートリアル)。
  3. Visual Studio を使用することもできます(インストール手順)。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#5-go","level":3,"title":"5.   Go 環境","text":"
  1. go をダウンロードしてインストールします。
  2. VS Code の拡張機能マーケットプレイスで go を検索し、Go をインストールします。
  3. ショートカットキー Ctrl + Shift + P を押してコマンドパレットを開き、go と入力して Go: Install/Update Tools を選択し、すべてにチェックを入れてインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#6-swift","level":3,"title":"6.   Swift 環境","text":"
  1. Swift をダウンロードしてインストールします。
  2. VS Code の拡張機能マーケットプレイスで swift を検索し、Swift for Visual Studio Code をインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#7-javascript","level":3,"title":"7.   JavaScript 環境","text":"
  1. Node.js をダウンロードしてインストールします。
  2. (任意)VS Code の拡張機能マーケットプレイスで Prettier を検索し、コード整形ツールをインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#8-typescript","level":3,"title":"8.   TypeScript 環境","text":"
  1. JavaScript 環境と同じ手順でインストールします。
  2. TypeScript Execute (tsx) をインストールします。
  3. VS Code の拡張機能マーケットプレイスで typescript を検索し、Pretty TypeScript Errors をインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#9-dart","level":3,"title":"9.   Dart 環境","text":"
  1. Dart をダウンロードしてインストールします。
  2. VS Code の拡張機能マーケットプレイスで dart を検索し、Dart をインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#10-rust","level":3,"title":"10.   Rust 環境","text":"
  1. Rust をダウンロードしてインストールします。
  2. VS Code の拡張機能マーケットプレイスで rust を検索し、rust-analyzer をインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/terminology/","level":1,"title":"16.3   用語集","text":"

以下の表は、本書に登場する重要な用語を一覧にしたものです。特に次の点に注意してください。

  • 名詞の英語表現も覚えておくと、英語文献を読む際に役立ちます。
  • 一部の名詞は、簡体字中国語と繁体字中国語で呼び方が異なります。

表 16-1   データ構造とアルゴリズムの重要用語

English 日本語 日本語 algorithm アルゴリズム アルゴリズム data structure データ構造 データ構造 code コード コード file ファイル ファイル function 関数 関数 method メソッド メソッド variable 変数 変数 asymptotic complexity analysis 漸近計算量解析 漸近計算量解析 time complexity 時間計算量 時間計算量 space complexity 空間計算量 空間計算量 loop ループ ループ iteration 反復 反復 recursion 再帰 再帰 tail recursion 末尾再帰 末尾再帰 recursion tree 再帰木 再帰木 big-\\(O\\) notation ビッグオー記法 ビッグオー記法 asymptotic upper bound 漸近上界 漸近上界 sign-magnitude 符号絶対値表現 符号絶対値表現 1’s complement 1の補数 1の補数 2’s complement 2の補数 2の補数 array 配列 配列 index インデックス インデックス linked list 連結リスト 連結リスト linked list node, list node 連結リストノード 連結リストノード head node 先頭ノード 先頭ノード tail node 末尾ノード 末尾ノード list リスト リスト dynamic array 動的配列 動的配列 hard disk ハードディスク ハードディスク random-access memory (RAM) メモリ メモリ cache memory キャッシュ キャッシュ cache miss キャッシュミス キャッシュミス cache hit rate キャッシュヒット率 キャッシュヒット率 stack スタック スタック top of the stack スタックトップ スタックトップ bottom of the stack スタックボトム スタックボトム queue キュー キュー double-ended queue 両端キュー 両端キュー front of the queue キュー先頭 キュー先頭 rear of the queue キュー末尾 キュー末尾 hash table ハッシュテーブル ハッシュテーブル hash set ハッシュ集合 ハッシュ集合 bucket バケット バケット hash function ハッシュ関数 ハッシュ関数 hash collision ハッシュ衝突 ハッシュ衝突 load factor 負荷率 負荷率 separate chaining 連鎖アドレス法 連鎖アドレス法 open addressing オープンアドレス法 オープンアドレス法 linear probing 線形探索 線形探索 lazy deletion 遅延削除 遅延削除 binary tree 二分木 二分木 tree node ノード ノード left-child node 左子ノード 左子ノード right-child node 右子ノード 右子ノード parent node 親ノード 親ノード left subtree 左部分木 左部分木 right subtree 右部分木 右部分木 root node 根ノード 根ノード leaf node 葉ノード 葉ノード edge 辺 辺 level レベル レベル degree 次数 次数 height 高さ 高さ depth 深さ 深さ perfect binary tree 完備二分木 完備二分木 complete binary tree 完全二分木 完全二分木 full binary tree 満二分木 満二分木 balanced binary tree 平衡二分木 平衡二分木 binary search tree 二分探索木 二分探索木 AVL tree AVL 木 AVL 木 red-black tree 赤黒木 赤黒木 level-order traversal レベル順走査 レベル順走査 breadth-first traversal 幅優先走査 幅優先走査 depth-first traversal 深さ優先走査 深さ優先走査 binary search tree 二分探索木 二分探索木 balanced binary search tree 平衡二分探索木 平衡二分探索木 balance factor 平衡係数 平衡係数 heap ヒープ ヒープ max heap 最大ヒープ 最大ヒープ min heap 最小ヒープ 最小ヒープ priority queue 優先度付きキュー 優先度付きキュー heapify ヒープ化 ヒープ化 top-\\(k\\) problem Top-\\(k\\) 問題 Top-\\(k\\) 問題 graph グラフ グラフ vertex 頂点 頂点 undirected graph 無向グラフ 無向グラフ directed graph 有向グラフ 有向グラフ connected graph 連結グラフ 連結グラフ disconnected graph 非連結グラフ 非連結グラフ weighted graph 重み付きグラフ 重み付きグラフ adjacency 隣接 隣接 path 経路 経路 in-degree 入次数 入次数 out-degree 出次数 出次数 adjacency matrix 隣接行列 隣接行列 adjacency list 隣接リスト 隣接リスト breadth-first search 幅優先探索 幅優先探索 depth-first search 深さ優先探索 深さ優先探索 binary search 二分探索 二分探索 searching algorithm 探索アルゴリズム 探索アルゴリズム sorting algorithm ソートアルゴリズム ソートアルゴリズム selection sort 選択ソート 選択ソート bubble sort バブルソート バブルソート insertion sort 挿入ソート 挿入ソート quick sort クイックソート クイックソート merge sort マージソート マージソート heap sort ヒープソート ヒープソート bucket sort バケットソート バケットソート counting sort 計数ソート 計数ソート radix sort 基数ソート 基数ソート divide and conquer 分割統治 分割統治 hanota problem ハノイの塔問題 ハノイの塔問題 backtracking algorithm バックトラッキングアルゴリズム バックトラッキングアルゴリズム constraint 制約 制約 solution 解 解 state 状態 状態 pruning 枝刈り 枝刈り permutations problem 全順列問題 全順列問題 subset-sum problem 部分和問題 部分和問題 \\(n\\)-queens problem \\(n\\) クイーン問題 \\(n\\) クイーン問題 dynamic programming 動的計画法 動的計画法 initial state 初期状態 初期状態 state-transition equation 状態遷移方程式 状態遷移方程式 knapsack problem ナップサック問題 ナップサック問題 edit distance problem 編集距離問題 編集距離問題 greedy algorithm 貪欲法 貪欲法","path":["第 16 章   付録","16.3   用語集"],"tags":[]},{"location":"chapter_array_and_linkedlist/","level":1,"title":"第 4 章   配列と連結リスト","text":"

Abstract

データ構造の世界は、まるで重厚なれんがの壁のようです。

配列のれんがは整然と並び、一つひとつがぴったりと接しています。連結リストのれんがはあちこちに分散し、それらをつなぐつるがれんがのすき間を自由に行き交います。

","path":["第 4 章   配列と連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"章の内容","text":"
  • 4.1   配列
  • 4.2   連結リスト
  • 4.3   リスト
  • 4.4   メモリとキャッシュ *
  • 4.5   まとめ
","path":["第 4 章   配列と連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   配列","text":"

配列(array)は線形データ構造の一種であり、同じ型の要素を連続したメモリ領域に格納します。要素が配列内にある位置を、その要素のインデックス(index)と呼びます。下図は、配列の主要な概念と格納方式を示しています。

図 4-1   配列の定義と格納方式

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#411","level":2,"title":"4.1.1   配列の一般的な操作","text":"","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#1","level":3,"title":"1.   配列の初期化","text":"

必要に応じて、配列の初期化方法として初期値なしと初期値ありの 2 種類を使い分けられます。初期値を指定しない場合、多くのプログラミング言語では配列要素は \\(0\\) に初期化されます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
# 配列を初期化する\narr: list[int] = [0] * 5  # [ 0, 0, 0, 0, 0 ]\nnums: list[int] = [1, 3, 2, 5, 4]\n
array.cpp
/* 配列を初期化する */\n// スタック上に格納\nint arr[5];\nint nums[5] = { 1, 3, 2, 5, 4 };\n// ヒープ上に格納(手動で領域を解放する必要がある)\nint* arr1 = new int[5];\nint* nums1 = new int[5] { 1, 3, 2, 5, 4 };\n
array.java
/* 配列を初期化する */\nint[] arr = new int[5]; // { 0, 0, 0, 0, 0 }\nint[] nums = { 1, 3, 2, 5, 4 };\n
array.cs
/* 配列を初期化する */\nint[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]\nint[] nums = [1, 3, 2, 5, 4];\n
array.go
/* 配列を初期化する */\nvar arr [5]int\n// Go では、長さを指定する場合([5]int)は配列であり、長さを指定しない場合([]int)はスライス\n// Go の配列はコンパイル時に長さが確定するよう設計されているため、長さの指定には定数しか使用できない\n// 拡張 extend() メソッドを実装しやすくするため、以下ではスライス(Slice)を配列(Array)として扱う\nnums := []int{1, 3, 2, 5, 4}\n
array.swift
/* 配列を初期化する */\nlet arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]\nlet nums = [1, 3, 2, 5, 4]\n
array.js
/* 配列を初期化する */\nvar arr = new Array(5).fill(0);\nvar nums = [1, 3, 2, 5, 4];\n
array.ts
/* 配列を初期化する */\nlet arr: number[] = new Array(5).fill(0);\nlet nums: number[] = [1, 3, 2, 5, 4];\n
array.dart
/* 配列を初期化する */\nList<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]\nList<int> nums = [1, 3, 2, 5, 4];\n
array.rs
/* 配列を初期化する */\nlet arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]\nlet slice: &[i32] = &[0; 5];\n// Rust では、長さを指定する場合([i32; 5])は配列であり、長さを指定しない場合(&[i32])はスライス\n// Rust の配列はコンパイル時に長さが確定するよう設計されているため、長さの指定には定数しか使用できない\n// Vector は Rust で一般に動的配列として使われる型\n// 拡張 extend() メソッドを実装しやすくするため、以下では vector を配列(array)として扱う\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
array.c
/* 配列を初期化する */\nint arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }\nint nums[5] = { 1, 3, 2, 5, 4 };\n
array.kt
/* 配列を初期化する */\nvar arr = IntArray(5) // { 0, 0, 0, 0, 0 }\nvar nums = intArrayOf(1, 3, 2, 5, 4)\n
array.rb
# 配列を初期化する\narr = Array.new(5, 0)\nnums = [1, 3, 2, 5, 4]\n
実行の可視化

https://pythontutor.com/render.html#code=%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0Aarr%20%3D%20%5B0%5D%20*%205%20%20%23%20%5B%200,%200,%200,%200,%200%20%5D%0Anums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#2","level":3,"title":"2.   要素へのアクセス","text":"

配列要素は連続したメモリ領域に格納されるため、要素のメモリアドレスの計算は非常に容易です。配列のメモリアドレス(先頭要素のメモリアドレス)とある要素のインデックスが与えられれば、下図の式を使ってその要素のメモリアドレスを計算でき、直接その要素にアクセスできます。

図 4-2   配列要素のメモリアドレスの計算

上図を見ると、配列の最初の要素のインデックスは \\(0\\) であり、これは少し直感に反するように思えます。というのも、\\(1\\) から数え始めるほうが自然だからです。しかし、アドレス計算式の観点では、**インデックスの本質はメモリアドレスのオフセット**です。先頭要素のアドレスのオフセットは \\(0\\) であるため、そのインデックスが \\(0\\) なのは妥当です。

配列では要素へのアクセスは非常に効率的であり、\\(O(1)\\) 時間で任意の要素にランダムアクセスできます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def random_access(nums: list[int]) -> int:\n    \"\"\"要素へランダムアクセス\"\"\"\n    # 区間 [0, len(nums)-1] からランダムに数字を 1 つ選ぶ\n    random_index = random.randint(0, len(nums) - 1)\n    # ランダムな要素を取得して返す\n    random_num = nums[random_index]\n    return random_num\n
array.cpp
/* 要素へランダムアクセス */\nint randomAccess(int *nums, int size) {\n    // 区間 [0, size) からランダムに 1 つの数を選ぶ\n    int randomIndex = rand() % size;\n    // ランダムな要素を取得して返す\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.java
/* 要素へランダムアクセス */\nint randomAccess(int[] nums) {\n    // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n    int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);\n    // ランダムな要素を取得して返す\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.cs
/* 要素へランダムアクセス */\nint RandomAccess(int[] nums) {\n    Random random = new();\n    // 区間 [0, nums.Length) からランダムに数字を 1 つ選ぶ\n    int randomIndex = random.Next(nums.Length);\n    // ランダムな要素を取得して返す\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.go
/* 要素へランダムアクセス */\nfunc randomAccess(nums []int) (randomNum int) {\n    // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n    randomIndex := rand.Intn(len(nums))\n    // ランダムな要素を取得して返す\n    randomNum = nums[randomIndex]\n    return\n}\n
array.swift
/* 要素へランダムアクセス */\nfunc randomAccess(nums: [Int]) -> Int {\n    // 区間 [0, nums.count) からランダムに数字を 1 つ選ぶ\n    let randomIndex = nums.indices.randomElement()!\n    // ランダムな要素を取得して返す\n    let randomNum = nums[randomIndex]\n    return randomNum\n}\n
array.js
/* 要素へランダムアクセス */\nfunction randomAccess(nums) {\n    // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n    const random_index = Math.floor(Math.random() * nums.length);\n    // ランダムな要素を取得して返す\n    const random_num = nums[random_index];\n    return random_num;\n}\n
array.ts
/* 要素へランダムアクセス */\nfunction randomAccess(nums: number[]): number {\n    // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n    const random_index = Math.floor(Math.random() * nums.length);\n    // ランダムな要素を取得して返す\n    const random_num = nums[random_index];\n    return random_num;\n}\n
array.dart
/* 要素へランダムアクセス */\nint randomAccess(List<int> nums) {\n  // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n  int randomIndex = Random().nextInt(nums.length);\n  // ランダムな要素を取得して返す\n  int randomNum = nums[randomIndex];\n  return randomNum;\n}\n
array.rs
/* 要素へランダムアクセス */\nfn random_access(nums: &[i32]) -> i32 {\n    // 区間 [0, nums.len()) からランダムに数字を 1 つ選ぶ\n    let random_index = rand::thread_rng().gen_range(0..nums.len());\n    // ランダムな要素を取得して返す\n    let random_num = nums[random_index];\n    random_num\n}\n
array.c
/* 要素へランダムアクセス */\nint randomAccess(int *nums, int size) {\n    // 区間 [0, size) からランダムに 1 つの数を選ぶ\n    int randomIndex = rand() % size;\n    // ランダムな要素を取得して返す\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.kt
/* 要素へランダムアクセス */\nfun randomAccess(nums: IntArray): Int {\n    // 区間 [0, nums.size) からランダムに数字を 1 つ選ぶ\n    val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size)\n    // ランダムな要素を取得して返す\n    val randomNum = nums[randomIndex]\n    return randomNum\n}\n
array.rb
### 要素にランダムアクセス ###\ndef random_access(nums)\n  # 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n  random_index = Random.rand(0...nums.length)\n\n  # ランダムな要素を取得して返す\n  nums[random_index]\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#3","level":3,"title":"3.   要素の挿入","text":"

配列要素はメモリ内で「ぴったり隣接して」おり、その間にほかのデータを格納する余地はありません。下図のように、配列の途中に要素を挿入したい場合は、その要素より後ろにあるすべての要素を 1 つずつ後ろへずらし、その後でそのインデックスに要素を代入する必要があります。

図 4-3   配列への要素挿入の例

注意すべき点として、配列の長さは固定であるため、要素を 1 つ挿入すると配列末尾の要素が必ず「失われ」ます。この問題の解決策は「リスト」の章で扱います。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def insert(nums: list[int], num: int, index: int):\n    \"\"\"配列の index 番目に要素 num を挿入\"\"\"\n    # インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in range(len(nums) - 1, index, -1):\n        nums[i] = nums[i - 1]\n    # index の要素に num を代入する\n    nums[index] = num\n
array.cpp
/* 配列の index 番目に要素 num を挿入 */\nvoid insert(int *nums, int size, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.java
/* 配列の index 番目に要素 num を挿入 */\nvoid insert(int[] nums, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.cs
/* 配列の index 番目に要素 num を挿入 */\nvoid Insert(int[] nums, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = nums.Length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.go
/* 配列の index 番目に要素 num を挿入 */\nfunc insert(nums []int, num int, index int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i := len(nums) - 1; i > index; i-- {\n        nums[i] = nums[i-1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
array.swift
/* 配列の index 番目に要素 num を挿入 */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in nums.indices.dropFirst(index).reversed() {\n        nums[i] = nums[i - 1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
array.js
/* 配列の index 番目に要素 num を挿入 */\nfunction insert(nums, num, index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.ts
/* 配列の index 番目に要素 num を挿入 */\nfunction insert(nums: number[], num: number, index: number): void {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.dart
/* 配列の添字 index に要素 _num を挿入 */\nvoid insert(List<int> nums, int _num, int index) {\n  // インデックス index 以降の全要素を 1 つ後ろへ移動する\n  for (var i = nums.length - 1; i > index; i--) {\n    nums[i] = nums[i - 1];\n  }\n  // _num を index の位置の要素に代入\n  nums[index] = _num;\n}\n
array.rs
/* 配列の index 番目に要素 num を挿入 */\nfn insert(nums: &mut [i32], num: i32, index: usize) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in (index + 1..nums.len()).rev() {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.c
/* 配列の index 番目に要素 num を挿入 */\nvoid insert(int *nums, int size, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.kt
/* 配列の index 番目に要素 num を挿入 */\nfun insert(nums: IntArray, num: Int, index: Int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (i in nums.size - 1 downTo index + 1) {\n        nums[i] = nums[i - 1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
array.rb
### 配列のインデックス index に要素 num を挿入 ###\ndef insert(nums, num, index)\n  # インデックス index 以降の全要素を 1 つ後ろへ移動する\n  for i in (nums.length - 1).downto(index + 1)\n    nums[i] = nums[i - 1]\n  end\n\n  # index の要素に num を代入する\n  nums[index] = num\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4","level":3,"title":"4.   要素の削除","text":"

同様に、下図のように、インデックス \\(i\\) の要素を削除したい場合は、インデックス \\(i\\) より後ろの要素をすべて 1 つずつ前へずらす必要があります。

図 4-4   配列からの要素削除の例

注意してください。要素の削除が完了すると、もともとの末尾要素は「意味を持たない」状態になるため、わざわざ変更する必要はありません。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def remove(nums: list[int], index: int):\n    \"\"\"index の要素を削除する\"\"\"\n    # インデックス index より後ろの全要素を 1 つ前へ移動する\n    for i in range(index, len(nums) - 1):\n        nums[i] = nums[i + 1]\n
array.cpp
/* index の要素を削除する */\nvoid remove(int *nums, int size, int index) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.java
/* index の要素を削除する */\nvoid remove(int[] nums, int index) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (int i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.cs
/* index の要素を削除する */\nvoid Remove(int[] nums, int index) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (int i = index; i < nums.Length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.go
/* index の要素を削除する */\nfunc remove(nums []int, index int) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for i := index; i < len(nums)-1; i++ {\n        nums[i] = nums[i+1]\n    }\n}\n
array.swift
/* index の要素を削除する */\nfunc remove(nums: inout [Int], index: Int) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for i in nums.indices.dropFirst(index).dropLast() {\n        nums[i] = nums[i + 1]\n    }\n}\n
array.js
/* index の要素を削除する */\nfunction remove(nums, index) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.ts
/* index の要素を削除する */\nfunction remove(nums: number[], index: number): void {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.dart
/* index の要素を削除する */\nvoid remove(List<int> nums, int index) {\n  // インデックス index より後ろの全要素を 1 つ前へ移動する\n  for (var i = index; i < nums.length - 1; i++) {\n    nums[i] = nums[i + 1];\n  }\n}\n
array.rs
/* index の要素を削除する */\nfn remove(nums: &mut [i32], index: usize) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for i in index..nums.len() - 1 {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.c
/* index の要素を削除する */\n// 注意: stdio.h が remove 識別子を使用している\nvoid removeItem(int *nums, int size, int index) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.kt
/* index の要素を削除する */\nfun remove(nums: IntArray, index: Int) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (i in index..<nums.size - 1) {\n        nums[i] = nums[i + 1]\n    }\n}\n
array.rb
### インデックス index の要素を削除 ###\ndef remove(nums, index)\n  # インデックス index より後ろの全要素を 1 つ前へ移動する\n  for i in index...(nums.length - 1)\n    nums[i] = nums[i + 1]\n  end\nend\n
コードの可視化

全画面で見る >

全体として見ると、配列の挿入と削除には次の欠点があります。

  • 時間計算量が高い:配列の挿入と削除の平均時間計算量はいずれも \\(O(n)\\) であり、ここで \\(n\\) は配列長です。
  • 要素が失われる:配列の長さは不変であるため、要素を挿入すると配列長の範囲を超えた要素は失われます。
  • メモリの浪費:やや長めの配列を初期化して先頭部分だけを使うこともでき、この場合データ挿入時に失われる末尾要素はすべて「無意味」ですが、その代わり一部のメモリ領域が無駄になります。
","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#5","level":3,"title":"5.   配列の走査","text":"

ほとんどのプログラミング言語では、インデックスを使って配列を走査することも、各要素を直接取り出しながら走査することもできます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def traverse(nums: list[int]):\n    \"\"\"配列を走査\"\"\"\n    count = 0\n    # インデックスで配列を走査\n    for i in range(len(nums)):\n        count += nums[i]\n    # 配列要素を直接走査\n    for num in nums:\n        count += num\n    # データのインデックスと要素を同時に走査する\n    for i, num in enumerate(nums):\n        count += nums[i]\n        count += num\n
array.cpp
/* 配列を走査 */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // インデックスで配列を走査\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
array.java
/* 配列を走査 */\nvoid traverse(int[] nums) {\n    int count = 0;\n    // インデックスで配列を走査\n    for (int i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // 配列要素を直接走査\n    for (int num : nums) {\n        count += num;\n    }\n}\n
array.cs
/* 配列を走査 */\nvoid Traverse(int[] nums) {\n    int count = 0;\n    // インデックスで配列を走査\n    for (int i = 0; i < nums.Length; i++) {\n        count += nums[i];\n    }\n    // 配列要素を直接走査\n    foreach (int num in nums) {\n        count += num;\n    }\n}\n
array.go
/* 配列を走査 */\nfunc traverse(nums []int) {\n    count := 0\n    // インデックスで配列を走査\n    for i := 0; i < len(nums); i++ {\n        count += nums[i]\n    }\n    count = 0\n    // 配列要素を直接走査\n    for _, num := range nums {\n        count += num\n    }\n    // データのインデックスと要素を同時に走査する\n    for i, num := range nums {\n        count += nums[i]\n        count += num\n    }\n}\n
array.swift
/* 配列を走査 */\nfunc traverse(nums: [Int]) {\n    var count = 0\n    // インデックスで配列を走査\n    for i in nums.indices {\n        count += nums[i]\n    }\n    // 配列要素を直接走査\n    for num in nums {\n        count += num\n    }\n    // データのインデックスと要素を同時に走査する\n    for (i, num) in nums.enumerated() {\n        count += nums[i]\n        count += num\n    }\n}\n
array.js
/* 配列を走査 */\nfunction traverse(nums) {\n    let count = 0;\n    // インデックスで配列を走査\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // 配列要素を直接走査\n    for (const num of nums) {\n        count += num;\n    }\n}\n
array.ts
/* 配列を走査 */\nfunction traverse(nums: number[]): void {\n    let count = 0;\n    // インデックスで配列を走査\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // 配列要素を直接走査\n    for (const num of nums) {\n        count += num;\n    }\n}\n
array.dart
/* 配列要素を走査する */\nvoid traverse(List<int> nums) {\n  int count = 0;\n  // インデックスで配列を走査\n  for (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n  }\n  // 配列要素を直接走査\n  for (int _num in nums) {\n    count += _num;\n  }\n  // forEach メソッドで配列を走査する\n  nums.forEach((_num) {\n    count += _num;\n  });\n}\n
array.rs
/* 配列を走査 */\nfn traverse(nums: &[i32]) {\n    let mut _count = 0;\n    // インデックスで配列を走査\n    for i in 0..nums.len() {\n        _count += nums[i];\n    }\n    // 配列要素を直接走査\n    _count = 0;\n    for &num in nums {\n        _count += num;\n    }\n}\n
array.c
/* 配列を走査 */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // インデックスで配列を走査\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
array.kt
/* 配列を走査 */\nfun traverse(nums: IntArray) {\n    var count = 0\n    // インデックスで配列を走査\n    for (i in nums.indices) {\n        count += nums[i]\n    }\n    // 配列要素を直接走査\n    for (j in nums) {\n        count += j\n    }\n}\n
array.rb
### 配列を走査 ###\ndef traverse(nums)\n  count = 0\n\n  # インデックスで配列を走査\n  for i in 0...nums.length\n    count += nums[i]\n  end\n\n  # 配列要素を直接走査\n  for num in nums\n    count += num\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#6","level":3,"title":"6.   要素の検索","text":"

配列内で指定した要素を探すには、配列を走査し、各反復で要素値が一致するかを判定し、一致したら対応するインデックスを出力します。

配列は線形データ構造であるため、上記の検索操作は「線形探索」と呼ばれます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def find(nums: list[int], target: int) -> int:\n    \"\"\"配列内で指定要素を探す\"\"\"\n    for i in range(len(nums)):\n        if nums[i] == target:\n            return i\n    return -1\n
array.cpp
/* 配列内で指定要素を探す */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.java
/* 配列内で指定要素を探す */\nint find(int[] nums, int target) {\n    for (int i = 0; i < nums.length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.cs
/* 配列内で指定要素を探す */\nint Find(int[] nums, int target) {\n    for (int i = 0; i < nums.Length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.go
/* 配列内で指定要素を探す */\nfunc find(nums []int, target int) (index int) {\n    index = -1\n    for i := 0; i < len(nums); i++ {\n        if nums[i] == target {\n            index = i\n            break\n        }\n    }\n    return\n}\n
array.swift
/* 配列内で指定要素を探す */\nfunc find(nums: [Int], target: Int) -> Int {\n    for i in nums.indices {\n        if nums[i] == target {\n            return i\n        }\n    }\n    return -1\n}\n
array.js
/* 配列内で指定要素を探す */\nfunction find(nums, target) {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) return i;\n    }\n    return -1;\n}\n
array.ts
/* 配列内で指定要素を探す */\nfunction find(nums: number[], target: number): number {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) {\n            return i;\n        }\n    }\n    return -1;\n}\n
array.dart
/* 配列内で指定要素を探す */\nint find(List<int> nums, int target) {\n  for (var i = 0; i < nums.length; i++) {\n    if (nums[i] == target) return i;\n  }\n  return -1;\n}\n
array.rs
/* 配列内で指定要素を探す */\nfn find(nums: &[i32], target: i32) -> Option<usize> {\n    for i in 0..nums.len() {\n        if nums[i] == target {\n            return Some(i);\n        }\n    }\n    None\n}\n
array.c
/* 配列内で指定要素を探す */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.kt
/* 配列内で指定要素を探す */\nfun find(nums: IntArray, target: Int): Int {\n    for (i in nums.indices) {\n        if (nums[i] == target)\n            return i\n    }\n    return -1\n}\n
array.rb
### 配列内の指定要素を検索 ###\ndef find(nums, target)\n  for i in 0...nums.length\n    return i if nums[i] == target\n  end\n\n  -1\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#7","level":3,"title":"7.   配列の拡張","text":"

複雑なシステム環境では、配列の後方にあるメモリ領域が利用可能であることをプログラム側で保証できず、そのため安全に配列容量を拡張できません。したがって、ほとんどのプログラミング言語では、配列の長さは不変です。

配列を拡張したい場合は、より大きな新しい配列を作り、元の配列の要素を順に新配列へコピーする必要があります。これは \\(O(n)\\) の操作であり、配列が大きい場合は非常に時間がかかります。コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def extend(nums: list[int], enlarge: int) -> list[int]:\n    \"\"\"配列長を拡張する\"\"\"\n    # 拡張後の長さを持つ配列を初期化する\n    res = [0] * (len(nums) + enlarge)\n    # 元の配列の全要素を新しい配列にコピー\n    for i in range(len(nums)):\n        res[i] = nums[i]\n    # 拡張後の新しい配列を返す\n    return res\n
array.cpp
/* 配列長を拡張する */\nint *extend(int *nums, int size, int enlarge) {\n    // 拡張後の長さを持つ配列を初期化する\n    int *res = new int[size + enlarge];\n    // 元の配列の全要素を新しい配列にコピー\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // メモリを解放する\n    delete[] nums;\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.java
/* 配列長を拡張する */\nint[] extend(int[] nums, int enlarge) {\n    // 拡張後の長さを持つ配列を初期化する\n    int[] res = new int[nums.length + enlarge];\n    // 元の配列の全要素を新しい配列にコピー\n    for (int i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.cs
/* 配列長を拡張する */\nint[] Extend(int[] nums, int enlarge) {\n    // 拡張後の長さを持つ配列を初期化する\n    int[] res = new int[nums.Length + enlarge];\n    // 元の配列の全要素を新しい配列にコピー\n    for (int i = 0; i < nums.Length; i++) {\n        res[i] = nums[i];\n    }\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.go
/* 配列長を拡張する */\nfunc extend(nums []int, enlarge int) []int {\n    // 拡張後の長さを持つ配列を初期化する\n    res := make([]int, len(nums)+enlarge)\n    // 元の配列の全要素を新しい配列にコピー\n    for i, num := range nums {\n        res[i] = num\n    }\n    // 拡張後の新しい配列を返す\n    return res\n}\n
array.swift
/* 配列長を拡張する */\nfunc extend(nums: [Int], enlarge: Int) -> [Int] {\n    // 拡張後の長さを持つ配列を初期化する\n    var res = Array(repeating: 0, count: nums.count + enlarge)\n    // 元の配列の全要素を新しい配列にコピー\n    for i in nums.indices {\n        res[i] = nums[i]\n    }\n    // 拡張後の新しい配列を返す\n    return res\n}\n
array.js
/* 配列長を拡張する */\n// JavaScript の Array は動的配列であり、直接拡張できます\n// 学習しやすいよう、本関数では Array を長さ不変の配列として扱います\nfunction extend(nums, enlarge) {\n    // 拡張後の長さを持つ配列を初期化する\n    const res = new Array(nums.length + enlarge).fill(0);\n    // 元の配列の全要素を新しい配列にコピー\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.ts
/* 配列長を拡張する */\n// TypeScript の Array は動的配列であり、直接拡張できます\n// 学習しやすいよう、本関数では Array を長さ不変の配列として扱います\nfunction extend(nums: number[], enlarge: number): number[] {\n    // 拡張後の長さを持つ配列を初期化する\n    const res = new Array(nums.length + enlarge).fill(0);\n    // 元の配列の全要素を新しい配列にコピー\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.dart
/* 配列長を拡張する */\nList<int> extend(List<int> nums, int enlarge) {\n  // 拡張後の長さを持つ配列を初期化する\n  List<int> res = List.filled(nums.length + enlarge, 0);\n  // 元の配列の全要素を新しい配列にコピー\n  for (var i = 0; i < nums.length; i++) {\n    res[i] = nums[i];\n  }\n  // 拡張後の新しい配列を返す\n  return res;\n}\n
array.rs
/* 配列長を拡張する */\nfn extend(nums: &[i32], enlarge: usize) -> Vec<i32> {\n    // 拡張後の長さを持つ配列を初期化する\n    let mut res: Vec<i32> = vec![0; nums.len() + enlarge];\n    // 元の配列の全要素を新しい配列にコピー\n    res[0..nums.len()].copy_from_slice(nums);\n\n    // 拡張後の新しい配列を返す\n    res\n}\n
array.c
/* 配列長を拡張する */\nint *extend(int *nums, int size, int enlarge) {\n    // 拡張後の長さを持つ配列を初期化する\n    int *res = (int *)malloc(sizeof(int) * (size + enlarge));\n    // 元の配列の全要素を新しい配列にコピー\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // 拡張後の領域を初期化する\n    for (int i = size; i < size + enlarge; i++) {\n        res[i] = 0;\n    }\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.kt
/* 配列長を拡張する */\nfun extend(nums: IntArray, enlarge: Int): IntArray {\n    // 拡張後の長さを持つ配列を初期化する\n    val res = IntArray(nums.size + enlarge)\n    // 元の配列の全要素を新しい配列にコピー\n    for (i in nums.indices) {\n        res[i] = nums[i]\n    }\n    // 拡張後の新しい配列を返す\n    return res\n}\n
array.rb
### 配列長を拡張 ###\n# Ruby の Array は動的配列であり、直接拡張できます\n# 学習しやすいよう、本関数では Array を長さ不変の配列として扱います\ndef extend(nums, enlarge)\n  # 拡張後の長さを持つ配列を初期化する\n  res = Array.new(nums.length + enlarge, 0)\n\n  # 元の配列の全要素を新しい配列にコピー\n  for i in 0...nums.length\n    res[i] = nums[i]\n  end\n\n  # 拡張後の新しい配列を返す\n  res\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#412","level":2,"title":"4.1.2   配列の利点と限界","text":"

配列は連続したメモリ領域に格納され、要素の型も同一です。この方法には豊富な事前情報が含まれており、システムはそれらを利用してデータ構造の操作効率を最適化できます。

  • 空間効率が高い:配列はデータに連続したメモリブロックを割り当てるため、追加の構造オーバーヘッドが不要です。
  • ランダムアクセスをサポートする:配列では任意の要素に \\(O(1)\\) 時間でアクセスできます。
  • キャッシュ局所性:配列要素にアクセスする際、コンピュータはその要素だけでなく周囲のデータもキャッシュするため、高速キャッシュを利用して後続操作の実行速度を高められます。

連続領域への格納は諸刃の剣であり、次のような制約があります。

  • 挿入と削除の効率が低い:配列内の要素が多い場合、挿入や削除では大量の要素を移動する必要があります。
  • 長さが不変:配列は初期化後に長さが固定され、拡張するにはすべてのデータを新しい配列へコピーする必要があり、コストが大きくなります。
  • 空間の浪費:配列に割り当てたサイズが実際の必要量を上回る場合、余分な領域は無駄になります。
","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#413","level":2,"title":"4.1.3   配列の典型的な応用","text":"

配列は基礎的で一般的なデータ構造であり、さまざまなアルゴリズムで頻繁に使われるだけでなく、多様な複雑データ構造の実装にも利用できます。

  • ランダムアクセス:いくつかのサンプルをランダムに抽出したい場合、配列に格納してランダムな系列を生成し、インデックスに基づいてランダムサンプリングを行えます。
  • ソートと探索:配列はソートアルゴリズムと探索アルゴリズムで最もよく使われるデータ構造です。クイックソート、マージソート、二分探索などは主に配列上で行われます。
  • ルックアップテーブル:ある要素やその対応関係を高速に調べる必要がある場合、配列をルックアップテーブルとして使えます。たとえば文字から ASCII コードへの対応を実装したいなら、文字の ASCII コード値をインデックスとし、対応する要素を配列の対応位置に格納できます。
  • 機械学習:ニューラルネットワークでは、ベクトル、行列、テンソル間の線形代数演算が大量に使われ、これらのデータはいずれも配列の形で構築されます。配列はニューラルネットワークプログラミングで最もよく使われるデータ構造です。
  • データ構造の実装:配列はスタック、キュー、ハッシュテーブル、ヒープ、グラフなどのデータ構造の実装に利用できます。たとえば、グラフの隣接行列表現は実際には 2 次元配列です。
","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   連結リスト","text":"

メモリ空間はすべてのプログラムに共通の資源であり、複雑なシステム実行環境では、空きメモリがメモリの各所に散在している可能性があります。配列を格納するメモリ空間は連続していなければなりませんが、配列が非常に大きい場合、メモリはそのような大きな連続領域を提供できないことがあります。このとき、連結リストの柔軟性という利点が現れます。

連結リスト(linked list)は線形データ構造の一種であり、各要素はノードオブジェクトです。各ノードは「参照」によって接続されます。参照には次のノードのメモリアドレスが記録されており、これによって現在のノードから次のノードへアクセスできます。

連結リストの設計では、各ノードをメモリの各所に分散して格納でき、それらのメモリアドレスは連続している必要がありません。

図 4-5   連結リストの定義と格納方式

上図を見ると、連結リストの構成単位はノード(node)オブジェクトです。各ノードは 2 つのデータ、すなわちノードの「値」と次のノードを指す「参照」を含みます。

  • 連結リストの最初のノードを「先頭ノード」、最後のノードを「末尾ノード」と呼びます。
  • 末尾ノードが指す先は「空」であり、Java、C++、Python ではそれぞれ nullnullptrNone と表記します。
  • C、C++、Go、Rust などポインタをサポートする言語では、上記の「参照」は「ポインタ」に置き換えるべきです。

以下のコードが示すように、連結リストノード ListNode は値のほかに、追加で 1 つの参照(ポインタ)を保持する必要があります。そのため、同じデータ量であれば、連結リストは配列より多くのメモリ空間を消費します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class ListNode:\n    \"\"\"連結リストノードクラス\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val               # ノードの値\n        self.next: ListNode | None = None # 次のノードへの参照\n
/* 連結リストノード構造体 */\nstruct ListNode {\n    int val;         // ノードの値\n    ListNode *next;  // 次のノードへのポインタ\n    ListNode(int x) : val(x), next(nullptr) {}  // コンストラクタ\n};\n
/* 連結リストノードクラス */\nclass ListNode {\n    int val;        // ノードの値\n    ListNode next;  // 次のノードへの参照\n    ListNode(int x) { val = x; }  // コンストラクタ\n}\n
/* 連結リストノードクラス */\nclass ListNode(int x) {  //コンストラクタ\n    int val = x;         // ノードの値\n    ListNode? next;      // 次のノードへの参照\n}\n
/* 連結リストノード構造体 */\ntype ListNode struct {\n    Val  int       // ノードの値\n    Next *ListNode // 次のノードへのポインタ\n}\n\n// NewListNode コンストラクタ。新しい連結リストを作成する\nfunc NewListNode(val int) *ListNode {\n    return &ListNode{\n        Val:  val,\n        Next: nil,\n    }\n}\n
/* 連結リストノードクラス */\nclass ListNode {\n    var val: Int // ノードの値\n    var next: ListNode? // 次のノードへの参照\n\n    init(x: Int) { // コンストラクタ\n        val = x\n    }\n}\n
/* 連結リストノードクラス */\nclass ListNode {\n    constructor(val, next) {\n        this.val = (val === undefined ? 0 : val);       // ノードの値\n        this.next = (next === undefined ? null : next); // 次のノードへの参照\n    }\n}\n
/* 連結リストノードクラス */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    constructor(val?: number, next?: ListNode | null) {\n        this.val = val === undefined ? 0 : val;        // ノードの値\n        this.next = next === undefined ? null : next;  // 次のノードへの参照\n    }\n}\n
/* 連結リストノードクラス */\nclass ListNode {\n  int val; // ノードの値\n  ListNode? next; // 次のノードへの参照\n  ListNode(this.val, [this.next]); // コンストラクタ\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n/* 連結リストノードクラス */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // ノードの値\n    next: Option<Rc<RefCell<ListNode>>>, // 次のノードへのポインタ\n}\n
/* 連結リストノード構造体 */\ntypedef struct ListNode {\n    int val;               // ノードの値\n    struct ListNode *next; // 次のノードへのポインタ\n} ListNode;\n\n/* コンストラクタ */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    return node;\n}\n
/* 連結リストノードクラス */\n// コンストラクタ\nclass ListNode(x: Int) {\n    val _val: Int = x          // ノードの値\n    val next: ListNode? = null // 次のノードへの参照\n}\n
# 連結リストノードクラス\nclass ListNode\n  attr_accessor :val  # ノードの値\n  attr_accessor :next # 次のノードへの参照\n\n  def initialize(val=0, next_node=nil)\n    @val = val\n    @next = next_node\n  end\nend\n
","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#421","level":2,"title":"4.2.1   連結リストの基本操作","text":"","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#1","level":3,"title":"1.   連結リストの初期化","text":"

連結リストの構築は 2 つの手順に分かれます。第 1 に各ノードオブジェクトを初期化し、第 2 にノード間の参照関係を構築します。初期化が完了したら、連結リストの先頭ノードから出発し、参照で next をたどってすべてのノードに順にアクセスできます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
# 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化\n# 各ノードを初期化\nn0 = ListNode(1)\nn1 = ListNode(3)\nn2 = ListNode(2)\nn3 = ListNode(5)\nn4 = ListNode(4)\n# ノード間の参照を構築\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
linked_list.cpp
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nListNode* n0 = new ListNode(1);\nListNode* n1 = new ListNode(3);\nListNode* n2 = new ListNode(2);\nListNode* n3 = new ListNode(5);\nListNode* n4 = new ListNode(4);\n// ノード間の参照を構築\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
linked_list.java
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nListNode n0 = new ListNode(1);\nListNode n1 = new ListNode(3);\nListNode n2 = new ListNode(2);\nListNode n3 = new ListNode(5);\nListNode n4 = new ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.cs
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nListNode n0 = new(1);\nListNode n1 = new(3);\nListNode n2 = new(2);\nListNode n3 = new(5);\nListNode n4 = new(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.go
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nn0 := NewListNode(1)\nn1 := NewListNode(3)\nn2 := NewListNode(2)\nn3 := NewListNode(5)\nn4 := NewListNode(4)\n// ノード間の参照を構築\nn0.Next = n1\nn1.Next = n2\nn2.Next = n3\nn3.Next = n4\n
linked_list.swift
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nlet n0 = ListNode(x: 1)\nlet n1 = ListNode(x: 3)\nlet n2 = ListNode(x: 2)\nlet n3 = ListNode(x: 5)\nlet n4 = ListNode(x: 4)\n// ノード間の参照を構築\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
linked_list.js
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.ts
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.dart
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\\\n// 各ノードを初期化\nListNode n0 = ListNode(1);\nListNode n1 = ListNode(3);\nListNode n2 = ListNode(2);\nListNode n3 = ListNode(5);\nListNode n4 = ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.rs
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nlet n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));\nlet n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));\nlet n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));\nlet n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));\nlet n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));\n\n// ノード間の参照を構築\nn0.borrow_mut().next = Some(n1.clone());\nn1.borrow_mut().next = Some(n2.clone());\nn2.borrow_mut().next = Some(n3.clone());\nn3.borrow_mut().next = Some(n4.clone());\n
linked_list.c
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nListNode* n0 = newListNode(1);\nListNode* n1 = newListNode(3);\nListNode* n2 = newListNode(2);\nListNode* n3 = newListNode(5);\nListNode* n4 = newListNode(4);\n// ノード間の参照を構築\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
linked_list.kt
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nval n0 = ListNode(1)\nval n1 = ListNode(3)\nval n2 = ListNode(2)\nval n3 = ListNode(5)\nval n4 = ListNode(4)\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.rb
# 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化\n# 各ノードを初期化\nn0 = ListNode.new(1)\nn1 = ListNode.new(3)\nn2 = ListNode.new(2)\nn3 = ListNode.new(5)\nn4 = ListNode.new(4)\n# ノード間の参照を構築\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
可視化実行

https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

配列全体は 1 つの変数であり、たとえば配列 nums には nums[0]nums[1] などの要素が含まれます。一方、連結リストは複数の独立したノードオブジェクトで構成されます。通常、先頭ノードを連結リストの代名詞として扱います。たとえば上記のコードの連結リストは n0 と表せます。

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2","level":3,"title":"2.   ノードの挿入","text":"

連結リストへのノード挿入は非常に簡単です。下図に示すように、隣り合う 2 つのノード n0n1 の間に新しいノード P を挿入したいとします。このとき 2 つのノードの参照(ポインタ)を変更するだけでよく、時間計算量は \\(O(1)\\) です。

これに対して、配列に要素を挿入する時間計算量は \\(O(n)\\) であり、データ量が大きい場合の効率は低くなります。

図 4-6   連結リストへのノード挿入例

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def insert(n0: ListNode, P: ListNode):\n    \"\"\"連結リストでノード n0 の後ろにノード P を挿入する\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
linked_list.cpp
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
linked_list.java
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.cs
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.go
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
linked_list.swift
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
linked_list.js
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.ts
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunction insert(n0: ListNode, P: ListNode): void {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.dart
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
linked_list.rs
/* 連結リストでノード n0 の後ろにノード P を挿入する */\n#[allow(non_snake_case)]\npub fn insert<T>(n0: &Rc<RefCell<ListNode<T>>>, P: Rc<RefCell<ListNode<T>>>) {\n    let n1 = n0.borrow_mut().next.take();\n    P.borrow_mut().next = n1;\n    n0.borrow_mut().next = Some(P);\n}\n
linked_list.c
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
linked_list.kt
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
linked_list.rb
### 連結リストのノード n0 の後にノード _p を挿入 ###\n# Ruby の `p` は組み込み関数で、`P` は定数なので、代わりに `_p` を使える\ndef insert(n0, _p)\n  n1 = n0.next\n  _p.next = n1\n  n0.next = _p\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#3","level":3,"title":"3.   ノードの削除","text":"

下図に示すように、連結リストでのノード削除も非常に簡単で、1 つのノードの参照(ポインタ)を変更するだけで済みます。

なお、削除操作が完了した後もノード P は依然として n1 を指していますが、実際にはこの連結リストをたどっても P へはアクセスできません。つまり、P はすでにこの連結リストには属していません。

図 4-7   連結リストのノード削除

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def remove(n0: ListNode):\n    \"\"\"連結リストでノード n0 の直後のノードを削除する\"\"\"\n    if not n0.next:\n        return\n    # n0 -> P -> n1\n    P = n0.next\n    n1 = P.next\n    n0.next = n1\n
linked_list.cpp
/* 連結リストでノード n0 の直後のノードを削除する */\nvoid remove(ListNode *n0) {\n    if (n0->next == nullptr)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // メモリを解放する\n    delete P;\n}\n
linked_list.java
/* 連結リストでノード n0 の直後のノードを削除する */\nvoid remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.cs
/* 連結リストでノード n0 の直後のノードを削除する */\nvoid Remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode? n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.go
/* 連結リストでノード n0 の直後のノードを削除する */\nfunc removeItem(n0 *ListNode) {\n    if n0.Next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    P := n0.Next\n    n1 := P.Next\n    n0.Next = n1\n}\n
linked_list.swift
/* 連結リストでノード n0 の直後のノードを削除する */\nfunc remove(n0: ListNode) {\n    if n0.next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    let P = n0.next\n    let n1 = P?.next\n    n0.next = n1\n}\n
linked_list.js
/* 連結リストでノード n0 の直後のノードを削除する */\nfunction remove(n0) {\n    if (!n0.next) return;\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.ts
/* 連結リストでノード n0 の直後のノードを削除する */\nfunction remove(n0: ListNode): void {\n    if (!n0.next) {\n        return;\n    }\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.dart
/* 連結リストでノード n0 の直後のノードを削除する */\nvoid remove(ListNode n0) {\n  if (n0.next == null) return;\n  // n0 -> P -> n1\n  ListNode P = n0.next!;\n  ListNode? n1 = P.next;\n  n0.next = n1;\n}\n
linked_list.rs
/* 連結リストでノード n0 の直後のノードを削除する */\n#[allow(non_snake_case)]\npub fn remove<T>(n0: &Rc<RefCell<ListNode<T>>>) {\n    // n0 -> P -> n1\n    let P = n0.borrow_mut().next.take();\n    if let Some(node) = P {\n        let n1 = node.borrow_mut().next.take();\n        n0.borrow_mut().next = n1;\n    }\n}\n
linked_list.c
/* 連結リストでノード n0 の直後のノードを削除する */\n// 注意: stdio.h が remove 識別子を使用している\nvoid removeItem(ListNode *n0) {\n    if (!n0->next)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // メモリを解放する\n    free(P);\n}\n
linked_list.kt
/* 連結リストでノード n0 の直後のノードを削除する */\nfun remove(n0: ListNode?) {\n    if (n0?.next == null)\n        return\n    // n0 -> P -> n1\n    val p = n0.next\n    val n1 = p?.next\n    n0.next = n1\n}\n
linked_list.rb
### 連結リストのノード n0 の直後のノードを削除 ###\ndef remove(n0)\n  return if n0.next.nil?\n\n  # n0 -> remove_node -> n1\n  remove_node = n0.next\n  n1 = remove_node.next\n  n0.next = n1\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#4","level":3,"title":"4.   ノードへのアクセス","text":"

**連結リストでノードにアクセスする効率は低い**です。前節で述べたように、配列では任意の要素へ \\(O(1)\\) 時間でアクセスできます。これに対して連結リストでは、プログラムは先頭ノードから出発し、1 つずつ後ろへたどって目的のノードを見つける必要があります。つまり、連結リストの第 \\(i\\) ノードにアクセスするには \\(i - 1\\) 回のループが必要であり、時間計算量は \\(O(n)\\) です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def access(head: ListNode, index: int) -> ListNode | None:\n    \"\"\"連結リスト内で index 番目のノードにアクセス\"\"\"\n    for _ in range(index):\n        if not head:\n            return None\n        head = head.next\n    return head\n
linked_list.cpp
/* 連結リスト内で index 番目のノードにアクセス */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == nullptr)\n            return nullptr;\n        head = head->next;\n    }\n    return head;\n}\n
linked_list.java
/* 連結リスト内で index 番目のノードにアクセス */\nListNode access(ListNode head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.cs
/* 連結リスト内で index 番目のノードにアクセス */\nListNode? Access(ListNode? head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.go
/* 連結リスト内で index 番目のノードにアクセス */\nfunc access(head *ListNode, index int) *ListNode {\n    for i := 0; i < index; i++ {\n        if head == nil {\n            return nil\n        }\n        head = head.Next\n    }\n    return head\n}\n
linked_list.swift
/* 連結リスト内で index 番目のノードにアクセス */\nfunc access(head: ListNode, index: Int) -> ListNode? {\n    var head: ListNode? = head\n    for _ in 0 ..< index {\n        if head == nil {\n            return nil\n        }\n        head = head?.next\n    }\n    return head\n}\n
linked_list.js
/* 連結リスト内で index 番目のノードにアクセス */\nfunction access(head, index) {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.ts
/* 連結リスト内で index 番目のノードにアクセス */\nfunction access(head: ListNode | null, index: number): ListNode | null {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.dart
/* 連結リスト内で index 番目のノードにアクセス */\nListNode? access(ListNode? head, int index) {\n  for (var i = 0; i < index; i++) {\n    if (head == null) return null;\n    head = head.next;\n  }\n  return head;\n}\n
linked_list.rs
/* 連結リスト内で index 番目のノードにアクセス */\npub fn access<T>(head: Rc<RefCell<ListNode<T>>>, index: i32) -> Option<Rc<RefCell<ListNode<T>>>> {\n    fn dfs<T>(\n        head: Option<&Rc<RefCell<ListNode<T>>>>,\n        index: i32,\n    ) -> Option<Rc<RefCell<ListNode<T>>>> {\n        if index <= 0 {\n            return head.cloned();\n        }\n\n        if let Some(node) = head {\n            dfs(node.borrow().next.as_ref(), index - 1)\n        } else {\n            None\n        }\n    }\n\n    dfs(Some(head).as_ref(), index)\n}\n
linked_list.c
/* 連結リスト内で index 番目のノードにアクセス */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == NULL)\n            return NULL;\n        head = head->next;\n    }\n    return head;\n}\n
linked_list.kt
/* 連結リスト内で index 番目のノードにアクセス */\nfun access(head: ListNode?, index: Int): ListNode? {\n    var h = head\n    for (i in 0..<index) {\n        if (h == null)\n            return null\n        h = h.next\n    }\n    return h\n}\n
linked_list.rb
### 連結リスト内の index 番目のノードにアクセス ###\ndef access(head, index)\n  for i in 0...index\n    return nil if head.nil?\n    head = head.next\n  end\n\n  head\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#5","level":3,"title":"5.   ノードの探索","text":"

連結リストを走査し、その中から値が target のノードを探し、そのノードの連結リスト内でのインデックスを出力します。この処理も線形探索に属します。コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def find(head: ListNode, target: int) -> int:\n    \"\"\"連結リストで値が target の最初のノードを探す\"\"\"\n    index = 0\n    while head:\n        if head.val == target:\n            return index\n        head = head.next\n        index += 1\n    return -1\n
linked_list.cpp
/* 連結リストで値が target の最初のノードを探す */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head != nullptr) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.java
/* 連結リストで値が target の最初のノードを探す */\nint find(ListNode head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.cs
/* 連結リストで値が target の最初のノードを探す */\nint Find(ListNode? head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.go
/* 連結リストで値が target の最初のノードを探す */\nfunc findNode(head *ListNode, target int) int {\n    index := 0\n    for head != nil {\n        if head.Val == target {\n            return index\n        }\n        head = head.Next\n        index++\n    }\n    return -1\n}\n
linked_list.swift
/* 連結リストで値が target の最初のノードを探す */\nfunc find(head: ListNode, target: Int) -> Int {\n    var head: ListNode? = head\n    var index = 0\n    while head != nil {\n        if head?.val == target {\n            return index\n        }\n        head = head?.next\n        index += 1\n    }\n    return -1\n}\n
linked_list.js
/* 連結リストで値が target の最初のノードを探す */\nfunction find(head, target) {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
linked_list.ts
/* 連結リストで値が target の最初のノードを探す */\nfunction find(head: ListNode | null, target: number): number {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
linked_list.dart
/* 連結リストで値が target の最初のノードを探す */\nint find(ListNode? head, int target) {\n  int index = 0;\n  while (head != null) {\n    if (head.val == target) {\n      return index;\n    }\n    head = head.next;\n    index++;\n  }\n  return -1;\n}\n
linked_list.rs
/* 連結リストで値が target の最初のノードを探す */\npub fn find<T: PartialEq>(head: Rc<RefCell<ListNode<T>>>, target: T) -> i32 {\n    fn find<T: PartialEq>(head: Option<&Rc<RefCell<ListNode<T>>>>, target: T, idx: i32) -> i32 {\n        if let Some(node) = head {\n            if node.borrow().val == target {\n                return idx;\n            }\n            return find(node.borrow().next.as_ref(), target, idx + 1);\n        } else {\n            -1\n        }\n    }\n\n    find(Some(head).as_ref(), target, 0)\n}\n
linked_list.c
/* 連結リストで値が target の最初のノードを探す */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.kt
/* 連結リストで値が target の最初のノードを探す */\nfun find(head: ListNode?, target: Int): Int {\n    var index = 0\n    var h = head\n    while (h != null) {\n        if (h._val == target)\n            return index\n        h = h.next\n        index++\n    }\n    return -1\n}\n
linked_list.rb
### 連結リストで値が target の最初のノードを探す ###\ndef find(head, target)\n  index = 0\n  while head\n    return index if head.val == target\n    head = head.next\n    index += 1\n  end\n\n  -1\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#422-vs","level":2,"title":"4.2.2   配列 vs. 連結リスト","text":"

次の表は、配列と連結リストの各種特徴と操作効率をまとめたものです。両者は互いに逆の格納戦略を採用しているため、各種性質や操作効率にも対照的な特徴が現れます。

表 4-1   配列と連結リストの効率比較

配列 連結リスト 格納方式 連続したメモリ空間 分散したメモリ空間 容量拡張 長さは不変 柔軟に拡張可能 メモリ効率 要素のメモリ消費は少ないが、空間を無駄にする可能性がある 要素のメモリ消費が多い 要素へのアクセス \\(O(1)\\) \\(O(n)\\) 要素の追加 \\(O(n)\\) \\(O(1)\\) 要素の削除 \\(O(n)\\) \\(O(1)\\)","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#423","level":2,"title":"4.2.3   一般的な連結リストの種類","text":"

下図に示すように、一般的な連結リストの種類は 3 つあります。

  • 単方向連結リスト:前述した通常の連結リストのことです。単方向連結リストのノードは、値と次のノードを指す参照の 2 つのデータを含みます。最初のノードを先頭ノード、最後のノードを末尾ノードと呼び、末尾ノードは空 None を指します。
  • 循環連結リスト:単方向連結リストの末尾ノードを先頭ノードへ向けると(先頭と末尾をつなぐと)、循環連結リストが得られます。循環連結リストでは、任意のノードを先頭ノードとみなせます。
  • 双方向連結リスト:単方向連結リストと比べて、双方向連結リストは 2 方向の参照を記録します。双方向連結リストのノード定義には、後続ノード(次のノード)と前駆ノード(前のノード)を指す参照(ポインタ)が含まれます。単方向連結リストより柔軟で、2 方向に連結リストを走査できますが、そのぶん多くのメモリ空間を必要とします。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class ListNode:\n    \"\"\"双方向連結リストノードクラス\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # ノードの値\n        self.next: ListNode | None = None  # 後続ノードへの参照\n        self.prev: ListNode | None = None  # 前駆ノードへの参照\n
/* 双方向連結リストノード構造体 */\nstruct ListNode {\n    int val;         // ノードの値\n    ListNode *next;  // 後続ノードへのポインタ\n    ListNode *prev;  // 前駆ノードへのポインタ\n    ListNode(int x) : val(x), next(nullptr), prev(nullptr) {}  // コンストラクタ\n};\n
/* 双方向連結リストノードクラス */\nclass ListNode {\n    int val;        // ノードの値\n    ListNode next;  // 後続ノードへの参照\n    ListNode prev;  // 前駆ノードへの参照\n    ListNode(int x) { val = x; }  // コンストラクタ\n}\n
/* 双方向連結リストノードクラス */\nclass ListNode(int x) {  // コンストラクタ\n    int val = x;    // ノードの値\n    ListNode next;  // 後続ノードへの参照\n    ListNode prev;  // 前駆ノードへの参照\n}\n
/* 双方向連結リストノード構造体 */\ntype DoublyListNode struct {\n    Val  int             // ノードの値\n    Next *DoublyListNode // 後続ノードへのポインタ\n    Prev *DoublyListNode // 前駆ノードへのポインタ\n}\n\n// NewDoublyListNode の初期化\nfunc NewDoublyListNode(val int) *DoublyListNode {\n    return &DoublyListNode{\n        Val:  val,\n        Next: nil,\n        Prev: nil,\n    }\n}\n
/* 双方向連結リストノードクラス */\nclass ListNode {\n    var val: Int // ノードの値\n    var next: ListNode? // 後続ノードへの参照\n    var prev: ListNode? // 前駆ノードへの参照\n\n    init(x: Int) { // コンストラクタ\n        val = x\n    }\n}\n
/* 双方向連結リストノードクラス */\nclass ListNode {\n    constructor(val, next, prev) {\n        this.val = val  ===  undefined ? 0 : val;        // ノードの値\n        this.next = next  ===  undefined ? null : next;  // 後続ノードへの参照\n        this.prev = prev  ===  undefined ? null : prev;  // 前駆ノードへの参照\n    }\n}\n
/* 双方向連結リストノードクラス */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    prev: ListNode | null;\n    constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {\n        this.val = val  ===  undefined ? 0 : val;        // ノードの値\n        this.next = next  ===  undefined ? null : next;  // 後続ノードへの参照\n        this.prev = prev  ===  undefined ? null : prev;  // 前駆ノードへの参照\n    }\n}\n
/* 双方向連結リストノードクラス */\nclass ListNode {\n    int val;        // ノードの値\n    ListNode? next;  // 後続ノードへの参照\n    ListNode? prev;  // 前駆ノードへの参照\n    ListNode(this.val, [this.next, this.prev]);  // コンストラクタ\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 双方向連結リストノード型 */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // ノードの値\n    next: Option<Rc<RefCell<ListNode>>>, // 後続ノードへのポインタ\n    prev: Option<Rc<RefCell<ListNode>>>, // 前駆ノードへのポインタ\n}\n\n/* コンストラクタ */\nimpl ListNode {\n    fn new(val: i32) -> Self {\n        ListNode {\n            val,\n            next: None,\n            prev: None,\n        }\n    }\n}\n
/* 双方向連結リストノード構造体 */\ntypedef struct ListNode {\n    int val;               // ノードの値\n    struct ListNode *next; // 後続ノードへのポインタ\n    struct ListNode *prev; // 前駆ノードへのポインタ\n} ListNode;\n\n/* コンストラクタ */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    node->prev = NULL;\n    return node;\n}\n
/* 双方向連結リストノードクラス */\n// コンストラクタ\nclass ListNode(x: Int) {\n    val _val: Int = x           // ノードの値\n    val next: ListNode? = null  // 後続ノードへの参照\n    val prev: ListNode? = null  // 前駆ノードへの参照\n}\n
# 双方向連結リストノードクラス\nclass ListNode\n  attr_accessor :val    # ノードの値\n  attr_accessor :next   # 後続ノードへの参照\n  attr_accessor :prev   # 前駆ノードへの参照\n\n  def initialize(val=0, next_node=nil, prev_node=nil)\n    @val = val\n    @next = next_node\n    @prev = prev_node\n  end\nend\n

図 4-8   一般的な連結リストの種類

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#424","level":2,"title":"4.2.4   連結リストの典型的な応用","text":"

単方向連結リストは、スタック、キュー、ハッシュテーブル、グラフなどのデータ構造の実装によく用いられます。

  • スタックとキュー:挿入と削除の両方の操作を連結リストの一端で行うと、その性質は後入れ先出しとなり、スタックに対応します。挿入を連結リストの一端で行い、削除をもう一端で行うと、その性質は先入れ先出しとなり、キューに対応します。
  • ハッシュテーブル:連鎖アドレス法はハッシュ衝突を解決する主流の方式の 1 つであり、この方式では、衝突したすべての要素が 1 つの連結リストに格納されます。
  • グラフ:隣接リストはグラフを表現する一般的な方法の 1 つであり、グラフの各頂点は 1 つの連結リストに関連付けられます。連結リスト内の各要素は、その頂点に接続されたほかの頂点を表します。

双方向連結リストは、前後の要素をすばやく見つける必要がある場面でよく用いられます。

  • 高度なデータ構造:たとえば赤黒木や B 木では、ノードの親ノードへアクセスする必要があります。これは、ノード内に親ノードを指す参照を保持することで実現でき、双方向連結リストに似ています。
  • ブラウザ履歴:Web ブラウザでユーザーが進むボタンや戻るボタンをクリックしたとき、ブラウザはユーザーが訪れた前後のページを知る必要があります。双方向連結リストの性質によって、この操作は簡単になります。
  • LRU アルゴリズム:キャッシュ淘汰(LRU)アルゴリズムでは、最近最も使用されていないデータをすばやく見つける必要があり、さらにノードの高速な追加と削除も必要です。そのため、双方向連結リストが非常に適しています。

循環連結リストは、オペレーティングシステムのリソーススケジューリングのように、周期的な操作が必要な場面でよく用いられます。

  • ラウンドロビン時間片スケジューリングアルゴリズム:オペレーティングシステムにおいて、ラウンドロビン時間片スケジューリングは一般的な CPU スケジューリングアルゴリズムであり、一連のプロセスを循環的に処理する必要があります。各プロセスには 1 つの時間片が割り当てられ、その時間片を使い切ると、CPU は次のプロセスへ切り替わります。この循環操作は、循環連結リストで実現できます。
  • データバッファ:一部のデータバッファ実装でも、循環連結リストが使われることがあります。たとえば音声・動画プレーヤーでは、データストリームを複数のバッファブロックに分割して循環連結リストへ格納し、シームレス再生を実現できます。
","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   リスト","text":"

リスト(list)は抽象的なデータ構造の概念であり、要素の順序付き集合を表す。要素のアクセス、更新、追加、削除、走査などの操作をサポートし、利用者は容量制限の問題を考慮する必要がない。リストは連結リストまたは配列に基づいて実装できる。

  • 連結リストは本質的にリストと見なすことができ、要素の追加・削除・参照・更新をサポートし、柔軟に動的拡張できる。
  • 配列も要素の追加・削除・参照・更新をサポートするが、長さが不変であるため、長さ制限のあるリストとしか見なせない。

配列でリストを実装する場合、長さが不変である性質によってリストの実用性が低下する。これは、通常は事前にどれだけのデータを格納する必要があるかを決められず、適切なリスト長を選びにくいためである。長さが小さすぎると利用要件を満たせない可能性が高く、大きすぎるとメモリ空間の浪費を招く。

この問題を解決するために、動的配列(dynamic array)を用いてリストを実装できる。これは配列の各種利点を引き継ぎつつ、プログラム実行中に動的な拡張を行える。

実際には、多くのプログラミング言語の標準ライブラリが提供するリストは動的配列に基づいて実装されている。たとえば、Python の list 、Java の ArrayList 、C++ の vector 、C# の List などである。以降の議論では、「リスト」と「動的配列」を同じ概念として扱う。

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#431","level":2,"title":"4.3.1   リストの基本操作","text":"","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#1","level":3,"title":"1.   リストの初期化","text":"

通常は「初期値なし」と「初期値あり」の 2 つの初期化方法を用いる。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# リストを初期化\n# 初期値なし\nnums1: list[int] = []\n# 初期値あり\nnums: list[int] = [1, 3, 2, 5, 4]\n
list.cpp
/* リストを初期化 */\n// なお、C++ では vector が本稿でいう nums に相当する\n// 初期値なし\nvector<int> nums1;\n// 初期値あり\nvector<int> nums = { 1, 3, 2, 5, 4 };\n
list.java
/* リストを初期化 */\n// 初期値なし\nList<Integer> nums1 = new ArrayList<>();\n// 初期値あり(配列の要素型は int[] のラッパークラスである Integer[] である必要があることに注意)\nInteger[] numbers = new Integer[] { 1, 3, 2, 5, 4 };\nList<Integer> nums = new ArrayList<>(Arrays.asList(numbers));\n
list.cs
/* リストを初期化 */\n// 初期値なし\nList<int> nums1 = [];\n// 初期値あり\nint[] numbers = [1, 3, 2, 5, 4];\nList<int> nums = [.. numbers];\n
list_test.go
/* リストを初期化 */\n// 初期値なし\nnums1 := []int{}\n// 初期値あり\nnums := []int{1, 3, 2, 5, 4}\n
list.swift
/* リストを初期化 */\n// 初期値なし\nlet nums1: [Int] = []\n// 初期値あり\nvar nums = [1, 3, 2, 5, 4]\n
list.js
/* リストを初期化 */\n// 初期値なし\nconst nums1 = [];\n// 初期値あり\nconst nums = [1, 3, 2, 5, 4];\n
list.ts
/* リストを初期化 */\n// 初期値なし\nconst nums1: number[] = [];\n// 初期値あり\nconst nums: number[] = [1, 3, 2, 5, 4];\n
list.dart
/* リストを初期化 */\n// 初期値なし\nList<int> nums1 = [];\n// 初期値あり\nList<int> nums = [1, 3, 2, 5, 4];\n
list.rs
/* リストを初期化 */\n// 初期値なし\nlet nums1: Vec<i32> = Vec::new();\n// 初期値あり\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* リストを初期化 */\n// 初期値なし\nvar nums1 = listOf<Int>()\n// 初期値あり\nvar numbers = arrayOf(1, 3, 2, 5, 4)\nvar nums = numbers.toMutableList()\n
list.rb
# リストを初期化\n# 初期値なし\nnums1 = []\n# 初期値あり\nnums = [1, 3, 2, 5, 4]\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20%23%20%E6%97%A0%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#2","level":3,"title":"2.   要素へのアクセス","text":"

リストの本質は配列であるため、要素へのアクセスと更新は \\(O(1)\\) 時間で行え、効率が高い。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# 要素にアクセス\nnum: int = nums[1]  # インデックス 1 の要素にアクセス\n\n# 要素を更新\nnums[1] = 0    # インデックス 1 の要素を 0 に更新\n
list.cpp
/* 要素にアクセス */\nint num = nums[1];  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0;  // インデックス 1 の要素を 0 に更新\n
list.java
/* 要素にアクセス */\nint num = nums.get(1);  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums.set(1, 0);  // インデックス 1 の要素を 0 に更新\n
list.cs
/* 要素にアクセス */\nint num = nums[1];  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0;  // インデックス 1 の要素を 0 に更新\n
list_test.go
/* 要素にアクセス */\nnum := nums[1]  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0     // インデックス 1 の要素を 0 に更新\n
list.swift
/* 要素にアクセス */\nlet num = nums[1] // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0 // インデックス 1 の要素を 0 に更新\n
list.js
/* 要素にアクセス */\nconst num = nums[1];  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0;  // インデックス 1 の要素を 0 に更新\n
list.ts
/* 要素にアクセス */\nconst num: number = nums[1];  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0;  // インデックス 1 の要素を 0 に更新\n
list.dart
/* 要素にアクセス */\nint num = nums[1];  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0;  // インデックス 1 の要素を 0 に更新\n
list.rs
/* 要素にアクセス */\nlet num: i32 = nums[1];  // インデックス 1 の要素にアクセス\n/* 要素を更新 */\nnums[1] = 0;             // インデックス 1 の要素を 0 に更新\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* 要素にアクセス */\nval num = nums[1]       // インデックス 1 の要素にアクセス\n/* 要素を更新 */\nnums[1] = 0             // インデックス 1 の要素を 0 に更新\n
list.rb
# 要素にアクセス\nnum = nums[1] # インデックス 1 の要素にアクセス\n# 要素を更新\nnums[1] = 0 # インデックス 1 の要素を 0 に更新\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%E8%AE%BF%E9%97%AE%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%0A%0A%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%E5%B0%86%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%E6%9B%B4%E6%96%B0%E4%B8%BA%200&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#3","level":3,"title":"3.   要素の挿入と削除","text":"

配列と比べて、リストでは要素を自由に追加・削除できる。リスト末尾への要素追加の時間計算量は \\(O(1)\\) だが、要素の挿入と削除の効率は依然として配列と同じで、時間計算量は \\(O(n)\\) である。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# リストをクリア\nnums.clear()\n\n# 末尾に要素を追加\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n# 途中に要素を挿入\nnums.insert(3, 6)  # インデックス 3 に数値 6 を挿入\n\n# 要素を削除\nnums.pop(3)        # インデックス 3 の要素を削除\n
list.cpp
/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.push_back(1);\nnums.push_back(3);\nnums.push_back(2);\nnums.push_back(5);\nnums.push_back(4);\n\n/* 途中に要素を挿入 */\nnums.insert(nums.begin() + 3, 6);  // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.erase(nums.begin() + 3);      // インデックス 3 の要素を削除\n
list.java
/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 途中に要素を挿入 */\nnums.add(3, 6);  // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.remove(3);  // インデックス 3 の要素を削除\n
list.cs
/* リストをクリア */\nnums.Clear();\n\n/* 末尾に要素を追加 */\nnums.Add(1);\nnums.Add(3);\nnums.Add(2);\nnums.Add(5);\nnums.Add(4);\n\n/* 途中に要素を挿入 */\nnums.Insert(3, 6);  // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.RemoveAt(3);  // インデックス 3 の要素を削除\n
list_test.go
/* リストをクリア */\nnums = nil\n\n/* 末尾に要素を追加 */\nnums = append(nums, 1)\nnums = append(nums, 3)\nnums = append(nums, 2)\nnums = append(nums, 5)\nnums = append(nums, 4)\n\n/* 途中に要素を挿入 */\nnums = append(nums[:3], append([]int{6}, nums[3:]...)...) // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums = append(nums[:3], nums[4:]...) // インデックス 3 の要素を削除\n
list.swift
/* リストをクリア */\nnums.removeAll()\n\n/* 末尾に要素を追加 */\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n/* 途中に要素を挿入 */\nnums.insert(6, at: 3) // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.remove(at: 3) // インデックス 3 の要素を削除\n
list.js
/* リストをクリア */\nnums.length = 0;\n\n/* 末尾に要素を追加 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 途中に要素を挿入 */\nnums.splice(3, 0, 6); // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.splice(3, 1);  // インデックス 3 の要素を削除\n
list.ts
/* リストをクリア */\nnums.length = 0;\n\n/* 末尾に要素を追加 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 途中に要素を挿入 */\nnums.splice(3, 0, 6); // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.splice(3, 1);  // インデックス 3 の要素を削除\n
list.dart
/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 途中に要素を挿入 */\nnums.insert(3, 6); // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.removeAt(3); // インデックス 3 の要素を削除\n
list.rs
/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 途中に要素を挿入 */\nnums.insert(3, 6);  // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.remove(3);    // インデックス 3 の要素を削除\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 途中に要素を挿入 */\nnums.add(3, 6);  // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.remove(3);  // インデックス 3 の要素を削除\n
list.rb
# リストをクリア\nnums.clear\n\n# 末尾に要素を追加\nnums << 1\nnums << 3\nnums << 2\nnums << 5\nnums << 4\n\n# 途中に要素を挿入\nnums.insert(3, 6) # インデックス 3 に数値 6 を挿入\n\n# 要素を削除\nnums.delete_at(3) # インデックス 3 の要素を削除\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B8%85%E7%A9%BA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.clear%28%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E5%B0%BE%E9%83%A8%E6%B7%BB%E5%8A%A0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.append%281%29%0A%20%20%20%20nums.append%283%29%0A%20%20%20%20nums.append%282%29%0A%20%20%20%20nums.append%285%29%0A%20%20%20%20nums.append%284%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.insert%283,%206%29%20%20%23%20%E5%9C%A8%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E6%8F%92%E5%85%A5%E6%95%B0%E5%AD%97%206%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.pop%283%29%20%20%20%20%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#4","level":3,"title":"4.   リストの走査","text":"

配列と同様に、リストもインデックスに基づいて走査することも、各要素を直接走査することもできる。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# インデックスでリストを走査\ncount = 0\nfor i in range(len(nums)):\n    count += nums[i]\n\n# リストの要素を直接走査\nfor num in nums:\n    count += num\n
list.cpp
/* インデックスでリストを走査 */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums[i];\n}\n\n/* リストの要素を直接走査 */\ncount = 0;\nfor (int num : nums) {\n    count += num;\n}\n
list.java
/* インデックスでリストを走査 */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums.get(i);\n}\n\n/* リストの要素を直接走査 */\nfor (int num : nums) {\n    count += num;\n}\n
list.cs
/* インデックスでリストを走査 */\nint count = 0;\nfor (int i = 0; i < nums.Count; i++) {\n    count += nums[i];\n}\n\n/* リストの要素を直接走査 */\ncount = 0;\nforeach (int num in nums) {\n    count += num;\n}\n
list_test.go
/* インデックスでリストを走査 */\ncount := 0\nfor i := 0; i < len(nums); i++ {\n    count += nums[i]\n}\n\n/* リストの要素を直接走査 */\ncount = 0\nfor _, num := range nums {\n    count += num\n}\n
list.swift
/* インデックスでリストを走査 */\nvar count = 0\nfor i in nums.indices {\n    count += nums[i]\n}\n\n/* リストの要素を直接走査 */\ncount = 0\nfor num in nums {\n    count += num\n}\n
list.js
/* インデックスでリストを走査 */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* リストの要素を直接走査 */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
list.ts
/* インデックスでリストを走査 */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* リストの要素を直接走査 */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
list.dart
/* インデックスでリストを走査 */\nint count = 0;\nfor (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* リストの要素を直接走査 */\ncount = 0;\nfor (var num in nums) {\n    count += num;\n}\n
list.rs
// インデックスでリストを走査\nlet mut _count = 0;\nfor i in 0..nums.len() {\n    _count += nums[i];\n}\n\n// リストの要素を直接走査\n_count = 0;\nfor num in &nums {\n    _count += num;\n}\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* インデックスでリストを走査 */\nvar count = 0\nfor (i in nums.indices) {\n    count += nums[i]\n}\n\n/* リストの要素を直接走査 */\nfor (num in nums) {\n    count += num\n}\n
list.rb
# インデックスでリストを走査\ncount = 0\nfor i in 0...nums.length\n    count += nums[i]\nend\n\n# リストの要素を直接走査\ncount = 0\nfor num in nums\n    count += num\nend\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%80%9A%E8%BF%87%E7%B4%A2%E5%BC%95%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%0A%20%20%20%20%23%20%E7%9B%B4%E6%8E%A5%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#5","level":3,"title":"5.   リストの連結","text":"

新しいリスト nums1 が与えられたとき、それを元のリストの末尾に連結できる。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# 2 つのリストを連結\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1  # リスト nums1 を nums の後ろに連結\n
list.cpp
/* 2 つのリストを連結 */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// リスト nums1 を nums の後ろに連結\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n
list.java
/* 2 つのリストを連結 */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1);  // リスト nums1 を nums の後ろに連結\n
list.cs
/* 2 つのリストを連結 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1);  // リスト nums1 を nums の後ろに連結\n
list_test.go
/* 2 つのリストを連結 */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...)  // リスト nums1 を nums の後ろに連結\n
list.swift
/* 2 つのリストを連結 */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // リスト nums1 を nums の後ろに連結\n
list.js
/* 2 つのリストを連結 */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // リスト nums1 を nums の後ろに連結\n
list.ts
/* 2 つのリストを連結 */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // リスト nums1 を nums の後ろに連結\n
list.dart
/* 2 つのリストを連結 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1);  // リスト nums1 を nums の後ろに連結\n
list.rs
/* 2 つのリストを連結 */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* 2 つのリストを連結 */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1)  // リスト nums1 を nums の後ろに連結\n
list.rb
# 2 つのリストを連結\nnums1 = [6, 8, 7, 10, 9]\nnums += nums1\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8B%BC%E6%8E%A5%E4%B8%A4%E4%B8%AA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums1%20%3D%20%5B6,%208,%207,%2010,%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%E5%B0%86%E5%88%97%E8%A1%A8%20nums1%20%E6%8B%BC%E6%8E%A5%E5%88%B0%20nums%20%E4%B9%8B%E5%90%8E&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#6","level":3,"title":"6.   リストをソート","text":"

リストのソートが完了すると、配列系アルゴリズム問題でよく問われる「二分探索」や「両ポインタ」アルゴリズムを使えるようになる。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# リストをソート\nnums.sort()  # ソート後、リスト要素は小さい順に並ぶ\n
list.cpp
/* リストをソート */\nsort(nums.begin(), nums.end());  // ソート後、リスト要素は小さい順に並ぶ\n
list.java
/* リストをソート */\nCollections.sort(nums);  // ソート後、リスト要素は小さい順に並ぶ\n
list.cs
/* リストをソート */\nnums.Sort(); // ソート後、リスト要素は小さい順に並ぶ\n
list_test.go
/* リストをソート */\nsort.Ints(nums)  // ソート後、リスト要素は小さい順に並ぶ\n
list.swift
/* リストをソート */\nnums.sort() // ソート後、リスト要素は小さい順に並ぶ\n
list.js
/* リストをソート */\nnums.sort((a, b) => a - b);  // ソート後、リスト要素は小さい順に並ぶ\n
list.ts
/* リストをソート */\nnums.sort((a, b) => a - b);  // ソート後、リスト要素は小さい順に並ぶ\n
list.dart
/* リストをソート */\nnums.sort(); // ソート後、リスト要素は小さい順に並ぶ\n
list.rs
/* リストをソート */\nnums.sort(); // ソート後、リスト要素は小さい順に並ぶ\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* リストをソート */\nnums.sort() // ソート後、リスト要素は小さい順に並ぶ\n
list.rb
# リストをソート\nnums = nums.sort { |a, b| a <=> b } # ソート後、リスト要素は小さい順に並ぶ\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8E%92%E5%BA%8F%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.sort%28%29%20%20%23%20%E6%8E%92%E5%BA%8F%E5%90%8E%EF%BC%8C%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%E4%BB%8E%E5%B0%8F%E5%88%B0%E5%A4%A7%E6%8E%92%E5%88%97&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#432","level":2,"title":"4.3.2   リストの実装","text":"

多くのプログラミング言語にはリストが組み込まれており、たとえば Java、C++、Python などがある。それらの実装は比較的複雑で、初期容量や拡張倍率など各種パラメータの設定もよく考えられている。興味があればソースコードを参照して学べる。

リストの動作原理への理解を深めるため、ここでは簡易版のリストを実装し、以下の 3 つの設計ポイントを含める。

  • 初期容量:妥当な配列の初期容量を選ぶ。この例では 10 を初期容量として選ぶ。
  • 要素数の記録:size という変数を宣言して、現在のリスト要素数を記録し、要素の挿入と削除に応じてリアルタイムに更新する。この変数により、リスト末尾の位置を特定し、拡張が必要かどうかを判断できる。
  • 拡張機構:要素を挿入する時点でリスト容量がいっぱいなら、拡張が必要になる。まず拡張倍率に応じてより大きな配列を作成し、次に現在の配列の全要素を順に新しい配列へ移す。この例では、配列を毎回以前の 2 倍に拡張する。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_list.py
class MyList:\n    \"\"\"リストクラス\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self._capacity: int = 10  # リスト容量\n        self._arr: list[int] = [0] * self._capacity  # 配列(リスト要素を格納)\n        self._size: int = 0  # リストの長さ(現在の要素数)\n        self._extend_ratio: int = 2  # リスト拡張時の増加倍率\n\n    def size(self) -> int:\n        \"\"\"リストの長さを取得(現在の要素数)\"\"\"\n        return self._size\n\n    def capacity(self) -> int:\n        \"\"\"リスト容量を取得する\"\"\"\n        return self._capacity\n\n    def get(self, index: int) -> int:\n        \"\"\"要素にアクセス\"\"\"\n        # インデックスが範囲外なら例外を送出する。以下同様\n        if index < 0 or index >= self._size:\n            raise IndexError(\"インデックスが範囲外です\")\n        return self._arr[index]\n\n    def set(self, num: int, index: int):\n        \"\"\"要素を更新\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"インデックスが範囲外です\")\n        self._arr[index] = num\n\n    def add(self, num: int):\n        \"\"\"末尾に要素を追加\"\"\"\n        # 要素数が容量を超えると、拡張機構が発動する\n        if self.size() == self.capacity():\n            self.extend_capacity()\n        self._arr[self._size] = num\n        self._size += 1\n\n    def insert(self, num: int, index: int):\n        \"\"\"中間に要素を挿入\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"インデックスが範囲外です\")\n        # 要素数が容量を超えると、拡張機構が発動する\n        if self._size == self.capacity():\n            self.extend_capacity()\n        # index 以降の要素をすべて 1 つ後ろへずらす\n        for j in range(self._size - 1, index - 1, -1):\n            self._arr[j + 1] = self._arr[j]\n        self._arr[index] = num\n        # 要素数を更新\n        self._size += 1\n\n    def remove(self, index: int) -> int:\n        \"\"\"要素を削除\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"インデックスが範囲外です\")\n        num = self._arr[index]\n        # インデックス index より後の要素をすべて 1 つ前に移動する\n        for j in range(index, self._size - 1):\n            self._arr[j] = self._arr[j + 1]\n        # 要素数を更新\n        self._size -= 1\n        # 削除された要素を返す\n        return num\n\n    def extend_capacity(self):\n        \"\"\"リストの拡張\"\"\"\n        # 元の配列の `_extend_ratio` 倍の長さを持つ新しい配列を作成し、元の配列を新しい配列にコピーする\n        self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1)\n        # リストの容量を更新\n        self._capacity = len(self._arr)\n\n    def to_array(self) -> list[int]:\n        \"\"\"有効長のリストを返す\"\"\"\n        return self._arr[: self._size]\n
my_list.cpp
/* リストクラス */\nclass MyList {\n  private:\n    int *arr;             // 配列(リスト要素を格納)\n    int arrCapacity = 10; // リスト容量\n    int arrSize = 0;      // リストの長さ(現在の要素数)\n    int extendRatio = 2;   // リスト拡張時の増加倍率\n\n  public:\n    /* コンストラクタ */\n    MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* デストラクタメソッド */\n    ~MyList() {\n        delete[] arr;\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    int size() {\n        return arrSize;\n    }\n\n    /* リスト容量を取得する */\n    int capacity() {\n        return arrCapacity;\n    }\n\n    /* 要素にアクセス */\n    int get(int index) {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= size())\n            throw out_of_range(\"インデックスが範囲外\");\n        return arr[index];\n    }\n\n    /* 要素を更新 */\n    void set(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"インデックスが範囲外\");\n        arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    void add(int num) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size() == capacity())\n            extendCapacity();\n        arr[size()] = num;\n        // 要素数を更新\n        arrSize++;\n    }\n\n    /* 中間に要素を挿入 */\n    void insert(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"インデックスが範囲外\");\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size() == capacity())\n            extendCapacity();\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (int j = size() - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // 要素数を更新\n        arrSize++;\n    }\n\n    /* 要素を削除 */\n    int remove(int index) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"インデックスが範囲外\");\n        int num = arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (int j = index; j < size() - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // 要素数を更新\n        arrSize--;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    void extendCapacity() {\n        // 元の配列の `extendRatio` 倍の長さを持つ新しい配列を作成する\n        int newCapacity = capacity() * extendRatio;\n        int *tmp = arr;\n        arr = new int[newCapacity];\n        // 元の配列の全要素を新しい配列にコピー\n        for (int i = 0; i < size(); i++) {\n            arr[i] = tmp[i];\n        }\n        // メモリを解放する\n        delete[] tmp;\n        arrCapacity = newCapacity;\n    }\n\n    /* 出力用にリストを Vector に変換 */\n    vector<int> toVector() {\n        // 有効長の範囲内のリスト要素のみを変換\n        vector<int> vec(size());\n        for (int i = 0; i < size(); i++) {\n            vec[i] = arr[i];\n        }\n        return vec;\n    }\n};\n
my_list.java
/* リストクラス */\nclass MyList {\n    private int[] arr; // 配列(リスト要素を格納)\n    private int capacity = 10; // リスト容量\n    private int size = 0; // リストの長さ(現在の要素数)\n    private int extendRatio = 2; // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    public MyList() {\n        arr = new int[capacity];\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    public int size() {\n        return size;\n    }\n\n    /* リスト容量を取得する */\n    public int capacity() {\n        return capacity;\n    }\n\n    /* 要素にアクセス */\n    public int get(int index) {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n        return arr[index];\n    }\n\n    /* 要素を更新 */\n    public void set(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n        arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    public void add(int num) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size == capacity())\n            extendCapacity();\n        arr[size] = num;\n        // 要素数を更新\n        size++;\n    }\n\n    /* 中間に要素を挿入 */\n    public void insert(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size == capacity())\n            extendCapacity();\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (int j = size - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // 要素数を更新\n        size++;\n    }\n\n    /* 要素を削除 */\n    public int remove(int index) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n        int num = arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (int j = index; j < size - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // 要素数を更新\n        size--;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    public void extendCapacity() {\n        // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n        arr = Arrays.copyOf(arr, capacity() * extendRatio);\n        // リストの容量を更新\n        capacity = arr.length;\n    }\n\n    /* リストを配列に変換する */\n    public int[] toArray() {\n        int size = size();\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] arr = new int[size];\n        for (int i = 0; i < size; i++) {\n            arr[i] = get(i);\n        }\n        return arr;\n    }\n}\n
my_list.cs
/* リストクラス */\nclass MyList {\n    private int[] arr;           // 配列(リスト要素を格納)\n    private int arrCapacity = 10;    // リスト容量\n    private int arrSize = 0;         // リストの長さ(現在の要素数)\n    private readonly int extendRatio = 2;  // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    public MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    public int Size() {\n        return arrSize;\n    }\n\n    /* リスト容量を取得する */\n    public int Capacity() {\n        return arrCapacity;\n    }\n\n    /* 要素にアクセス */\n    public int Get(int index) {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"インデックスが範囲外です\");\n        return arr[index];\n    }\n\n    /* 要素を更新 */\n    public void Set(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"インデックスが範囲外です\");\n        arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    public void Add(int num) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        arr[arrSize] = num;\n        // 要素数を更新\n        arrSize++;\n    }\n\n    /* 中間に要素を挿入 */\n    public void Insert(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"インデックスが範囲外です\");\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (int j = arrSize - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // 要素数を更新\n        arrSize++;\n    }\n\n    /* 要素を削除 */\n    public int Remove(int index) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"インデックスが範囲外です\");\n        int num = arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (int j = index; j < arrSize - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // 要素数を更新\n        arrSize--;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    public void ExtendCapacity() {\n        // `arrCapacity * extendRatio` の長さを持つ配列を新規作成し、元の配列を新しい配列にコピーする\n        Array.Resize(ref arr, arrCapacity * extendRatio);\n        // リストの容量を更新\n        arrCapacity = arr.Length;\n    }\n\n    /* リストを配列に変換する */\n    public int[] ToArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] arr = new int[arrSize];\n        for (int i = 0; i < arrSize; i++) {\n            arr[i] = Get(i);\n        }\n        return arr;\n    }\n}\n
my_list.go
/* リストクラス */\ntype myList struct {\n    arrCapacity int\n    arr         []int\n    arrSize     int\n    extendRatio int\n}\n\n/* コンストラクタ */\nfunc newMyList() *myList {\n    return &myList{\n        arrCapacity: 10,              // リスト容量\n        arr:         make([]int, 10), // 配列(リスト要素を格納)\n        arrSize:     0,               // リストの長さ(現在の要素数)\n        extendRatio: 2,               // リスト拡張時の増加倍率\n    }\n}\n\n/* リストの長さを取得(現在の要素数) */\nfunc (l *myList) size() int {\n    return l.arrSize\n}\n\n/* リスト容量を取得する */\nfunc (l *myList) capacity() int {\n    return l.arrCapacity\n}\n\n/* 要素にアクセス */\nfunc (l *myList) get(index int) int {\n    // インデックスが範囲外なら例外を送出する。以下同様\n    if index < 0 || index >= l.arrSize {\n        panic(\"インデックスが範囲外です\")\n    }\n    return l.arr[index]\n}\n\n/* 要素を更新 */\nfunc (l *myList) set(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"インデックスが範囲外です\")\n    }\n    l.arr[index] = num\n}\n\n/* 末尾に要素を追加 */\nfunc (l *myList) add(num int) {\n    // 要素数が容量を超えると、拡張機構が発動する\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    l.arr[l.arrSize] = num\n    // 要素数を更新\n    l.arrSize++\n}\n\n/* 中間に要素を挿入 */\nfunc (l *myList) insert(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"インデックスが範囲外です\")\n    }\n    // 要素数が容量を超えると、拡張機構が発動する\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    // index 以降の要素をすべて 1 つ後ろへずらす\n    for j := l.arrSize - 1; j >= index; j-- {\n        l.arr[j+1] = l.arr[j]\n    }\n    l.arr[index] = num\n    // 要素数を更新\n    l.arrSize++\n}\n\n/* 要素を削除 */\nfunc (l *myList) remove(index int) int {\n    if index < 0 || index >= l.arrSize {\n        panic(\"インデックスが範囲外です\")\n    }\n    num := l.arr[index]\n    // インデックス index より後の要素をすべて 1 つ前に移動する\n    for j := index; j < l.arrSize-1; j++ {\n        l.arr[j] = l.arr[j+1]\n    }\n    // 要素数を更新\n    l.arrSize--\n    // 削除された要素を返す\n    return num\n}\n\n/* リストの拡張 */\nfunc (l *myList) extendCapacity() {\n    // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n    l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...)\n    // リストの容量を更新\n    l.arrCapacity = len(l.arr)\n}\n\n/* 有効長のリストを返す */\nfunc (l *myList) toArray() []int {\n    // 有効長の範囲内のリスト要素のみを変換\n    return l.arr[:l.arrSize]\n}\n
my_list.swift
/* リストクラス */\nclass MyList {\n    private var arr: [Int] // 配列(リスト要素を格納)\n    private var _capacity: Int // リスト容量\n    private var _size: Int // リストの長さ(現在の要素数)\n    private let extendRatio: Int // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    init() {\n        _capacity = 10\n        _size = 0\n        extendRatio = 2\n        arr = Array(repeating: 0, count: _capacity)\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    func size() -> Int {\n        _size\n    }\n\n    /* リスト容量を取得する */\n    func capacity() -> Int {\n        _capacity\n    }\n\n    /* 要素にアクセス */\n    func get(index: Int) -> Int {\n        // インデックスが範囲外ならエラーを投げる。以下同様\n        if index < 0 || index >= size() {\n            fatalError(\"インデックスが範囲外\")\n        }\n        return arr[index]\n    }\n\n    /* 要素を更新 */\n    func set(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"インデックスが範囲外\")\n        }\n        arr[index] = num\n    }\n\n    /* 末尾に要素を追加 */\n    func add(num: Int) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if size() == capacity() {\n            extendCapacity()\n        }\n        arr[size()] = num\n        // 要素数を更新\n        _size += 1\n    }\n\n    /* 中間に要素を挿入 */\n    func insert(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"インデックスが範囲外\")\n        }\n        // 要素数が容量を超えると、拡張機構が発動する\n        if size() == capacity() {\n            extendCapacity()\n        }\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for j in (index ..< size()).reversed() {\n            arr[j + 1] = arr[j]\n        }\n        arr[index] = num\n        // 要素数を更新\n        _size += 1\n    }\n\n    /* 要素を削除 */\n    @discardableResult\n    func remove(index: Int) -> Int {\n        if index < 0 || index >= size() {\n            fatalError(\"インデックスが範囲外\")\n        }\n        let num = arr[index]\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for j in index ..< (size() - 1) {\n            arr[j] = arr[j + 1]\n        }\n        // 要素数を更新\n        _size -= 1\n        // 削除された要素を返す\n        return num\n    }\n\n    /* リストの拡張 */\n    func extendCapacity() {\n        // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n        arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1))\n        // リストの容量を更新\n        _capacity = arr.count\n    }\n\n    /* リストを配列に変換する */\n    func toArray() -> [Int] {\n        Array(arr.prefix(size()))\n    }\n}\n
my_list.js
/* リストクラス */\nclass MyList {\n    #arr = new Array(); // 配列(リスト要素を格納)\n    #capacity = 10; // リスト容量\n    #size = 0; // リストの長さ(現在の要素数)\n    #extendRatio = 2; // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    constructor() {\n        this.#arr = new Array(this.#capacity);\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    size() {\n        return this.#size;\n    }\n\n    /* リスト容量を取得する */\n    capacity() {\n        return this.#capacity;\n    }\n\n    /* 要素にアクセス */\n    get(index) {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です');\n        return this.#arr[index];\n    }\n\n    /* 要素を更新 */\n    set(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です');\n        this.#arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    add(num) {\n        // 長さが容量に等しい場合は拡張が必要\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // 新しい要素をリストの末尾に追加する\n        this.#arr[this.#size] = num;\n        this.#size++;\n    }\n\n    /* 中間に要素を挿入 */\n    insert(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です');\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (let j = this.#size - 1; j >= index; j--) {\n            this.#arr[j + 1] = this.#arr[j];\n        }\n        // 要素数を更新\n        this.#arr[index] = num;\n        this.#size++;\n    }\n\n    /* 要素を削除 */\n    remove(index) {\n        if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です');\n        let num = this.#arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (let j = index; j < this.#size - 1; j++) {\n            this.#arr[j] = this.#arr[j + 1];\n        }\n        // 要素数を更新\n        this.#size--;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    extendCapacity() {\n        // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n        this.#arr = this.#arr.concat(\n            new Array(this.capacity() * (this.#extendRatio - 1))\n        );\n        // リストの容量を更新\n        this.#capacity = this.#arr.length;\n    }\n\n    /* リストを配列に変換する */\n    toArray() {\n        let size = this.size();\n        // 有効長の範囲内のリスト要素のみを変換\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
my_list.ts
/* リストクラス */\nclass MyList {\n    private arr: Array<number>; // 配列(リスト要素を格納)\n    private _capacity: number = 10; // リスト容量\n    private _size: number = 0; // リストの長さ(現在の要素数)\n    private extendRatio: number = 2; // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    constructor() {\n        this.arr = new Array(this._capacity);\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    public size(): number {\n        return this._size;\n    }\n\n    /* リスト容量を取得する */\n    public capacity(): number {\n        return this._capacity;\n    }\n\n    /* 要素にアクセス */\n    public get(index: number): number {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です');\n        return this.arr[index];\n    }\n\n    /* 要素を更新 */\n    public set(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です');\n        this.arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    public add(num: number): void {\n        // 長さが容量に等しい場合は拡張が必要\n        if (this._size === this._capacity) this.extendCapacity();\n        // 新しい要素をリストの末尾に追加する\n        this.arr[this._size] = num;\n        this._size++;\n    }\n\n    /* 中間に要素を挿入 */\n    public insert(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です');\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (this._size === this._capacity) {\n            this.extendCapacity();\n        }\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (let j = this._size - 1; j >= index; j--) {\n            this.arr[j + 1] = this.arr[j];\n        }\n        // 要素数を更新\n        this.arr[index] = num;\n        this._size++;\n    }\n\n    /* 要素を削除 */\n    public remove(index: number): number {\n        if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です');\n        let num = this.arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (let j = index; j < this._size - 1; j++) {\n            this.arr[j] = this.arr[j + 1];\n        }\n        // 要素数を更新\n        this._size--;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    public extendCapacity(): void {\n        // `size` の長さを持つ配列を新規作成し、元の配列を新しい配列にコピーする\n        this.arr = this.arr.concat(\n            new Array(this.capacity() * (this.extendRatio - 1))\n        );\n        // リストの容量を更新\n        this._capacity = this.arr.length;\n    }\n\n    /* リストを配列に変換する */\n    public toArray(): number[] {\n        let size = this.size();\n        // 有効長の範囲内のリスト要素のみを変換\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
my_list.dart
/* リストクラス */\nclass MyList {\n  late List<int> _arr; // 配列(リスト要素を格納)\n  int _capacity = 10; // リスト容量\n  int _size = 0; // リストの長さ(現在の要素数)\n  int _extendRatio = 2; // リスト拡張時の増加倍率\n\n  /* コンストラクタ */\n  MyList() {\n    _arr = List.filled(_capacity, 0);\n  }\n\n  /* リストの長さを取得(現在の要素数) */\n  int size() => _size;\n\n  /* リスト容量を取得する */\n  int capacity() => _capacity;\n\n  /* 要素にアクセス */\n  int get(int index) {\n    if (index >= _size) throw RangeError('インデックスが範囲外です');\n    return _arr[index];\n  }\n\n  /* 要素を更新 */\n  void set(int index, int _num) {\n    if (index >= _size) throw RangeError('インデックスが範囲外です');\n    _arr[index] = _num;\n  }\n\n  /* 末尾に要素を追加 */\n  void add(int _num) {\n    // 要素数が容量を超えると、拡張機構が発動する\n    if (_size == _capacity) extendCapacity();\n    _arr[_size] = _num;\n    // 要素数を更新\n    _size++;\n  }\n\n  /* 中間に要素を挿入 */\n  void insert(int index, int _num) {\n    if (index >= _size) throw RangeError('インデックスが範囲外です');\n    // 要素数が容量を超えると、拡張機構が発動する\n    if (_size == _capacity) extendCapacity();\n    // index 以降の要素をすべて 1 つ後ろへずらす\n    for (var j = _size - 1; j >= index; j--) {\n      _arr[j + 1] = _arr[j];\n    }\n    _arr[index] = _num;\n    // 要素数を更新\n    _size++;\n  }\n\n  /* 要素を削除 */\n  int remove(int index) {\n    if (index >= _size) throw RangeError('インデックスが範囲外です');\n    int _num = _arr[index];\n    // インデックス index より後の要素をすべて 1 つ前に移動する\n    for (var j = index; j < _size - 1; j++) {\n      _arr[j] = _arr[j + 1];\n    }\n    // 要素数を更新\n    _size--;\n    // 削除された要素を返す\n    return _num;\n  }\n\n  /* リストの拡張 */\n  void extendCapacity() {\n    // 元の配列の `_extendRatio` 倍の長さを持つ新しい配列を作成する\n    final _newNums = List.filled(_capacity * _extendRatio, 0);\n    // 元の配列を新しい配列にコピー\n    List.copyRange(_newNums, 0, _arr);\n    // `_arr` の参照を更新\n    _arr = _newNums;\n    // リストの容量を更新\n    _capacity = _arr.length;\n  }\n\n  /* リストを配列に変換する */\n  List<int> toArray() {\n    List<int> arr = [];\n    for (var i = 0; i < _size; i++) {\n      arr.add(get(i));\n    }\n    return arr;\n  }\n}\n
my_list.rs
/* リストクラス */\n#[allow(dead_code)]\nstruct MyList {\n    arr: Vec<i32>,       // 配列(リスト要素を格納)\n    capacity: usize,     // リスト容量\n    size: usize,         // リストの長さ(現在の要素数)\n    extend_ratio: usize, // リスト拡張時の増加倍率\n}\n\n#[allow(unused, unused_comparisons)]\nimpl MyList {\n    /* コンストラクタ */\n    pub fn new(capacity: usize) -> Self {\n        let mut vec = vec![0; capacity];\n        Self {\n            arr: vec,\n            capacity,\n            size: 0,\n            extend_ratio: 2,\n        }\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    pub fn size(&self) -> usize {\n        return self.size;\n    }\n\n    /* リスト容量を取得する */\n    pub fn capacity(&self) -> usize {\n        return self.capacity;\n    }\n\n    /* 要素にアクセス */\n    pub fn get(&self, index: usize) -> i32 {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if index >= self.size {\n            panic!(\"インデックスが範囲外です\")\n        };\n        return self.arr[index];\n    }\n\n    /* 要素を更新 */\n    pub fn set(&mut self, index: usize, num: i32) {\n        if index >= self.size {\n            panic!(\"インデックスが範囲外です\")\n        };\n        self.arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    pub fn add(&mut self, num: i32) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        self.arr[self.size] = num;\n        // 要素数を更新\n        self.size += 1;\n    }\n\n    /* 中間に要素を挿入 */\n    pub fn insert(&mut self, index: usize, num: i32) {\n        if index >= self.size() {\n            panic!(\"インデックスが範囲外です\")\n        };\n        // 要素数が容量を超えると、拡張機構が発動する\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for j in (index..self.size).rev() {\n            self.arr[j + 1] = self.arr[j];\n        }\n        self.arr[index] = num;\n        // 要素数を更新\n        self.size += 1;\n    }\n\n    /* 要素を削除 */\n    pub fn remove(&mut self, index: usize) -> i32 {\n        if index >= self.size() {\n            panic!(\"インデックスが範囲外です\")\n        };\n        let num = self.arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for j in index..self.size - 1 {\n            self.arr[j] = self.arr[j + 1];\n        }\n        // 要素数を更新\n        self.size -= 1;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    pub fn extend_capacity(&mut self) {\n        // 元の配列の extend_ratio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n        let new_capacity = self.capacity * self.extend_ratio;\n        self.arr.resize(new_capacity, 0);\n        // リストの容量を更新\n        self.capacity = new_capacity;\n    }\n\n    /* リストを配列に変換する */\n    pub fn to_array(&self) -> Vec<i32> {\n        // 有効長の範囲内のリスト要素のみを変換\n        let mut arr = Vec::new();\n        for i in 0..self.size {\n            arr.push(self.get(i));\n        }\n        arr\n    }\n}\n
my_list.c
/* リストクラス */\ntypedef struct {\n    int *arr;        // 配列(リスト要素を格納)\n    int capacity;    // リスト容量\n    int size;        // リストのサイズ\n    int extendRatio; // リストが拡張されるたびの倍率\n} MyList;\n\n/* コンストラクタ */\nMyList *newMyList() {\n    MyList *nums = malloc(sizeof(MyList));\n    nums->capacity = 10;\n    nums->arr = malloc(sizeof(int) * nums->capacity);\n    nums->size = 0;\n    nums->extendRatio = 2;\n    return nums;\n}\n\n/* デストラクタ */\nvoid delMyList(MyList *nums) {\n    free(nums->arr);\n    free(nums);\n}\n\n/* リストの長さを取得 */\nint size(MyList *nums) {\n    return nums->size;\n}\n\n/* リスト容量を取得する */\nint capacity(MyList *nums) {\n    return nums->capacity;\n}\n\n/* 要素にアクセス */\nint get(MyList *nums, int index) {\n    assert(index >= 0 && index < nums->size);\n    return nums->arr[index];\n}\n\n/* 要素を更新 */\nvoid set(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < nums->size);\n    nums->arr[index] = num;\n}\n\n/* 末尾に要素を追加 */\nvoid add(MyList *nums, int num) {\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // 容量を拡張\n    }\n    nums->arr[size(nums)] = num;\n    nums->size++;\n}\n\n/* 中間に要素を挿入 */\nvoid insert(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < size(nums));\n    // 要素数が容量を超えると、拡張機構が発動する\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // 容量を拡張\n    }\n    for (int i = size(nums); i > index; --i) {\n        nums->arr[i] = nums->arr[i - 1];\n    }\n    nums->arr[index] = num;\n    nums->size++;\n}\n\n/* 要素を削除 */\n// 注意: stdio.h が remove 識別子を使用している\nint removeItem(MyList *nums, int index) {\n    assert(index >= 0 && index < size(nums));\n    int num = nums->arr[index];\n    for (int i = index; i < size(nums) - 1; i++) {\n        nums->arr[i] = nums->arr[i + 1];\n    }\n    nums->size--;\n    return num;\n}\n\n/* リストの拡張 */\nvoid extendCapacity(MyList *nums) {\n    // 先に領域を確保する\n    int newCapacity = capacity(nums) * nums->extendRatio;\n    int *extend = (int *)malloc(sizeof(int) * newCapacity);\n    int *temp = nums->arr;\n\n    // 古いデータを新しいデータにコピー\n    for (int i = 0; i < size(nums); i++)\n        extend[i] = nums->arr[i];\n\n    // 古いデータを解放する\n    free(temp);\n\n    // 新しいデータに更新\n    nums->arr = extend;\n    nums->capacity = newCapacity;\n}\n\n/* 出力用にリストを Array に変換 */\nint *toArray(MyList *nums) {\n    return nums->arr;\n}\n
my_list.kt
/* リストクラス */\nclass MyList {\n    private var arr: IntArray = intArrayOf() // 配列(リスト要素を格納)\n    private var capacity: Int = 10 // リスト容量\n    private var size: Int = 0 // リストの長さ(現在の要素数)\n    private var extendRatio: Int = 2 // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    init {\n        arr = IntArray(capacity)\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    fun size(): Int {\n        return size\n    }\n\n    /* リスト容量を取得する */\n    fun capacity(): Int {\n        return capacity\n    }\n\n    /* 要素にアクセス */\n    fun get(index: Int): Int {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"インデックスが範囲外\")\n        return arr[index]\n    }\n\n    /* 要素を更新 */\n    fun set(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"インデックスが範囲外\")\n        arr[index] = num\n    }\n\n    /* 末尾に要素を追加 */\n    fun add(num: Int) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size == capacity())\n            extendCapacity()\n        arr[size] = num\n        // 要素数を更新\n        size++\n    }\n\n    /* 中間に要素を挿入 */\n    fun insert(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"インデックスが範囲外\")\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size == capacity())\n            extendCapacity()\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (j in size - 1 downTo index)\n            arr[j + 1] = arr[j]\n        arr[index] = num\n        // 要素数を更新\n        size++\n    }\n\n    /* 要素を削除 */\n    fun remove(index: Int): Int {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"インデックスが範囲外\")\n        val num = arr[index]\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (j in index..<size - 1)\n            arr[j] = arr[j + 1]\n        // 要素数を更新\n        size--\n        // 削除された要素を返す\n        return num\n    }\n\n    /* リストの拡張 */\n    fun extendCapacity() {\n        // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n        arr = arr.copyOf(capacity() * extendRatio)\n        // リストの容量を更新\n        capacity = arr.size\n    }\n\n    /* リストを配列に変換する */\n    fun toArray(): IntArray {\n        val size = size()\n        // 有効長の範囲内のリスト要素のみを変換\n        val arr = IntArray(size)\n        for (i in 0..<size) {\n            arr[i] = get(i)\n        }\n        return arr\n    }\n}\n
my_list.rb
### リストクラス ###\nclass MyList\n  attr_reader :size       # リストの長さを取得(現在の要素数)\n  attr_reader :capacity   # リスト容量を取得する\n\n  ### コンストラクタ ###\n  def initialize\n    @capacity = 10\n    @size = 0\n    @extend_ratio = 2\n    @arr = Array.new(capacity)\n  end\n\n  ### 要素にアクセス ###\n  def get(index)\n    # インデックスが範囲外なら例外を送出する。以下同様\n    raise IndexError, \"インデックスが範囲外です\" if index < 0 || index >= size\n    @arr[index]\n  end\n\n  ### 要素にアクセス ###\n  def set(index, num)\n    raise IndexError, \"インデックスが範囲外です\" if index < 0 || index >= size\n    @arr[index] = num\n  end\n\n  ### 末尾に要素を追加 ###\n  def add(num)\n    # 要素数が容量を超えると、拡張機構が発動する\n    extend_capacity if size == capacity\n    @arr[size] = num\n\n    # 要素数を更新\n    @size += 1\n  end\n\n  ### 途中に要素を挿入 ###\n  def insert(index, num)\n    raise IndexError, \"インデックスが範囲外です\" if index < 0 || index >= size\n\n    # 要素数が容量を超えると、拡張機構が発動する\n    extend_capacity if size == capacity\n\n    # index 以降の要素をすべて 1 つ後ろへずらす\n    for j in (size - 1).downto(index)\n      @arr[j + 1] = @arr[j]\n    end\n    @arr[index] = num\n\n    # 要素数を更新\n    @size += 1\n  end\n\n  ### 要素の削除 ###\n  def remove(index)\n    raise IndexError, \"インデックスが範囲外です\" if index < 0 || index >= size\n    num = @arr[index]\n\n    # インデックス index より後の要素をすべて 1 つ前に移動する\n    for j in index...size\n      @arr[j] = @arr[j + 1]\n    end\n\n    # 要素数を更新\n    @size -= 1\n\n    # 削除された要素を返す\n    num\n  end\n\n  ### リストの容量拡張 ###\n  def extend_capacity\n    # 元の配列の extend_ratio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n    arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1))\n    # リストの容量を更新\n    @capacity = arr.length\n  end\n\n  ### リストを配列に変換 ###\n  def to_array\n    sz = size\n    # 有効長の範囲内のリスト要素のみを変換\n    arr = Array.new(sz)\n    for i in 0...sz\n      arr[i] = get(i)\n    end\n    arr\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/","level":1,"title":"4.4   メモリとキャッシュ *","text":"

本章の前二節では、配列と連結リストという二つの基礎的かつ重要なデータ構造を扱いました。これらはそれぞれ「連続格納」と「分散格納」という二つの物理構造を表しています。

実際には、物理構造はプログラムにおけるメモリとキャッシュの利用効率を大きく左右し、ひいてはアルゴリズムプログラム全体の性能に影響します。

","path":["第 4 章   配列と連結リスト","4.4   メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#441","level":2,"title":"4.4.1   コンピュータの記憶装置","text":"

コンピュータには三種類の記憶装置があります。ハードディスク(hard disk)、メモリ(random-access memory, RAM)、キャッシュ(cache memory)です。以下の表は、これらがコンピュータシステムで担う役割と性能上の特徴を示しています。

表 4-2   コンピュータの記憶装置

ハードディスク メモリ キャッシュ 用途 OS、プログラム、ファイルなどを長期保存 実行中のプログラムや処理中のデータを一時保存 頻繁にアクセスされるデータや命令を保存し、CPU のメモリアクセス回数を減らす 揮発性 電源断後もデータは失われない 電源断後にデータは失われる 電源断後にデータは失われる 容量 大きい、TB 級 小さい、GB 級 非常に小さい、MB 級 速度 遅い、数百〜数千 MB/s 速い、数十 GB/s 非常に速い、数十〜数百 GB/s 価格(人民元) 比較的安価、数角〜数元 / GB 比較的高価、数十〜数百元 / GB 非常に高価、CPU と一体で価格設定される

コンピュータの記憶システムは、下図のようなピラミッド構造として捉えられます。ピラミッドの頂点に近い記憶装置ほど速度は速く、容量は小さく、コストは高くなります。この多層構造は偶然ではなく、コンピュータ科学者やエンジニアによる熟慮の末の設計です。

  • ハードディスクはメモリで置き換えにくい。まず、メモリ内のデータは電源断後に失われるため、長期保存には向きません。次に、メモリのコストはハードディスクの数十倍であり、消費者市場で広く普及しにくいという問題があります。
  • キャッシュは大容量と高速性を両立しにくい。L1、L2、L3 キャッシュの容量が段階的に増えるにつれて、物理サイズは大きくなり、CPU コアとの物理的距離も遠くなります。その結果、データ転送時間が増え、要素アクセスの遅延も大きくなります。現在の技術では、多層キャッシュ構造が容量、速度、コストの最適なバランスです。

図 4-9   コンピュータの記憶システム

Tip

コンピュータの記憶階層は、速度、容量、コストの三者間にある巧妙なバランスを体現しています。実際、このようなトレードオフはあらゆる工業分野に広く存在しており、異なる利点と制約のあいだで最適な均衡点を見つけることが求められます。

要するに、ハードディスクは大量データの長期保存に、メモリはプログラム実行中に処理しているデータの一時保存に、キャッシュは頻繁にアクセスされるデータや命令の保存に用いられ、プログラム実行効率を高めます。三者は協調して動作し、コンピュータシステムの高効率な運用を支えています。

次の図に示すように、プログラム実行時にはデータがハードディスクからメモリへ読み込まれ、CPU の計算に使われます。キャッシュは CPU の一部と見なせ、メモリからデータを賢く読み込むことで、CPU に高速なデータ読み出しを提供し、プログラムの実行効率を大きく高め、低速なメモリへの依存を減らします。

図 4-10   ハードディスク、メモリ、キャッシュ間のデータの流れ

","path":["第 4 章   配列と連結リスト","4.4   メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#442","level":2,"title":"4.4.2   データ構造のメモリ効率","text":"

メモリ空間の利用という観点では、配列と連結リストにはそれぞれ利点と制約があります。

一方で、メモリは有限であり、同じメモリ領域を複数のプログラムで共有することはできません。そのため、データ構造にはできるだけ効率よく空間を使うことが求められます。配列の要素は密に並んでおり、連結リストのノード間参照(ポインタ)を保持する追加領域が不要なため、空間効率は高くなります。しかし、配列は十分な連続メモリを一度に確保する必要があり、メモリ浪費を招くことがありますし、拡張時にも追加の時間と空間コストがかかります。これに対して連結リストは「ノード」単位で動的にメモリを割り当て・解放でき、より高い柔軟性を備えています。

他方で、プログラムの実行中には、メモリの確保と解放を繰り返すにつれて、空きメモリの断片化はますます進み、メモリ利用効率の低下を招きます。配列は連続した格納方式を取るため、比較的メモリ断片化を起こしにくい構造です。反対に、連結リストの要素は分散して格納されるため、頻繁な挿入や削除を行うと、より断片化を招きやすくなります。

","path":["第 4 章   配列と連結リスト","4.4   メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#443","level":2,"title":"4.4.3   データ構造のキャッシュ効率","text":"

キャッシュは容量こそメモリよりはるかに小さいものの、速度はメモリよりずっと速く、プログラム実行速度において極めて重要な役割を果たします。キャッシュ容量には限りがあり、頻繁にアクセスされる一部のデータしか保持できません。そのため、CPU がアクセスしようとするデータがキャッシュ内に存在しない場合、キャッシュミス(cache miss)が発生し、CPU は低速なメモリから必要なデータを読み込まなければなりません。

当然ながら、「キャッシュミス」が少ないほど、CPU のデータ読み書き効率は高くなり、プログラム性能も向上します。CPU がキャッシュからデータを正常に取得できた割合をキャッシュヒット率(cache hit rate)と呼び、この指標は通常、キャッシュ効率の評価に用いられます。

できるだけ高い効率を実現するため、キャッシュは次のようなデータ読み込みの仕組みを採用しています。

  • キャッシュライン:キャッシュはデータを 1 バイト単位で保存・読み込みするのではなく、キャッシュライン単位で扱います。1 バイト単位の転送と比べて、キャッシュライン単位のほうが効率的です。
  • プリフェッチ機構:プロセッサはデータアクセスのパターン(たとえば順次アクセス、一定ステップ幅のスキップアクセスなど)を予測し、そのパターンに応じてデータをキャッシュへ読み込むことで、ヒット率を高めます。
  • 空間的局所性:あるデータがアクセスされた場合、その近傍のデータも近いうちにアクセスされる可能性があります。そのため、キャッシュはあるデータを読み込む際に、その周辺のデータもあわせて読み込み、ヒット率を高めます。
  • 時間的局所性:あるデータがアクセスされた場合、そのデータは近い将来に再びアクセスされる可能性が高いです。キャッシュはこの性質を利用し、最近アクセスしたデータを保持することでヒット率を高めます。

実際には、配列と連結リストではキャッシュの利用効率が異なり、主に次の点に表れます。

  • 使用空間:連結リストの要素は配列要素より多くの空間を占めるため、キャッシュに収まる有効データ量は少なくなります。
  • キャッシュライン:連結リストのデータはメモリの各所に分散しており、キャッシュは「ライン単位で読み込む」ため、無効データまで読み込む割合が高くなります。
  • プリフェッチ機構:配列のほうが連結リストよりもデータアクセスのパターンを「予測しやすく」、システムが次に読み込まれるデータを推測しやすくなります。
  • 空間的局所性:配列はまとまったメモリ空間に格納されるため、読み込まれたデータの近くにあるデータも、まもなくアクセスされる可能性が高くなります。

全体として、配列はより高いキャッシュヒット率を持つため、操作効率では通常、連結リストより優れています。このため、アルゴリズム問題を解く際には、配列ベースで実装されたデータ構造のほうが好まれることが多くなります。

注意すべきなのは、**キャッシュ効率が高いからといって、配列があらゆる状況で連結リストより優れているとは限らない**という点です。実際にどのデータ構造を選ぶかは、具体的な要件に応じて決めるべきです。たとえば、配列と連結リストはいずれも「スタック」データ構造を実装できますが(次章で詳しく説明します)、適した場面は異なります。

  • アルゴリズム問題に取り組むときは、一般に配列ベースのスタックを選ぶ傾向があります。より高い操作効率とランダムアクセス能力を備えており、その代償は配列用に一定量のメモリを事前確保することだけです。
  • データ量が非常に大きく、動的性が高く、スタックの想定サイズを見積もりにくい場合は、連結リストベースのスタックのほうが適しています。連結リストなら大量のデータをメモリの異なる場所に分散して保存でき、配列拡張による追加コストも回避できます。
","path":["第 4 章   配列と連結リスト","4.4   メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/","level":1,"title":"4.5   まとめ","text":"","path":["第 4 章   配列と連結リスト","4.5   まとめ"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • 配列と連結リストは 2 種類の基本的なデータ構造であり、それぞれコンピュータメモリにおけるデータの 2 つの格納方式、すなわち連続領域への格納と分散領域への格納を表す。両者の特徴は相互補完的である。
  • 配列はランダムアクセスをサポートし、使用メモリも少ない。一方で、要素の挿入と削除の効率は低く、初期化後に長さを変更できない。
  • 連結リストは参照(ポインタ)を変更することでノードの挿入と削除を効率的に行え、長さも柔軟に調整できる。一方で、ノードへのアクセス効率は低く、メモリ使用量も多い。一般的な連結リストには単方向連結リスト、循環連結リスト、双方向連結リストがある。
  • リストは、追加・削除・検索・更新をサポートする順序付き要素集合であり、通常は動的配列に基づいて実装される。配列の利点を保ちながら、長さを柔軟に調整できる。
  • リストの登場により配列の実用性は大幅に高まったが、一部のメモリ領域が無駄になる可能性がある。
  • プログラムの実行時、データは主にメモリに格納される。配列はより高いメモリ空間効率を提供でき、連結リストはメモリ利用の面でより柔軟である。
  • キャッシュは、キャッシュライン、プリフェッチ機構、空間局所性と時間局所性といったデータ読み込み機構を通じて CPU に高速なデータアクセスを提供し、プログラムの実行効率を大きく向上させる。
  • 配列はキャッシュヒット率が高いため、通常は連結リストよりも高効率である。データ構造を選択する際は、具体的な要件や場面に応じて適切に選ぶべきである。
","path":["第 4 章   配列と連結リスト","4.5   まとめ"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:配列をスタックに格納する場合とヒープに格納する場合では、時間効率と空間効率に影響がありますか?

スタック上とヒープ上の配列はいずれも連続したメモリ領域に格納されるため、データ操作の効率は基本的に同じである。ただし、スタックとヒープにはそれぞれ特徴があり、以下の違いが生じる。

  1. 確保と解放の効率:スタックは比較的小さなメモリ領域で、確保はコンパイラによって自動的に行われる。一方、ヒープメモリは相対的に大きく、コード内で動的に確保できる反面、断片化しやすい。そのため、ヒープ上での確保と解放は通常スタック上より遅い。
  2. サイズ制限:スタックメモリは比較的小さく、ヒープのサイズは一般に利用可能メモリに制限される。そのため、ヒープは大きな配列の格納により適している。
  3. 柔軟性:スタック上の配列サイズはコンパイル時に確定している必要があるが、ヒープ上の配列サイズは実行時に動的に決定できる。

Q:なぜ配列では同じ型の要素が求められるのに、連結リストでは同じ型であることが強調されないのですか?

連結リストはノードで構成され、ノード同士は参照(ポインタ)で接続されている。各ノードには intdoublestringobject など、異なる型のデータを格納できる。

これに対して、配列要素は同じ型でなければならない。そうでなければ、オフセットを計算して対応する要素位置を取得できないからである。たとえば、配列に intlong の 2 種類が同時に含まれていて、各要素がそれぞれ 4 バイトと 8 バイトを占める場合、配列内に 2 種類の「要素長」が存在するため、次の式ではオフセットを計算できない。

# 要素のメモリアドレス = 配列のメモリアドレス(先頭要素のメモリアドレス) + 要素長 * 要素インデックス\n

Q:ノード P を削除した後、P.nextNone に設定する必要はありますか?

P.next を変更しなくてもよい。この連結リストの観点では、先頭ノードから末尾ノードまでたどっても、もはや P に出会うことはない。つまり、ノード P はすでに連結リストから削除されており、この時点で P がどこを指していても、この連結リストには影響しない。

データ構造とアルゴリズム(問題を解くとき)の観点では、切り離さなくても問題はなく、プログラムのロジックが正しいことを保証すればよい。標準ライブラリの観点では、切り離したほうがより安全で、ロジックも明確である。切り離さない場合、削除されたノードが適切に回収されなかったとすると、後続ノードのメモリ回収に影響する可能性がある。

Q:連結リストでの挿入と削除の時間計算量は \\(O(1)\\) です。しかし、追加や削除の前には要素を探すのに \\(O(n)\\) の時間が必要です。では、なぜ時間計算量は \\(O(n)\\) ではないのですか?

要素を先に探してから削除するのであれば、時間計算量が \\(O(n)\\) であるのは確かである。しかし、連結リストの \\(O(1)\\) での追加・削除という利点は、ほかの用途で生かせる。たとえば、両端キューは連結リストで実装するのに適しており、先頭ノードと末尾ノードを常に指すポインタ変数を維持すれば、各挿入・削除操作はどれも \\(O(1)\\) になる。

Q:図「連結リストの定義と格納方式」で、薄青色のノードポインタ部分は 1 つのメモリアドレスを占めているのですか? それともノード値と半分ずつなのでしょうか?

この模式図は定性的な表現にすぎず、定量的な表現は具体的な状況に応じて分析する必要がある。

  • ノード値が占める領域は型によって異なり、たとえば intlongdouble、インスタンスオブジェクトなどがある。
  • ポインタ変数が占めるメモリ空間の大きさは、使用する OS やコンパイル環境によって異なり、多くは 8 バイトまたは 4 バイトである。

Q:リストの末尾への要素追加は常に \\(O(1)\\) ですか?

要素を追加する際にリスト長を超える場合は、先にリストを拡張してから追加する必要がある。システムは新しいメモリ領域を確保し、元のリストの全要素をそこへ移動するため、このとき時間計算量は \\(O(n)\\) になる。

Q:「リストの登場により配列の実用性は大きく向上したが、一部のメモリ空間が無駄になる可能性がある」というのは、容量、長さ、拡張倍率のような追加変数が占めるメモリのことですか?

ここでいう空間の無駄には主に 2 つの意味がある。一方では、リストには初期長が設定されるが、必ずしもそれだけ必要とは限らない。もう一方では、頻繁な拡張を防ぐため、拡張時には通常ある係数、たとえば \\(\\times 1.5\\) を掛ける。このため、多くの空きスロットが生じ、通常それらを完全に埋めることはできない。

Q:Python で n = [1, 2, 3] を初期化した後、この 3 つの要素のアドレスは連続しています。しかし m = [2, 1, 3] を初期化すると、各要素の id は連続しておらず、それぞれ n 内の同じ値と一致していることがわかります。これらの要素のアドレスが連続していないなら、m も配列なのですか?

仮にリスト要素を連結リストのノード n = [n1, n2, n3, n4, n5] に置き換えたとしても、通常この 5 つのノードオブジェクトもメモリ上の各所に分散して格納される。それでも、与えられたリストインデックスに対して、私たちは依然として \\(O(1)\\) 時間でノードのメモリアドレスを取得し、対応するノードにアクセスできる。これは、配列に格納されているのがノードそのものではなく、ノードへの参照だからである。

多くの言語と異なり、Python では数値もオブジェクトとしてラップされており、リストに格納されているのは数値そのものではなく、数値への参照である。そのため、2 つの配列内の同じ数値が同一の id を持つことがあり、しかもそれらの数値のメモリアドレスは連続している必要がない。

Q:C++ STL の std::list はすでに双方向連結リストを実装していますが、アルゴリズム本ではあまり直接使われないようです。何か制約があるのでしょうか?

一方では、私たちは多くの場合、アルゴリズムの実装に配列を好み、必要なときにだけ連結リストを使う。その主な理由は 2 つある。

  • 空間オーバーヘッド:各要素には 2 つの追加ポインタ(前の要素用と次の要素用)が必要なため、std::list は通常 std::vector より多くの空間を消費する。
  • キャッシュ非効率:データが連続して格納されていないため、std::list はキャッシュの利用効率が低い。一般には、std::vector のほうが性能がよい。

もう一方では、連結リストを使う必要がある代表的な場面は主に二分木とグラフである。スタックやキューについては、連結リストではなく、たいてい言語が提供する stackqueue を使う。

Q:res = [[0]] * n という操作で 2 次元リストを生成した場合、それぞれの [0] は独立していますか?

独立していない。この 2 次元リストでは、すべての [0] は実際には同一オブジェクトへの参照である。そのうちの 1 つを変更すると、対応するすべての要素が一緒に変化することがわかる。

2 次元リスト内の各 [0] を独立させたい場合は、res = [[0] for _ in range(n)] を使って実現できる。この方式の原理は、独立した [0] リストオブジェクトを \\(n\\) 個初期化していることにある。

Q:res = [0] * n という操作で生成されたリストでは、それぞれの整数 0 は独立していますか?

このリストでは、すべての整数 0 が同一オブジェクトへの参照である。これは、Python が小さな整数(通常は -5 から 256)に対してキャッシュプール機構を採用し、オブジェクトの再利用を最大化して性能を向上させているためである。

それらは同じオブジェクトを指しているが、それでもリスト内の各要素は独立して変更できる。これは、Python の整数が「イミュータブルオブジェクト」だからである。ある要素を変更するとき、実際には別のオブジェクトへの参照に切り替わるのであって、元のオブジェクトそのものを変更しているわけではない。

しかし、リスト要素が「ミュータブルオブジェクト」(たとえばリスト、辞書、クラスインスタンスなど)である場合は、ある要素を変更するとそのオブジェクト自体が直接変更され、そのオブジェクトを参照しているすべての要素に同じ変化が生じる。

","path":["第 4 章   配列と連結リスト","4.5   まとめ"],"tags":[]},{"location":"chapter_backtracking/","level":1,"title":"第 13 章   バックトラッキング","text":"

Abstract

私たちは迷宮の探検者のように、前へ進む道で困難に出会うことがあります。

バックトラッキングの力によってやり直しができ、試行を重ね、最後には光へ通じる出口を見つけられます。

","path":["第 13 章   バックトラッキング"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"章の内容","text":"
  • 13.1   バックトラッキングアルゴリズム
  • 13.2   全順列問題
  • 13.3   部分和問題
  • 13.4   n クイーン問題
  • 13.5   まとめ
","path":["第 13 章   バックトラッキング"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   バックトラッキングアルゴリズム","text":"

バックトラッキングアルゴリズム(backtracking algorithm)は、総当たりによって問題を解く手法です。その中核となる考え方は、初期状態から出発し、あり得るすべての解を力任せに探索し、正しい解に到達したらそれを記録し、解を見つけるか、考えられるすべての選択を試しても解が見つからなくなるまで続ける、というものです。

バックトラッキングアルゴリズムでは、通常「深さ優先探索」を用いて解空間をたどります。「二分木」の章で述べたように、前順・中順・後順走査はいずれも深さ優先探索に属します。ここでは前順走査を使ってバックトラッキング問題を構成し、その仕組みを段階的に理解していきます。

例題1

1 本の二分木が与えられたとき、値が \\(7\\) のノードをすべて探索して記録し、そのノードのリストを返してください。

この問題では、この木を前順走査し、現在のノードの値が \\(7\\) かどうかを判定します。該当する場合は、そのノードの値を結果リスト res に追加します。関連する処理は下図と次のコードのとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
def pre_order(root: TreeNode):\n    \"\"\"前順走査:例題 1\"\"\"\n    if root is None:\n        return\n    if root.val == 7:\n        # 解を記録\n        res.append(root)\n    pre_order(root.left)\n    pre_order(root.right)\n
preorder_traversal_i_compact.cpp
/* 前順走査:例題 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    if (root->val == 7) {\n        // 解を記録\n        res.push_back(root);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
preorder_traversal_i_compact.java
/* 前順走査:例題 1 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // 解を記録\n        res.add(root);\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n}\n
preorder_traversal_i_compact.cs
/* 前順走査:例題 1 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // 解を記録\n        res.Add(root);\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n
preorder_traversal_i_compact.go
/* 前順走査:例題 1 */\nfunc preOrderI(root *TreeNode, res *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    if (root.Val).(int) == 7 {\n        // 解を記録\n        *res = append(*res, root)\n    }\n    preOrderI(root.Left, res)\n    preOrderI(root.Right, res)\n}\n
preorder_traversal_i_compact.swift
/* 前順走査:例題 1 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    if root.val == 7 {\n        // 解を記録\n        res.append(root)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n
preorder_traversal_i_compact.js
/* 前順走査:例題 1 */\nfunction preOrder(root, res) {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // 解を記録\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.ts
/* 前順走査:例題 1 */\nfunction preOrder(root: TreeNode | null, res: TreeNode[]): void {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // 解を記録\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.dart
/* 前順走査:例題 1 */\nvoid preOrder(TreeNode? root, List<TreeNode> res) {\n  if (root == null) {\n    return;\n  }\n  if (root.val == 7) {\n    // 解を記録\n    res.add(root);\n  }\n  preOrder(root.left, res);\n  preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.rs
/* 前順走査:例題 1 */\nfn pre_order(res: &mut Vec<Rc<RefCell<TreeNode>>>, root: Option<&Rc<RefCell<TreeNode>>>) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        if node.borrow().val == 7 {\n            // 解を記録\n            res.push(node.clone());\n        }\n        pre_order(res, node.borrow().left.as_ref());\n        pre_order(res, node.borrow().right.as_ref());\n    }\n}\n
preorder_traversal_i_compact.c
/* 前順走査:例題 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    if (root->val == 7) {\n        // 解を記録\n        res[resSize++] = root;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
preorder_traversal_i_compact.kt
/* 前順走査:例題 1 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    if (root._val == 7) {\n        // 解を記録\n        res!!.add(root)\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n}\n
preorder_traversal_i_compact.rb
### 前順走査:例題1 ###\ndef pre_order(root)\n  return unless root\n\n  # 解を記録\n  $res << root if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\nend\n
コードの可視化

全画面で見る >

図 13-1   前順走査でノードを探索する

","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1311","level":2,"title":"13.1.1   試行と戻る","text":"

バックトラッキングアルゴリズムと呼ばれるのは、解空間を探索する際に「試行」と「戻る」という戦略を取るためです。探索中に、ある状態から先へ進めない、または条件を満たす解を得られないと分かった場合、アルゴリズムは直前の選択を取り消して前の状態へ戻り、別の選択肢を試します。

例題1では、各ノードへの訪問が 1 回の「試行」に対応し、葉ノードを越えるか親ノードへ戻る return は「戻る」を表します。

ここで強調しておきたいのは、**戻るとは関数の return だけを指すわけではない**という点です。これを説明するために、例題1を少し拡張します。

例題2

二分木の中で値が \\(7\\) のノードをすべて探索し、根ノードからそれらのノードまでの経路を返してください。

例題1のコードを土台に、訪問済みノードの経路を記録するためのリスト path を導入します。値が \\(7\\) のノードに到達したら、path をコピーして結果リスト res に追加します。走査が完了すると、res にはすべての解が保存されています。コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
def pre_order(root: TreeNode):\n    \"\"\"前順走査:例題 2\"\"\"\n    if root is None:\n        return\n    # 試す\n    path.append(root)\n    if root.val == 7:\n        # 解を記録\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # バックトラック\n    path.pop()\n
preorder_traversal_ii_compact.cpp
/* 前順走査:例題 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    // 試す\n    path.push_back(root);\n    if (root->val == 7) {\n        // 解を記録\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // バックトラック\n    path.pop_back();\n}\n
preorder_traversal_ii_compact.java
/* 前順走査:例題 2 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    // 試す\n    path.add(root);\n    if (root.val == 7) {\n        // 解を記録\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // バックトラック\n    path.remove(path.size() - 1);\n}\n
preorder_traversal_ii_compact.cs
/* 前順走査:例題 2 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    // 試す\n    path.Add(root);\n    if (root.val == 7) {\n        // 解を記録\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // バックトラック\n    path.RemoveAt(path.Count - 1);\n}\n
preorder_traversal_ii_compact.go
/* 前順走査:例題 2 */\nfunc preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    // 試す\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // 解を記録\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderII(root.Left, res, path)\n    preOrderII(root.Right, res, path)\n    // バックトラック\n    *path = (*path)[:len(*path)-1]\n}\n
preorder_traversal_ii_compact.swift
/* 前順走査:例題 2 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 試す\n    path.append(root)\n    if root.val == 7 {\n        // 解を記録\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // バックトラック\n    path.removeLast()\n}\n
preorder_traversal_ii_compact.js
/* 前順走査:例題 2 */\nfunction preOrder(root, path, res) {\n    if (root === null) {\n        return;\n    }\n    // 試す\n    path.push(root);\n    if (root.val === 7) {\n        // 解を記録\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // バックトラック\n    path.pop();\n}\n
preorder_traversal_ii_compact.ts
/* 前順走査:例題 2 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    if (root === null) {\n        return;\n    }\n    // 試す\n    path.push(root);\n    if (root.val === 7) {\n        // 解を記録\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // バックトラック\n    path.pop();\n}\n
preorder_traversal_ii_compact.dart
/* 前順走査:例題 2 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null) {\n    return;\n  }\n\n  // 試す\n  path.add(root);\n  if (root.val == 7) {\n    // 解を記録\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // バックトラック\n  path.removeLast();\n}\n
preorder_traversal_ii_compact.rs
/* 前順走査:例題 2 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        // 試す\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // 解を記録\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // バックトラック\n        path.pop();\n    }\n}\n
preorder_traversal_ii_compact.c
/* 前順走査:例題 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    // 試す\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // 解を記録\n        for (int i = 0; i < pathSize; ++i) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // バックトラック\n    pathSize--;\n}\n
preorder_traversal_ii_compact.kt
/* 前順走査:例題 2 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    // 試す\n    path!!.add(root)\n    if (root._val == 7) {\n        // 解を記録\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // バックトラック\n    path!!.removeAt(path!!.size - 1)\n}\n
preorder_traversal_ii_compact.rb
### 前順走査:例題2 ###\ndef pre_order(root)\n  return unless root\n\n  # 試す\n  $path << root\n\n  # 解を記録\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # バックトラック\n  $path.pop\nend\n
コードの可視化

全画面で見る >

各「試行」で現在のノードを path に追加して経路を記録し、「戻る」前にはそのノードを path から取り除き、**今回の試行前の状態を復元する**必要があります。

次の図に示す過程を見ると、試行と戻るは「前進」と「取り消し」として理解できます。この 2 つの操作は互いに逆向きです。

<1><2><3><4><5><6><7><8><9><10><11>

図 13-2   試行と戻る

","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1312","level":2,"title":"13.1.2   枝刈り","text":"

複雑なバックトラッキング問題には、通常 1 つ以上の制約条件が含まれます。制約条件は多くの場合「枝刈り」に利用できます。

例題3

二分木の中で値が \\(7\\) のノードをすべて探索し、根ノードからそれらのノードまでの経路を返してください。ただし、経路には値が \\(3\\) のノードを含めてはいけません。

上の制約条件を満たすために、枝刈り操作を追加する必要があります。探索中に値が \\(3\\) のノードに出会った場合は、そこで早めに return し、それ以上探索を続けません。コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
def pre_order(root: TreeNode):\n    \"\"\"前順走査:例題 3\"\"\"\n    # 枝刈り\n    if root is None or root.val == 3:\n        return\n    # 試す\n    path.append(root)\n    if root.val == 7:\n        # 解を記録\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # バックトラック\n    path.pop()\n
preorder_traversal_iii_compact.cpp
/* 前順走査:例題 3 */\nvoid preOrder(TreeNode *root) {\n    // 枝刈り\n    if (root == nullptr || root->val == 3) {\n        return;\n    }\n    // 試す\n    path.push_back(root);\n    if (root->val == 7) {\n        // 解を記録\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // バックトラック\n    path.pop_back();\n}\n
preorder_traversal_iii_compact.java
/* 前順走査:例題 3 */\nvoid preOrder(TreeNode root) {\n    // 枝刈り\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // 試す\n    path.add(root);\n    if (root.val == 7) {\n        // 解を記録\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // バックトラック\n    path.remove(path.size() - 1);\n}\n
preorder_traversal_iii_compact.cs
/* 前順走査:例題 3 */\nvoid PreOrder(TreeNode? root) {\n    // 枝刈り\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // 試す\n    path.Add(root);\n    if (root.val == 7) {\n        // 解を記録\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // バックトラック\n    path.RemoveAt(path.Count - 1);\n}\n
preorder_traversal_iii_compact.go
/* 前順走査:例題 3 */\nfunc preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    // 枝刈り\n    if root == nil || root.Val == 3 {\n        return\n    }\n    // 試す\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // 解を記録\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderIII(root.Left, res, path)\n    preOrderIII(root.Right, res, path)\n    // バックトラック\n    *path = (*path)[:len(*path)-1]\n}\n
preorder_traversal_iii_compact.swift
/* 前順走査:例題 3 */\nfunc preOrder(root: TreeNode?) {\n    // 枝刈り\n    guard let root = root, root.val != 3 else {\n        return\n    }\n    // 試す\n    path.append(root)\n    if root.val == 7 {\n        // 解を記録\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // バックトラック\n    path.removeLast()\n}\n
preorder_traversal_iii_compact.js
/* 前順走査:例題 3 */\nfunction preOrder(root, path, res) {\n    // 枝刈り\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // 試す\n    path.push(root);\n    if (root.val === 7) {\n        // 解を記録\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // バックトラック\n    path.pop();\n}\n
preorder_traversal_iii_compact.ts
/* 前順走査:例題 3 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // 枝刈り\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // 試す\n    path.push(root);\n    if (root.val === 7) {\n        // 解を記録\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // バックトラック\n    path.pop();\n}\n
preorder_traversal_iii_compact.dart
/* 前順走査:例題 3 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null || root.val == 3) {\n    return;\n  }\n\n  // 試す\n  path.add(root);\n  if (root.val == 7) {\n    // 解を記録\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // バックトラック\n  path.removeLast();\n}\n
preorder_traversal_iii_compact.rs
/* 前順走査:例題 3 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    // 枝刈り\n    if root.is_none() || root.as_ref().unwrap().borrow().val == 3 {\n        return;\n    }\n    if let Some(node) = root {\n        // 試す\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // 解を記録\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // バックトラック\n        path.pop();\n    }\n}\n
preorder_traversal_iii_compact.c
/* 前順走査:例題 3 */\nvoid preOrder(TreeNode *root) {\n    // 枝刈り\n    if (root == NULL || root->val == 3) {\n        return;\n    }\n    // 試す\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // 解を記録\n        for (int i = 0; i < pathSize; i++) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // バックトラック\n    pathSize--;\n}\n
preorder_traversal_iii_compact.kt
/* 前順走査:例題 3 */\nfun preOrder(root: TreeNode?) {\n    // 枝刈り\n    if (root == null || root._val == 3) {\n        return\n    }\n    // 試す\n    path!!.add(root)\n    if (root._val == 7) {\n        // 解を記録\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // バックトラック\n    path!!.removeAt(path!!.size - 1)\n}\n
preorder_traversal_iii_compact.rb
### 前順走査:例題3 ###\ndef pre_order(root)\n  # 枝刈り\n  return if !root || root.val == 3\n\n  # 試す\n  $path.append(root)\n\n  # 解を記録\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # バックトラック\n  $path.pop\nend\n
コードの可視化

全画面で見る >

「枝刈り」は非常にイメージしやすい名称です。次の図のように、探索中に**制約条件を満たさない探索分岐を切り落とす**ことで、多くの無意味な試行を避け、探索効率を高められます。

図 13-3   制約条件にもとづく枝刈り

","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1313","level":2,"title":"13.1.3   フレームワークコード","text":"

次に、バックトラッキングにおける「試行・戻る・枝刈り」の本体部分を抽出し、汎用性の高いコードフレームワークへまとめてみます。

以下のフレームワークコードでは、state は問題の現在状態、choices はその状態で取り得る選択肢を表します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def backtrack(state: State, choices: list[choice], res: list[state]):\n    \"\"\"バックトラッキングアルゴリズムのフレームワーク\"\"\"\n    # 解かどうかを判定\n    if is_solution(state):\n        # 解を記録\n        record_solution(state, res)\n        # これ以上探索しない\n        return\n    # すべての選択肢を走査\n    for choice in choices:\n        # 枝刈り: 選択が妥当かを判定\n        if is_valid(state, choice):\n            # 試行: 選択を行い、状態を更新\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # 戻る: 選択を取り消し、前の状態に戻す\n            undo_choice(state, choice)\n
/* バックトラッキングアルゴリズムのフレームワーク */\nvoid backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for (Choice choice : choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nvoid backtrack(State state, List<Choice> choices, List<State> res) {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for (Choice choice : choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nvoid Backtrack(State state, List<Choice> choices, List<State> res) {\n    // 解かどうかを判定\n    if (IsSolution(state)) {\n        // 解を記録\n        RecordSolution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    foreach (Choice choice in choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (IsValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            MakeChoice(state, choice);\n            Backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            UndoChoice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfunc backtrack(state *State, choices []Choice, res *[]State) {\n    // 解かどうかを判定\n    if isSolution(state) {\n        // 解を記録\n        recordSolution(state, res)\n        // これ以上探索しない\n        return\n    }\n    // すべての選択肢を走査\n    for _, choice := range choices {\n        // 枝刈り: 選択が妥当かを判定\n        if isValid(state, choice) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice)\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfunc backtrack(state: inout State, choices: [Choice], res: inout [State]) {\n    // 解かどうかを判定\n    if isSolution(state: state) {\n        // 解を記録\n        recordSolution(state: state, res: &res)\n        // これ以上探索しない\n        return\n    }\n    // すべての選択肢を走査\n    for choice in choices {\n        // 枝刈り: 選択が妥当かを判定\n        if isValid(state: state, choice: choice) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state: &state, choice: choice)\n            backtrack(state: &state, choices: choices, res: &res)\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfunction backtrack(state, choices, res) {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for (let choice of choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfunction backtrack(state: State, choices: Choice[], res: State[]): void {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for (let choice of choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nvoid backtrack(State state, List<Choice>, List<State> res) {\n  // 解かどうかを判定\n  if (isSolution(state)) {\n    // 解を記録\n    recordSolution(state, res);\n    // これ以上探索しない\n    return;\n  }\n  // すべての選択肢を走査\n  for (Choice choice in choices) {\n    // 枝刈り: 選択が妥当かを判定\n    if (isValid(state, choice)) {\n      // 試行: 選択を行い、状態を更新\n      makeChoice(state, choice);\n      backtrack(state, choices, res);\n      // 戻る: 選択を取り消し、前の状態に戻す\n      undoChoice(state, choice);\n    }\n  }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {\n    // 解かどうかを判定\n    if is_solution(state) {\n        // 解を記録\n        record_solution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for choice in choices {\n        // 枝刈り: 選択が妥当かを判定\n        if is_valid(state, choice) {\n            // 試行: 選択を行い、状態を更新\n            make_choice(state, choice);\n            backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undo_choice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nvoid backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res, numRes);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < numChoices; i++) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, &choices[i])) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, &choices[i]);\n            backtrack(state, choices, numChoices, res, numRes);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, &choices[i]);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res)\n        // これ以上探索しない\n        return\n    }\n    // すべての選択肢を走査\n    for (choice in choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice)\n        }\n    }\n}\n
### バックトラッキングアルゴリズムのフレームワーク ###\ndef backtrack(state, choices, res)\n    # 解かどうかを判定\n    if is_solution?(state)\n        # 解を記録\n        record_solution(state, res)\n        return\n    end\n\n    # すべての選択肢を走査\n    for choice in choices\n        # 枝刈り: 選択が妥当かを判定\n        if is_valid?(state, choice)\n            # 試行: 選択を行い、状態を更新\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # 戻る: 選択を取り消し、前の状態に戻す\n            undo_choice(state, choice)\n        end\n    end\nend\n

次に、このフレームワークコードを用いて例題3を解きます。状態 state はノードの走査経路、選択肢 choices は現在のノードの左子ノードと右子ノード、結果 res は経路のリストです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_template.py
def is_solution(state: list[TreeNode]) -> bool:\n    \"\"\"現在の状態が解かどうかを判定\"\"\"\n    return state and state[-1].val == 7\n\ndef record_solution(state: list[TreeNode], res: list[list[TreeNode]]):\n    \"\"\"解を記録\"\"\"\n    res.append(list(state))\n\ndef is_valid(state: list[TreeNode], choice: TreeNode) -> bool:\n    \"\"\"現在の状態で、この選択が有効かどうかを判定\"\"\"\n    return choice is not None and choice.val != 3\n\ndef make_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"状態を更新\"\"\"\n    state.append(choice)\n\ndef undo_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"状態を元に戻す\"\"\"\n    state.pop()\n\ndef backtrack(\n    state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]]\n):\n    \"\"\"バックトラッキング:例題 3\"\"\"\n    # 解かどうかを確認\n    if is_solution(state):\n        # 解を記録\n        record_solution(state, res)\n    # すべての選択肢を走査\n    for choice in choices:\n        # 枝刈り:選択が妥当かを確認する\n        if is_valid(state, choice):\n            # 試行: 選択を行い、状態を更新\n            make_choice(state, choice)\n            # 次の選択へ進む\n            backtrack(state, [choice.left, choice.right], res)\n            # バックトラック:選択を取り消し、前の状態に戻す\n            undo_choice(state, choice)\n
preorder_traversal_iii_template.cpp
/* 現在の状態が解かどうかを判定 */\nbool isSolution(vector<TreeNode *> &state) {\n    return !state.empty() && state.back()->val == 7;\n}\n\n/* 解を記録 */\nvoid recordSolution(vector<TreeNode *> &state, vector<vector<TreeNode *>> &res) {\n    res.push_back(state);\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nbool isValid(vector<TreeNode *> &state, TreeNode *choice) {\n    return choice != nullptr && choice->val != 3;\n}\n\n/* 状態を更新 */\nvoid makeChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.push_back(choice);\n}\n\n/* 状態を元に戻す */\nvoid undoChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.pop_back();\n}\n\n/* バックトラッキング:例題 3 */\nvoid backtrack(vector<TreeNode *> &state, vector<TreeNode *> &choices, vector<vector<TreeNode *>> &res) {\n    // 解かどうかを確認\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n    }\n    // すべての選択肢を走査\n    for (TreeNode *choice : choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            // 次の選択へ進む\n            vector<TreeNode *> nextChoices{choice->left, choice->right};\n            backtrack(state, nextChoices, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.java
/* 現在の状態が解かどうかを判定 */\nboolean isSolution(List<TreeNode> state) {\n    return !state.isEmpty() && state.get(state.size() - 1).val == 7;\n}\n\n/* 解を記録 */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.add(new ArrayList<>(state));\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nboolean isValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* 状態を更新 */\nvoid makeChoice(List<TreeNode> state, TreeNode choice) {\n    state.add(choice);\n}\n\n/* 状態を元に戻す */\nvoid undoChoice(List<TreeNode> state, TreeNode choice) {\n    state.remove(state.size() - 1);\n}\n\n/* バックトラッキング:例題 3 */\nvoid backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // 解かどうかを確認\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n    }\n    // すべての選択肢を走査\n    for (TreeNode choice : choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            // 次の選択へ進む\n            backtrack(state, Arrays.asList(choice.left, choice.right), res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.cs
/* 現在の状態が解かどうかを判定 */\nbool IsSolution(List<TreeNode> state) {\n    return state.Count != 0 && state[^1].val == 7;\n}\n\n/* 解を記録 */\nvoid RecordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.Add(new List<TreeNode>(state));\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nbool IsValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* 状態を更新 */\nvoid MakeChoice(List<TreeNode> state, TreeNode choice) {\n    state.Add(choice);\n}\n\n/* 状態を元に戻す */\nvoid UndoChoice(List<TreeNode> state, TreeNode choice) {\n    state.RemoveAt(state.Count - 1);\n}\n\n/* バックトラッキング:例題 3 */\nvoid Backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // 解かどうかを確認\n    if (IsSolution(state)) {\n        // 解を記録\n        RecordSolution(state, res);\n    }\n    // すべての選択肢を走査\n    foreach (TreeNode choice in choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (IsValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            MakeChoice(state, choice);\n            // 次の選択へ進む\n            Backtrack(state, [choice.left!, choice.right!], res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            UndoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.go
/* 現在の状態が解かどうかを判定 */\nfunc isSolution(state *[]*TreeNode) bool {\n    return len(*state) != 0 && (*state)[len(*state)-1].Val == 7\n}\n\n/* 解を記録 */\nfunc recordSolution(state *[]*TreeNode, res *[][]*TreeNode) {\n    *res = append(*res, append([]*TreeNode{}, *state...))\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfunc isValid(state *[]*TreeNode, choice *TreeNode) bool {\n    return choice != nil && choice.Val != 3\n}\n\n/* 状態を更新 */\nfunc makeChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = append(*state, choice)\n}\n\n/* 状態を元に戻す */\nfunc undoChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = (*state)[:len(*state)-1]\n}\n\n/* バックトラッキング:例題 3 */\nfunc backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) {\n    // 解かどうかを確認\n    if isSolution(state) {\n        // 解を記録\n        recordSolution(state, res)\n    }\n    // すべての選択肢を走査\n    for _, choice := range *choices {\n        // 枝刈り:選択が妥当かを確認する\n        if isValid(state, choice) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice)\n            // 次の選択へ進む\n            temp := make([]*TreeNode, 0)\n            temp = append(temp, choice.Left, choice.Right)\n            backtrackIII(state, &temp, res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state, choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.swift
/* 現在の状態が解かどうかを判定 */\nfunc isSolution(state: [TreeNode]) -> Bool {\n    !state.isEmpty && state.last!.val == 7\n}\n\n/* 解を記録 */\nfunc recordSolution(state: [TreeNode], res: inout [[TreeNode]]) {\n    res.append(state)\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfunc isValid(state: [TreeNode], choice: TreeNode?) -> Bool {\n    choice != nil && choice!.val != 3\n}\n\n/* 状態を更新 */\nfunc makeChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.append(choice)\n}\n\n/* 状態を元に戻す */\nfunc undoChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.removeLast()\n}\n\n/* バックトラッキング:例題 3 */\nfunc backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) {\n    // 解かどうかを確認\n    if isSolution(state: state) {\n        recordSolution(state: state, res: &res)\n    }\n    // すべての選択肢を走査\n    for choice in choices {\n        // 枝刈り:選択が妥当かを確認する\n        if isValid(state: state, choice: choice) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state: &state, choice: choice)\n            // 次の選択へ進む\n            backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.js
/* 現在の状態が解かどうかを判定 */\nfunction isSolution(state) {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* 解を記録 */\nfunction recordSolution(state, res) {\n    res.push([...state]);\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfunction isValid(state, choice) {\n    return choice !== null && choice.val !== 3;\n}\n\n/* 状態を更新 */\nfunction makeChoice(state, choice) {\n    state.push(choice);\n}\n\n/* 状態を元に戻す */\nfunction undoChoice(state) {\n    state.pop();\n}\n\n/* バックトラッキング:例題 3 */\nfunction backtrack(state, choices, res) {\n    // 解かどうかを確認\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n    }\n    // すべての選択肢を走査\n    for (const choice of choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            // 次の選択へ進む\n            backtrack(state, [choice.left, choice.right], res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state);\n        }\n    }\n}\n
preorder_traversal_iii_template.ts
/* 現在の状態が解かどうかを判定 */\nfunction isSolution(state: TreeNode[]): boolean {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* 解を記録 */\nfunction recordSolution(state: TreeNode[], res: TreeNode[][]): void {\n    res.push([...state]);\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfunction isValid(state: TreeNode[], choice: TreeNode): boolean {\n    return choice !== null && choice.val !== 3;\n}\n\n/* 状態を更新 */\nfunction makeChoice(state: TreeNode[], choice: TreeNode): void {\n    state.push(choice);\n}\n\n/* 状態を元に戻す */\nfunction undoChoice(state: TreeNode[]): void {\n    state.pop();\n}\n\n/* バックトラッキング:例題 3 */\nfunction backtrack(\n    state: TreeNode[],\n    choices: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // 解かどうかを確認\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n    }\n    // すべての選択肢を走査\n    for (const choice of choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            // 次の選択へ進む\n            backtrack(state, [choice.left, choice.right], res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state);\n        }\n    }\n}\n
preorder_traversal_iii_template.dart
/* 現在の状態が解かどうかを判定 */\nbool isSolution(List<TreeNode> state) {\n  return state.isNotEmpty && state.last.val == 7;\n}\n\n/* 解を記録 */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n  res.add(List.from(state));\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nbool isValid(List<TreeNode> state, TreeNode? choice) {\n  return choice != null && choice.val != 3;\n}\n\n/* 状態を更新 */\nvoid makeChoice(List<TreeNode> state, TreeNode? choice) {\n  state.add(choice!);\n}\n\n/* 状態を元に戻す */\nvoid undoChoice(List<TreeNode> state, TreeNode? choice) {\n  state.removeLast();\n}\n\n/* バックトラッキング:例題 3 */\nvoid backtrack(\n  List<TreeNode> state,\n  List<TreeNode?> choices,\n  List<List<TreeNode>> res,\n) {\n  // 解かどうかを確認\n  if (isSolution(state)) {\n    // 解を記録\n    recordSolution(state, res);\n  }\n  // すべての選択肢を走査\n  for (TreeNode? choice in choices) {\n    // 枝刈り:選択が妥当かを確認する\n    if (isValid(state, choice)) {\n      // 試行: 選択を行い、状態を更新\n      makeChoice(state, choice);\n      // 次の選択へ進む\n      backtrack(state, [choice!.left, choice.right], res);\n      // バックトラック:選択を取り消し、前の状態に戻す\n      undoChoice(state, choice);\n    }\n  }\n}\n
preorder_traversal_iii_template.rs
/* 現在の状態が解かどうかを判定 */\nfn is_solution(state: &mut Vec<Rc<RefCell<TreeNode>>>) -> bool {\n    return !state.is_empty() && state.last().unwrap().borrow().val == 7;\n}\n\n/* 解を記録 */\nfn record_solution(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    res.push(state.clone());\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfn is_valid(_: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Option<&Rc<RefCell<TreeNode>>>) -> bool {\n    return choice.is_some() && choice.unwrap().borrow().val != 3;\n}\n\n/* 状態を更新 */\nfn make_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Rc<RefCell<TreeNode>>) {\n    state.push(choice);\n}\n\n/* 状態を元に戻す */\nfn undo_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, _: Rc<RefCell<TreeNode>>) {\n    state.pop();\n}\n\n/* バックトラッキング:例題 3 */\nfn backtrack(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    choices: &Vec<Option<&Rc<RefCell<TreeNode>>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    // 解かどうかを確認\n    if is_solution(state) {\n        // 解を記録\n        record_solution(state, res);\n    }\n    // すべての選択肢を走査\n    for &choice in choices.iter() {\n        // 枝刈り:選択が妥当かを確認する\n        if is_valid(state, choice) {\n            // 試行: 選択を行い、状態を更新\n            make_choice(state, choice.unwrap().clone());\n            // 次の選択へ進む\n            backtrack(\n                state,\n                &vec![\n                    choice.unwrap().borrow().left.as_ref(),\n                    choice.unwrap().borrow().right.as_ref(),\n                ],\n                res,\n            );\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undo_choice(state, choice.unwrap().clone());\n        }\n    }\n}\n
preorder_traversal_iii_template.c
/* 現在の状態が解かどうかを判定 */\nbool isSolution(void) {\n    return pathSize > 0 && path[pathSize - 1]->val == 7;\n}\n\n/* 解を記録 */\nvoid recordSolution(void) {\n    for (int i = 0; i < pathSize; i++) {\n        res[resSize][i] = path[i];\n    }\n    resSize++;\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nbool isValid(TreeNode *choice) {\n    return choice != NULL && choice->val != 3;\n}\n\n/* 状態を更新 */\nvoid makeChoice(TreeNode *choice) {\n    path[pathSize++] = choice;\n}\n\n/* 状態を元に戻す */\nvoid undoChoice(void) {\n    pathSize--;\n}\n\n/* バックトラッキング:例題 3 */\nvoid backtrack(TreeNode *choices[2]) {\n    // 解かどうかを確認\n    if (isSolution()) {\n        // 解を記録\n        recordSolution();\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < 2; i++) {\n        TreeNode *choice = choices[i];\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(choice);\n            // 次の選択へ進む\n            TreeNode *nextChoices[2] = {choice->left, choice->right};\n            backtrack(nextChoices);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice();\n        }\n    }\n}\n
preorder_traversal_iii_template.kt
/* 現在の状態が解かどうかを判定 */\nfun isSolution(state: MutableList<TreeNode?>): Boolean {\n    return state.isNotEmpty() && state[state.size - 1]?._val == 7\n}\n\n/* 解を記録 */\nfun recordSolution(state: MutableList<TreeNode?>?, res: MutableList<MutableList<TreeNode?>?>) {\n    res.add(state!!.toMutableList())\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfun isValid(state: MutableList<TreeNode?>?, choice: TreeNode?): Boolean {\n    return choice != null && choice._val != 3\n}\n\n/* 状態を更新 */\nfun makeChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.add(choice)\n}\n\n/* 状態を元に戻す */\nfun undoChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.removeLast()\n}\n\n/* バックトラッキング:例題 3 */\nfun backtrack(\n    state: MutableList<TreeNode?>,\n    choices: MutableList<TreeNode?>,\n    res: MutableList<MutableList<TreeNode?>?>\n) {\n    // 解かどうかを確認\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res)\n    }\n    // すべての選択肢を走査\n    for (choice in choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice)\n            // 次の選択へ進む\n            backtrack(state, mutableListOf(choice!!.left, choice.right), res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state, choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.rb
### 現在の状態が解かどうかを判定 ###\ndef is_solution?(state)\n  !state.empty? && state.last.val == 7\nend\n\n### 解を記録 ###\ndef record_solution(state, res)\n  res << state.dup\nend\n\n### 現在の状態で、この選択が妥当かを判定 ###\ndef is_valid?(state, choice)\n  choice && choice.val != 3\nend\n\n### 状態を更新 ###\ndef make_choice(state, choice)\n  state << choice\nend\n\n### 状態を復元 ###\ndef undo_choice(state, choice)\n  state.pop\nend\n\n### バックトラッキング法:例題3 ###\ndef backtrack(state, choices, res)\n  # 解かどうかを確認\n  record_solution(state, res) if is_solution?(state)\n\n  # すべての選択肢を走査\n  for choice in choices\n    # 枝刈り:選択が妥当かを確認する\n    if is_valid?(state, choice)\n      # 試行: 選択を行い、状態を更新\n      make_choice(state, choice)\n      # 次の選択へ進む\n      backtrack(state, [choice.left, choice.right], res)\n      # バックトラック:選択を取り消し、前の状態に戻す\n      undo_choice(state, choice)\n    end\n  end\nend\n
コードの可視化

全画面で見る >

問題の条件より、値が \\(7\\) のノードを見つけた後も探索を続ける必要があります。そのため、解を記録した後の return 文は削除しなければなりません。次の図は、return 文を残す場合と削除する場合の探索過程を比較したものです。

図 13-4   return を残す場合と削除する場合の探索過程の比較

前順走査にもとづく実装と比べると、バックトラッキングアルゴリズムのフレームワークにもとづく実装はやや冗長に見えますが、汎用性に優れています。実際、多くのバックトラッキング問題はこのフレームワークで解けます。具体的な問題に応じて statechoices を定義し、各メソッドを実装すれば十分です。

","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   よく使われる用語","text":"

アルゴリズム問題をより明確に分析するために、バックトラッキングでよく使われる用語の意味を整理し、例題3に対応する例を次の表にまとめます。

表 13-1   よく使われるバックトラッキング用語

用語 定義 例題3 解(solution) 問題の特定の条件を満たす答えであり、1 つまたは複数存在し得る 根ノードからノード \\(7\\) までの、制約条件を満たすすべての経路 制約条件(constraint) 解の実現可能性を制限する条件であり、通常は枝刈りに用いられる 経路にノード \\(3\\) を含まないこと 状態(state) ある時点における問題の状況を表し、すでに行った選択を含む 現在までに訪問したノードの経路、すなわち path ノードリスト 試行(attempt) 利用可能な選択肢にもとづいて解空間を探索する過程であり、選択、状態更新、解判定を含む 左右の子ノードを再帰的に訪問し、ノードを path に追加し、値が \\(7\\) か判定する 戻る(backtracking) 制約条件を満たさない状態に出会ったとき、それまでの選択を取り消して前の状態へ戻ること 葉ノードを越えたとき、ノード訪問を終えたとき、値が \\(3\\) のノードに出会ったときに探索を終了し、関数から戻る 枝刈り(pruning) 問題の性質や制約条件にもとづき、無意味な探索経路を避ける方法であり、探索効率を高める 値が \\(3\\) のノードに出会ったら、それ以上探索しない

Tip

問題、解、状態などの概念は汎用的であり、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムにも共通して現れます。

","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1315","level":2,"title":"13.1.5   利点と限界","text":"

バックトラッキングアルゴリズムの本質は深さ優先探索です。条件を満たす解を見つけるまで、あり得るすべての解を試します。この方法の利点は、考えられるすべての解を見つけられることであり、適切な枝刈りを行えば高い効率を発揮します。

しかし、大規模または複雑な問題を扱う場合、バックトラッキングアルゴリズムの実行効率は受け入れがたいことがあります。

  • 時間:バックトラッキングアルゴリズムでは通常、状態空間のすべての可能性をたどる必要があり、時間計算量は指数時間や階乗時間に達することがあります。
  • 空間:再帰呼び出しの過程では現在の状態(たとえば経路や枝刈り用の補助変数など)を保持する必要があり、深さが大きいと空間使用量も大きくなります。

それでもなお、バックトラッキングアルゴリズムは一部の探索問題や制約充足問題に対する最良の解法です。この種の問題では、どの選択が有効な解を生むかを事前に予測できないため、可能な選択肢をすべてたどる必要があります。このときの鍵は**いかに効率を最適化するか**であり、代表的な方法は 2 つあります。

  • 枝刈り:解が生じないことが確実な経路を探索しないことで、時間と空間を節約する。
  • ヒューリスティック探索:探索中に何らかの戦略や推定値を導入し、有効な解を生みやすい経路を優先的に探索する。
","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316","level":2,"title":"13.1.6   バックトラッキングの典型例題","text":"

バックトラッキングアルゴリズムは、多くの探索問題、制約充足問題、組合せ最適化問題の解決に利用できます。

探索問題:この種の問題の目標は、特定の条件を満たす解を見つけることです。

  • 全順列問題:ある集合が与えられたとき、考えられるすべての順列を求める。
  • 部分和問題:ある集合と目標和が与えられたとき、和が目標値となるすべての部分集合を見つける。
  • ハノイの塔問題:3 本の柱と大きさの異なる複数の円盤が与えられたとき、すべての円盤を 1 本の柱から別の柱へ移動する。ただし 1 回に 1 枚しか動かせず、大きい円盤を小さい円盤の上に置いてはならない。

制約充足問題:この種の問題の目標は、すべての制約条件を満たす解を見つけることです。

  • \\(n\\) クイーン問題:\\(n \\times n\\) の盤面に \\(n\\) 個のクイーンを配置し、互いに攻撃し合わないようにする。
  • 数独:\\(9 \\times 9\\) のグリッドに数字 \\(1\\) ~ \\(9\\) を入れ、各行・各列・各 \\(3 \\times 3\\) の小区画で数字が重複しないようにする。
  • グラフ彩色問題:無向グラフが与えられたとき、隣接する頂点が同じ色にならないように、できるだけ少ない色で各頂点を彩色する。

組合せ最適化問題:この種の問題の目標は、組合せ空間の中で条件を満たす最適解を見つけることです。

  • 0-1 ナップサック問題:複数の品物とナップサックが与えられ、各品物には価値と重さがある。ナップサック容量の範囲内で総価値が最大になるように品物を選ぶ。
  • 巡回セールスマン問題:グラフ内のある頂点から出発し、他のすべての頂点をちょうど 1 回ずつ訪れて出発点へ戻るときの最短経路を求める。
  • 最大クリーク問題:無向グラフが与えられたとき、任意の 2 頂点間に辺が存在する最大の完全部分グラフを見つける。

多くの組合せ最適化問題では、バックトラッキングは最適な解法ではない点に注意してください。

  • 0-1 ナップサック問題は通常、より高い時間効率を得るために動的計画法で解く。
  • 巡回セールスマン問題は著名な NP-Hard 問題であり、よく用いられる解法には遺伝的アルゴリズムや蟻コロニー最適化などがある。
  • 最大クリーク問題はグラフ理論における古典的問題であり、貪欲法などのヒューリスティックで解ける。
","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/","level":1,"title":"13.4   n クイーン問題","text":"

Question

チェスのルールによれば、クイーンは同じ行、同じ列、または同じ斜線上にある駒を攻撃できます。\\(n\\) 個のクイーンと \\(n \\times n\\) サイズの盤面が与えられたとき、すべてのクイーンが互いに攻撃し合わない配置を求めます。

下図に示すように、\\(n = 4\\) のとき、2 つの解を見つけることができます。バックトラッキングの観点から見ると、\\(n \\times n\\) サイズの盤面には合計 \\(n^2\\) 個のマスがあり、これがすべての選択肢 choices を与えます。クイーンを 1 つずつ配置していく過程で、盤面の状態は絶えず変化し、その各時点の盤面が状態 state です。

図 13-15   4 クイーン問題の解

下図は本問題の 3 つの制約条件を示しています。複数のクイーンは同じ行、同じ列、同じ対角線上に置けません。なお、対角線には主対角線 \\ と副対角線 / の 2 種類があります。

図 13-16   n クイーン問題の制約条件

","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1","level":3,"title":"1.   行ごとの配置戦略","text":"

クイーンの数と盤面の行数はいずれも \\(n\\) なので、次の推論を容易に得られます:盤面の各行にはクイーンを 1 つだけ配置できます。

つまり、行ごとの配置戦略を採用できます:最初の行から始めて、各行に 1 つのクイーンを配置し、最後の行まで進みます。

下図は 4 クイーン問題における行ごとの配置過程を示しています。図の大きさの都合上、下図では 1 行目における検索分岐の 1 つだけを展開し、列制約と対角線制約を満たさない案はすべて枝刈りしています。

図 13-17   行ごとの配置戦略

本質的には、行ごとの配置戦略は枝刈りとして機能します。これにより、同じ行に複数のクイーンが現れるすべての探索分岐を回避できます。

","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2","level":3,"title":"2.   列と対角線の枝刈り","text":"

列制約を満たすために、長さ \\(n\\) のブール配列 cols を用いて、各列にクイーンがあるかどうかを記録できます。配置を決めるたびに、cols を使って既存のクイーンがある列を枝刈りし、バックトラッキングの中で cols の状態を動的に更新します。

Tip

注意として、行列の原点は左上にあり、行インデックスは上から下へ、列インデックスは左から右へ増加します。

では、対角線制約はどのように扱えばよいのでしょうか。盤面上のあるマスの行列インデックスを \\((row, col)\\) とし、行列内のある主対角線を選ぶと、その対角線上のすべてのマスで行インデックスから列インデックスを引いた値が等しいことが分かります。つまり、主対角線上のすべてのマスでは \\(row - col\\) が一定値になります。

つまり、2 つのマスが \\(row_1 - col_1 = row_2 - col_2\\) を満たすなら、それらは必ず同じ主対角線上にあります。この性質を利用して、下図の配列 diags1 により、各主対角線にクイーンがあるかどうかを記録できます。

同様に、副対角線上のすべてのマスでは \\(row + col\\) が一定値です。副対角線制約も配列 diags2 を使って処理できます。

図 13-18   列制約と対角線制約の処理

","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#3","level":3,"title":"3.   コード実装","text":"

注意として、\\(n\\) 次正方行列では \\(row - col\\) の範囲は \\([-n + 1, n - 1]\\) 、\\(row + col\\) の範囲は \\([0, 2n - 2]\\) です。したがって、主対角線と副対角線の本数はいずれも \\(2n - 1\\) であり、配列 diags1diags2 の長さもともに \\(2n - 1\\) です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby n_queens.py
def backtrack(\n    row: int,\n    n: int,\n    state: list[list[str]],\n    res: list[list[list[str]]],\n    cols: list[bool],\n    diags1: list[bool],\n    diags2: list[bool],\n):\n    \"\"\"バックトラッキング:N クイーン\"\"\"\n    # すべての行への配置が完了したら、解を記録する\n    if row == n:\n        res.append([list(row) for row in state])\n        return\n    # すべての列を走査\n    for col in range(n):\n        # このマスに対応する主対角線と副対角線を計算\n        diag1 = row - col + n - 1\n        diag2 = row + col\n        # 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if not cols[col] and not diags1[diag1] and not diags2[diag2]:\n            # 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\"\n            cols[col] = diags1[diag1] = diags2[diag2] = True\n            # 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            # 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\"\n            cols[col] = diags1[diag1] = diags2[diag2] = False\n\ndef n_queens(n: int) -> list[list[list[str]]]:\n    \"\"\"N クイーンを解く\"\"\"\n    # n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    state = [[\"#\" for _ in range(n)] for _ in range(n)]\n    cols = [False] * n  # 列にクイーンがあるか記録\n    diags1 = [False] * (2 * n - 1)  # 主対角線にクイーンがあるかを記録\n    diags2 = [False] * (2 * n - 1)  # 副対角線にクイーンがあるかを記録\n    res = []\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n
n_queens.cpp
/* バックトラッキング:N クイーン */\nvoid backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,\n               vector<bool> &diags1, vector<bool> &diags2) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row == n) {\n        res.push_back(state);\n        return;\n    }\n    // すべての列を走査\n    for (int col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nvector<vector<vector<string>>> nQueens(int n) {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    vector<vector<string>> state(n, vector<string>(n, \"#\"));\n    vector<bool> cols(n, false);           // 列にクイーンがあるか記録\n    vector<bool> diags1(2 * n - 1, false); // 主対角線にクイーンがあるかを記録\n    vector<bool> diags2(2 * n - 1, false); // 副対角線にクイーンがあるかを記録\n    vector<vector<vector<string>>> res;\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.java
/* バックトラッキング:N クイーン */\nvoid backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,\n        boolean[] cols, boolean[] diags1, boolean[] diags2) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row == n) {\n        List<List<String>> copyState = new ArrayList<>();\n        for (List<String> sRow : state) {\n            copyState.add(new ArrayList<>(sRow));\n        }\n        res.add(copyState);\n        return;\n    }\n    // すべての列を走査\n    for (int col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state.get(row).set(col, \"Q\");\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state.get(row).set(col, \"#\");\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nList<List<List<String>>> nQueens(int n) {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    List<List<String>> state = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<String> row = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            row.add(\"#\");\n        }\n        state.add(row);\n    }\n    boolean[] cols = new boolean[n]; // 列にクイーンがあるか記録\n    boolean[] diags1 = new boolean[2 * n - 1]; // 主対角線にクイーンがあるかを記録\n    boolean[] diags2 = new boolean[2 * n - 1]; // 副対角線にクイーンがあるかを記録\n    List<List<List<String>>> res = new ArrayList<>();\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.cs
/* バックトラッキング:N クイーン */\nvoid Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,\n        bool[] cols, bool[] diags1, bool[] diags2) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row == n) {\n        List<List<string>> copyState = [];\n        foreach (List<string> sRow in state) {\n            copyState.Add(new List<string>(sRow));\n        }\n        res.Add(copyState);\n        return;\n    }\n    // すべての列を走査\n    for (int col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            Backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nList<List<List<string>>> NQueens(int n) {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    List<List<string>> state = [];\n    for (int i = 0; i < n; i++) {\n        List<string> row = [];\n        for (int j = 0; j < n; j++) {\n            row.Add(\"#\");\n        }\n        state.Add(row);\n    }\n    bool[] cols = new bool[n]; // 列にクイーンがあるか記録\n    bool[] diags1 = new bool[2 * n - 1]; // 主対角線にクイーンがあるかを記録\n    bool[] diags2 = new bool[2 * n - 1]; // 副対角線にクイーンがあるかを記録\n    List<List<List<string>>> res = [];\n\n    Backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.go
/* バックトラッキング:N クイーン */\nfunc backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {\n    // すべての行への配置が完了したら、解を記録する\n    if row == n {\n        newState := make([][]string, len(*state))\n        for i, _ := range newState {\n            newState[i] = make([]string, len((*state)[0]))\n            copy(newState[i], (*state)[i])\n\n        }\n        *res = append(*res, newState)\n        return\n    }\n    // すべての列を走査\n    for col := 0; col < n; col++ {\n        // このマスに対応する主対角線と副対角線を計算\n        diag1 := row - col + n - 1\n        diag2 := row + col\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {\n            // 試行:そのマスにクイーンを置く\n            (*state)[row][col] = \"Q\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true\n            // 次の行に配置する\n            backtrack(row+1, n, state, res, cols, diags1, diags2)\n            // 戻す:そのマスを空きマスに戻す\n            (*state)[row][col] = \"#\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false\n        }\n    }\n}\n\n/* N クイーンを解く */\nfunc nQueens(n int) [][][]string {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    state := make([][]string, n)\n    for i := 0; i < n; i++ {\n        row := make([]string, n)\n        for i := 0; i < n; i++ {\n            row[i] = \"#\"\n        }\n        state[i] = row\n    }\n    // 列にクイーンがあるか記録\n    cols := make([]bool, n)\n    diags1 := make([]bool, 2*n-1)\n    diags2 := make([]bool, 2*n-1)\n    res := make([][][]string, 0)\n    backtrack(0, n, &state, &res, &cols, &diags1, &diags2)\n    return res\n}\n
n_queens.swift
/* バックトラッキング:N クイーン */\nfunc backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {\n    // すべての行への配置が完了したら、解を記録する\n    if row == n {\n        res.append(state)\n        return\n    }\n    // すべての列を走査\n    for col in 0 ..< n {\n        // このマスに対応する主対角線と副対角線を計算\n        let diag1 = row - col + n - 1\n        let diag2 = row + col\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\"\n            cols[col] = true\n            diags1[diag1] = true\n            diags2[diag2] = true\n            // 次の行に配置する\n            backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\"\n            cols[col] = false\n            diags1[diag1] = false\n            diags2[diag2] = false\n        }\n    }\n}\n\n/* N クイーンを解く */\nfunc nQueens(n: Int) -> [[[String]]] {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    var state = Array(repeating: Array(repeating: \"#\", count: n), count: n)\n    var cols = Array(repeating: false, count: n) // 列にクイーンがあるか記録\n    var diags1 = Array(repeating: false, count: 2 * n - 1) // 主対角線にクイーンがあるかを記録\n    var diags2 = Array(repeating: false, count: 2 * n - 1) // 副対角線にクイーンがあるかを記録\n    var res: [[[String]]] = []\n\n    backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n\n    return res\n}\n
n_queens.js
/* バックトラッキング:N クイーン */\nfunction backtrack(row, n, state, res, cols, diags1, diags2) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // すべての列を走査\n    for (let col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nfunction nQueens(n) {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // 列にクイーンがあるか記録\n    const diags1 = Array(2 * n - 1).fill(false); // 主対角線にクイーンがあるかを記録\n    const diags2 = Array(2 * n - 1).fill(false); // 副対角線にクイーンがあるかを記録\n    const res = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
n_queens.ts
/* バックトラッキング:N クイーン */\nfunction backtrack(\n    row: number,\n    n: number,\n    state: string[][],\n    res: string[][][],\n    cols: boolean[],\n    diags1: boolean[],\n    diags2: boolean[]\n): void {\n    // すべての行への配置が完了したら、解を記録する\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // すべての列を走査\n    for (let col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nfunction nQueens(n: number): string[][][] {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // 列にクイーンがあるか記録\n    const diags1 = Array(2 * n - 1).fill(false); // 主対角線にクイーンがあるかを記録\n    const diags2 = Array(2 * n - 1).fill(false); // 副対角線にクイーンがあるかを記録\n    const res: string[][][] = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
n_queens.dart
/* バックトラッキング:N クイーン */\nvoid backtrack(\n  int row,\n  int n,\n  List<List<String>> state,\n  List<List<List<String>>> res,\n  List<bool> cols,\n  List<bool> diags1,\n  List<bool> diags2,\n) {\n  // すべての行への配置が完了したら、解を記録する\n  if (row == n) {\n    List<List<String>> copyState = [];\n    for (List<String> sRow in state) {\n      copyState.add(List.from(sRow));\n    }\n    res.add(copyState);\n    return;\n  }\n  // すべての列を走査\n  for (int col = 0; col < n; col++) {\n    // このマスに対応する主対角線と副対角線を計算\n    int diag1 = row - col + n - 1;\n    int diag2 = row + col;\n    // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n    if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n      // 試行:そのマスにクイーンを置く\n      state[row][col] = \"Q\";\n      cols[col] = true;\n      diags1[diag1] = true;\n      diags2[diag2] = true;\n      // 次の行に配置する\n      backtrack(row + 1, n, state, res, cols, diags1, diags2);\n      // 戻す:そのマスを空きマスに戻す\n      state[row][col] = \"#\";\n      cols[col] = false;\n      diags1[diag1] = false;\n      diags2[diag2] = false;\n    }\n  }\n}\n\n/* N クイーンを解く */\nList<List<List<String>>> nQueens(int n) {\n  // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n  List<List<String>> state = List.generate(n, (index) => List.filled(n, \"#\"));\n  List<bool> cols = List.filled(n, false); // 列にクイーンがあるか記録\n  List<bool> diags1 = List.filled(2 * n - 1, false); // 主対角線にクイーンがあるかを記録\n  List<bool> diags2 = List.filled(2 * n - 1, false); // 副対角線にクイーンがあるかを記録\n  List<List<List<String>>> res = [];\n\n  backtrack(0, n, state, res, cols, diags1, diags2);\n\n  return res;\n}\n
n_queens.rs
/* バックトラッキング:N クイーン */\nfn backtrack(\n    row: usize,\n    n: usize,\n    state: &mut Vec<Vec<String>>,\n    res: &mut Vec<Vec<Vec<String>>>,\n    cols: &mut [bool],\n    diags1: &mut [bool],\n    diags2: &mut [bool],\n) {\n    // すべての行への配置が完了したら、解を記録する\n    if row == n {\n        res.push(state.clone());\n        return;\n    }\n    // すべての列を走査\n    for col in 0..n {\n        // このマスに対応する主対角線と副対角線を計算\n        let diag1 = row + n - 1 - col;\n        let diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);\n        }\n    }\n}\n\n/* N クイーンを解く */\nfn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    let mut state: Vec<Vec<String>> = vec![vec![\"#\".to_string(); n]; n];\n    let mut cols = vec![false; n]; // 列にクイーンがあるか記録\n    let mut diags1 = vec![false; 2 * n - 1]; // 主対角線にクイーンがあるかを記録\n    let mut diags2 = vec![false; 2 * n - 1]; // 副対角線にクイーンがあるかを記録\n    let mut res: Vec<Vec<Vec<String>>> = Vec::new();\n\n    backtrack(\n        0,\n        n,\n        &mut state,\n        &mut res,\n        &mut cols,\n        &mut diags1,\n        &mut diags2,\n    );\n\n    res\n}\n
n_queens.c
/* バックトラッキング:N クイーン */\nvoid backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE],\n               bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row == n) {\n        res[*resSize] = (char **)malloc(sizeof(char *) * n);\n        for (int i = 0; i < n; ++i) {\n            res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1));\n            strcpy(res[*resSize][i], state[i]);\n        }\n        (*resSize)++;\n        return;\n    }\n    // すべての列を走査\n    for (int col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nchar ***nQueens(int n, int *returnSize) {\n    char state[MAX_SIZE][MAX_SIZE];\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    for (int i = 0; i < n; ++i) {\n        for (int j = 0; j < n; ++j) {\n            state[i][j] = '#';\n        }\n        state[i][n] = '\\0';\n    }\n    bool cols[MAX_SIZE] = {false};           // 列にクイーンがあるか記録\n    bool diags1[2 * MAX_SIZE - 1] = {false}; // 主対角線にクイーンがあるかを記録\n    bool diags2[2 * MAX_SIZE - 1] = {false}; // 副対角線にクイーンがあるかを記録\n\n    char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE);\n    *returnSize = 0;\n    backtrack(0, n, state, res, returnSize, cols, diags1, diags2);\n    return res;\n}\n
n_queens.kt
/* バックトラッキング:N クイーン */\nfun backtrack(\n    row: Int,\n    n: Int,\n    state: MutableList<MutableList<String>>,\n    res: MutableList<MutableList<MutableList<String>>?>,\n    cols: BooleanArray,\n    diags1: BooleanArray,\n    diags2: BooleanArray\n) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row == n) {\n        val copyState = mutableListOf<MutableList<String>>()\n        for (sRow in state) {\n            copyState.add(sRow.toMutableList())\n        }\n        res.add(copyState)\n        return\n    }\n    // すべての列を走査\n    for (col in 0..<n) {\n        // このマスに対応する主対角線と副対角線を計算\n        val diag1 = row - col + n - 1\n        val diag2 = row + col\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\"\n            diags2[diag2] = true\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\"\n            diags2[diag2] = false\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n        }\n    }\n}\n\n/* N クイーンを解く */\nfun nQueens(n: Int): MutableList<MutableList<MutableList<String>>?> {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    val state = mutableListOf<MutableList<String>>()\n    for (i in 0..<n) {\n        val row = mutableListOf<String>()\n        for (j in 0..<n) {\n            row.add(\"#\")\n        }\n        state.add(row)\n    }\n    val cols = BooleanArray(n) // 列にクイーンがあるか記録\n    val diags1 = BooleanArray(2 * n - 1) // 主対角線にクイーンがあるかを記録\n    val diags2 = BooleanArray(2 * n - 1) // 副対角線にクイーンがあるかを記録\n    val res = mutableListOf<MutableList<MutableList<String>>?>()\n\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n}\n
n_queens.rb
### バックトラッキング法:Nクイーン ###\ndef backtrack(row, n, state, res, cols, diags1, diags2)\n  # すべての行への配置が完了したら、解を記録する\n  if row == n\n    res << state.map { |row| row.dup }\n    return\n  end\n\n  # すべての列を走査\n  for col in 0...n\n    # このマスに対応する主対角線と副対角線を計算\n    diag1 = row - col + n - 1\n    diag2 = row + col\n    # 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n    if !cols[col] && !diags1[diag1] && !diags2[diag2]\n      # 試行:そのマスにクイーンを置く\n      state[row][col] = \"Q\"\n      cols[col] = diags1[diag1] = diags2[diag2] = true\n      # 次の行に配置する\n      backtrack(row + 1, n, state, res, cols, diags1, diags2)\n      # 戻す:そのマスを空きマスに戻す\n      state[row][col] = \"#\"\n      cols[col] = diags1[diag1] = diags2[diag2] = false\n    end\n  end\nend\n\n### Nクイーンを解く ###\ndef n_queens(n)\n  # n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n  state = Array.new(n) { Array.new(n, \"#\") }\n  cols = Array.new(n, false) # 列にクイーンがあるか記録\n  diags1 = Array.new(2 * n - 1, false) # 主対角線にクイーンがあるかを記録\n  diags2 = Array.new(2 * n - 1, false) # 副対角線にクイーンがあるかを記録\n  res = []\n  backtrack(0, n, state, res, cols, diags1, diags2)\n\n  res\nend\n
コードの可視化

全画面で見る >

行ごとに \\(n\\) 回配置し、列制約を考慮すると、1 行目から最終行までの選択肢はそれぞれ \\(n\\)、\\(n-1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) 個となるため、時間計算量は \\(O(n!)\\) です。解を記録する際には、行列 state をコピーして res に追加する必要があり、このコピー操作には \\(O(n^2)\\) 時間を要します。したがって、全体の時間計算量は \\(O(n! \\cdot n^2)\\) です。実際には、対角線制約による枝刈りも探索空間を大きく縮小できるため、探索効率はしばしば上記の時間計算量より良くなります。

配列 state は \\(O(n^2)\\) の空間を使用し、配列 colsdiags1diags2 はいずれも \\(O(n)\\) の空間を使用します。最大再帰深さは \\(n\\) で、スタックフレーム空間として \\(O(n)\\) を使用します。したがって、空間計算量は \\(O(n^2)\\) です。

","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/","level":1,"title":"13.2   全順列問題","text":"

全順列問題はバックトラッキングアルゴリズムの典型的な応用例です。これは、ある集合(配列や文字列など)が与えられたとき、その要素のあり得るすべての順列を求める問題です。

下表に、入力配列とそれに対応するすべての順列から成る例をいくつか示します。

表 13-2   全順列の例

入力配列 すべての順列 \\([1]\\) \\([1]\\) \\([1, 2]\\) \\([1, 2], [2, 1]\\) \\([1, 2, 3]\\) \\([1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]\\)","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1321","level":2,"title":"13.2.1   等しい要素がない場合","text":"

Question

重複要素を含まない整数配列を入力として受け取り、あり得るすべての順列を返します。

バックトラッキングアルゴリズムの観点から見ると、順列生成の過程は一連の選択の結果として捉えられます。入力配列が \\([1, 2, 3]\\) だとすると、最初に \\(1\\) を選び、次に \\(3\\) を選び、最後に \\(2\\) を選べば、順列 \\([1, 3, 2]\\) が得られます。戻る操作は 1 つの選択を取り消し、その後で別の選択を試し続けることを表します。

バックトラッキングコードの観点では、候補集合 choices は入力配列中のすべての要素であり、状態 state は現時点までに選ばれた要素です。各要素は 1 回しか選べないことに注意してください。したがって state 内の要素はすべて一意でなければなりません。

下図のように、探索過程は再帰木として展開できます。木の各ノードは現在の状態 state を表します。根ノードから始めて 3 ラウンドの選択を経て葉ノードに到達し、各葉ノードが 1 つの順列に対応します。

図 13-5   全順列の再帰木

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1","level":3,"title":"1.   重複選択の枝刈り","text":"

各要素が 1 回しか選ばれないようにするため、ブール配列 selected の導入を考えます。ここで selected[i]choices[i] がすでに選ばれているかどうかを表し、これに基づいて次の枝刈りを行います。

  • 選択 choice[i] を行った後、selected[i] を \\(\\text{True}\\) に設定し、その要素が選択済みであることを表します。
  • 選択肢リスト choices を走査するとき、すでに選ばれたノードはすべてスキップします。これが枝刈りです。

下図のように、1 回目に 1、2 回目に 3、3 回目に 2 を選ぶとします。このとき 2 回目では要素 1 の分岐を、3 回目では要素 1 と要素 3 の分岐を刈り取る必要があります。

図 13-6   全順列の枝刈り例

上図から、この枝刈りにより探索空間の大きさは \\(O(n^n)\\) から \\(O(n!)\\) へ削減されることがわかります。

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2","level":3,"title":"2.   コード実装","text":"

以上を整理できれば、フレームワークコードの「穴埋め」を行えます。全体のコードを短くするため、フレームワークコード中の各関数を個別には実装せず、これらを backtrack() 関数内に展開します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_i.py
def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"バックトラッキング:順列 I\"\"\"\n    # 状態の長さが要素数に等しければ、解を記録\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # すべての選択肢を走査\n    for i, choice in enumerate(choices):\n        # 枝刈り:要素の重複選択を許可しない\n        if not selected[i]:\n            # 試行: 選択を行い、状態を更新\n            selected[i] = True\n            state.append(choice)\n            # 次の選択へ進む\n            backtrack(state, choices, selected, res)\n            # バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = False\n            state.pop()\n\ndef permutations_i(nums: list[int]) -> list[list[int]]:\n    \"\"\"全順列 I\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
permutations_i.cpp
/* バックトラッキング:順列 I */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.push_back(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* 全順列 I */\nvector<vector<int>> permutationsI(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
permutations_i.java
/* バックトラッキング:順列 I */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.add(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* 全順列 I */\nList<List<Integer>> permutationsI(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
permutations_i.cs
/* バックトラッキング:順列 I */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.Add(choice);\n            // 次の選択へ進む\n            Backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* 全順列 I */\nList<List<int>> PermutationsI(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
permutations_i.go
/* バックトラッキング:順列 I */\nfunc backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // すべての選択肢を走査\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // 枝刈り:要素の重複選択を許可しない\n        if !(*selected)[i] {\n            // 試行: 選択を行い、状態を更新\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // 次の選択へ進む\n            backtrackI(state, choices, selected, res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* 全順列 I */\nfunc permutationsI(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackI(&state, &nums, &selected, &res)\n    return res\n}\n
permutations_i.swift
/* バックトラッキング:順列 I */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // すべての選択肢を走査\n    for (i, choice) in choices.enumerated() {\n        // 枝刈り:要素の重複選択を許可しない\n        if !selected[i] {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true\n            state.append(choice)\n            // 次の選択へ進む\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* 全順列 I */\nfunc permutationsI(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
permutations_i.js
/* バックトラッキング:順列 I */\nfunction backtrack(state, choices, selected, res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    choices.forEach((choice, i) => {\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全順列 I */\nfunction permutationsI(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_i.ts
/* バックトラッキング:順列 I */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    choices.forEach((choice, i) => {\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全順列 I */\nfunction permutationsI(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_i.dart
/* バックトラッキング:順列 I */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // 状態の長さが要素数に等しければ、解を記録\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // すべての選択肢を走査\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // 枝刈り:要素の重複選択を許可しない\n    if (!selected[i]) {\n      // 試行: 選択を行い、状態を更新\n      selected[i] = true;\n      state.add(choice);\n      // 次の選択へ進む\n      backtrack(state, choices, selected, res);\n      // バックトラック:選択を取り消し、前の状態に戻す\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* 全順列 I */\nList<List<int>> permutationsI(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
permutations_i.rs
/* バックトラッキング:順列 I */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // すべての選択肢を走査\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // 枝刈り:要素の重複選択を許可しない\n        if !selected[i] {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state.clone(), choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* 全順列 I */\nfn permutations_i(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new(); // 状態(部分集合)\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
permutations_i.c
/* バックトラッキング:順列 I */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state[stateSize] = choice;\n            // 次の選択へ進む\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n        }\n    }\n}\n\n/* 全順列 I */\nint **permutationsI(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
permutations_i.kt
/* バックトラッキング:順列 I */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // すべての選択肢を走査\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true\n            state.add(choice)\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* 全順列 I */\nfun permutationsI(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
permutations_i.rb
### バックトラッキング法:全順列 I ###\ndef backtrack(state, choices, selected, res)\n  # 状態の長さが要素数に等しければ、解を記録\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # すべての選択肢を走査\n  choices.each_with_index do |choice, i|\n    # 枝刈り:要素の重複選択を許可しない\n    unless selected[i]\n      # 試行: 選択を行い、状態を更新\n      selected[i] = true\n      state << choice\n      # 次の選択へ進む\n      backtrack(state, choices, selected, res)\n      # バックトラック:選択を取り消し、前の状態に戻す\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### 全順列 I ###\ndef permutations_i(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
コードの可視化

全画面で見る >

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1322","level":2,"title":"13.2.2   等しい要素を考慮する場合","text":"

Question

整数配列を入力として受け取り、配列には重複要素が含まれる場合があります。重複しない順列をすべて返します。

入力配列が \\([1, 1, 2]\\) だと仮定します。2 つの重複する要素 \\(1\\) を区別しやすくするため、2 つ目の \\(1\\) を \\(\\hat{1}\\) と記します。

下図のように、上述の方法で生成される順列の半分は重複しています。

図 13-7   重複した順列

では、重複した順列をどのように取り除けばよいのでしょうか。最も直接的なのは、ハッシュ集合を用いて順列結果をそのまま重複排除する方法です。しかしこのやり方は十分に洗練されていません。なぜなら、重複順列を生成する探索分岐はそもそも不要であり、事前に見つけて枝刈りすべきだからです。そうすることで、アルゴリズム効率をさらに高められます。

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1_1","level":3,"title":"1.   等しい要素の枝刈り","text":"

下図を見ると、1 回目のラウンドでは \\(1\\) を選ぶことと \\(\\hat{1}\\) を選ぶことは等価であり、これら 2 つの選択の下で生成される順列はすべて重複します。したがって \\(\\hat{1}\\) を枝刈りすべきです。

同様に、1 回目で \\(2\\) を選んだ後では、2 回目のラウンドにおける \\(1\\) と \\(\\hat{1}\\) も重複分岐を生むため、2 回目の \\(\\hat{1}\\) も枝刈りすべきです。

本質的には、各ラウンドの選択において、等しい複数の要素が 1 回しか選ばれないようにすることが目標です。

図 13-8   重複順列の枝刈り

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2_1","level":3,"title":"2.   コード実装","text":"

前問のコードを土台として、各ラウンドの選択でハッシュ集合 duplicated を 1 つ用意し、そのラウンドですでに試した要素を記録して、重複要素を枝刈りすることを考えます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_ii.py
def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"バックトラッキング:順列 II\"\"\"\n    # 状態の長さが要素数に等しければ、解を記録\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # すべての選択肢を走査\n    duplicated = set[int]()\n    for i, choice in enumerate(choices):\n        # 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if not selected[i] and choice not in duplicated:\n            # 試行: 選択を行い、状態を更新\n            duplicated.add(choice)  # 選択済みの要素値を記録\n            selected[i] = True\n            state.append(choice)\n            # 次の選択へ進む\n            backtrack(state, choices, selected, res)\n            # バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = False\n            state.pop()\n\ndef permutations_ii(nums: list[int]) -> list[list[int]]:\n    \"\"\"全順列 II\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
permutations_ii.cpp
/* バックトラッキング:順列 II */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // すべての選択肢を走査\n    unordered_set<int> duplicated;\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && duplicated.find(choice) == duplicated.end()) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.emplace(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.push_back(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* 全順列 II */\nvector<vector<int>> permutationsII(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
permutations_ii.java
/* バックトラッキング:順列 II */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    Set<Integer> duplicated = new HashSet<Integer>();\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.add(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.add(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* 全順列 II */\nList<List<Integer>> permutationsII(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
permutations_ii.cs
/* バックトラッキング:順列 II */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    HashSet<int> duplicated = [];\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated.Contains(choice)) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.Add(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.Add(choice);\n            // 次の選択へ進む\n            Backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* 全順列 II */\nList<List<int>> PermutationsII(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
permutations_ii.go
/* バックトラッキング:順列 II */\nfunc backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // すべての選択肢を走査\n    duplicated := make(map[int]struct{}, 0)\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if _, ok := duplicated[choice]; !ok && !(*selected)[i] {\n            // 試す: 選択を行って状態を更新\n            // 選択済みの要素値を記録\n            duplicated[choice] = struct{}{}\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // 次の選択へ進む\n            backtrackII(state, choices, selected, res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* 全順列 II */\nfunc permutationsII(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackII(&state, &nums, &selected, &res)\n    return res\n}\n
permutations_ii.swift
/* バックトラッキング:順列 II */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // すべての選択肢を走査\n    var duplicated: Set<Int> = []\n    for (i, choice) in choices.enumerated() {\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if !selected[i], !duplicated.contains(choice) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.insert(choice) // 選択済みの要素値を記録\n            selected[i] = true\n            state.append(choice)\n            // 次の選択へ進む\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* 全順列 II */\nfunc permutationsII(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
permutations_ii.js
/* バックトラッキング:順列 II */\nfunction backtrack(state, choices, selected, res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated.has(choice)) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.add(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全順列 II */\nfunction permutationsII(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_ii.ts
/* バックトラッキング:順列 II */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated.has(choice)) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.add(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全順列 II */\nfunction permutationsII(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_ii.dart
/* バックトラッキング:順列 II */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // 状態の長さが要素数に等しければ、解を記録\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // すべての選択肢を走査\n  Set<int> duplicated = {};\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n    if (!selected[i] && !duplicated.contains(choice)) {\n      // 試行: 選択を行い、状態を更新\n      duplicated.add(choice); // 選択済みの要素値を記録\n      selected[i] = true;\n      state.add(choice);\n      // 次の選択へ進む\n      backtrack(state, choices, selected, res);\n      // バックトラック:選択を取り消し、前の状態に戻す\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* 全順列 II */\nList<List<int>> permutationsII(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
permutations_ii.rs
/* バックトラッキング:順列 II */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // すべての選択肢を走査\n    let mut duplicated = HashSet::<i32>::new();\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if !selected[i] && !duplicated.contains(&choice) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.insert(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state.clone(), choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* 全順列 II */\nfn permutations_ii(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new();\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
permutations_ii.c
/* バックトラッキング:順列 II */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // すべての選択肢を走査\n    bool duplicated[MAX_SIZE] = {false};\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated[choice]) {\n            // 試行: 選択を行い、状態を更新\n            duplicated[choice] = true; // 選択済みの要素値を記録\n            selected[i] = true;\n            state[stateSize] = choice;\n            // 次の選択へ進む\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n        }\n    }\n}\n\n/* 全順列 II */\nint **permutationsII(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
permutations_ii.kt
/* バックトラッキング:順列 II */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // すべての選択肢を走査\n    val duplicated = HashSet<Int>()\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.add(choice) // 選択済みの要素値を記録\n            selected[i] = true\n            state.add(choice)\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* 全順列 II */\nfun permutationsII(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
permutations_ii.rb
### バックトラッキング法:全順列 II ###\ndef backtrack(state, choices, selected, res)\n  # 状態の長さが要素数に等しければ、解を記録\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # すべての選択肢を走査\n  duplicated = Set.new\n  choices.each_with_index do |choice, i|\n    # 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n    if !selected[i] && !duplicated.include?(choice)\n      # 試行: 選択を行い、状態を更新\n      duplicated.add(choice)\n      selected[i] = true\n      state << choice\n      # 次の選択へ進む\n      backtrack(state, choices, selected, res)\n      # バックトラック:選択を取り消し、前の状態に戻す\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### 全順列 II ###\ndef permutations_ii(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
コードの可視化

全画面で見る >

要素どうしがすべて互いに異なると仮定すると、\\(n\\) 個の要素には全部で \\(n!\\) 通りの順列(階乗)があります。結果を記録する際には、長さ \\(n\\) のリストをコピーする必要があり、これに \\(O(n)\\) 時間を要します。したがって時間計算量は \\(O(n!n)\\) です。

再帰の最大深さは \\(n\\) であり、\\(O(n)\\) のスタックフレーム空間を使います。selected は \\(O(n)\\) 空間を使用します。同時刻に存在する duplicated は最大で \\(n\\) 個であり、\\(O(n^2)\\) 空間を要します。したがって空間計算量は \\(O(n^2)\\) です。

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3-2","level":3,"title":"3.   2 種類の枝刈りの比較","text":"

selectedduplicated はどちらも枝刈りに用いられますが、目的は異なる点に注意してください。

  • 重複選択の枝刈り:探索全体を通して selected は 1 つだけです。これは現在の状態にどの要素が含まれているかを記録し、ある要素が state に重複して現れるのを防ぎます。
  • 等しい要素の枝刈り:各ラウンドの選択、すなわち各回の backtrack 呼び出しには duplicated が含まれます。これはそのラウンドの走査(for ループ)でどの要素がすでに選ばれたかを記録し、等しい要素が 1 回しか選ばれないことを保証します。

下図は、2 つの枝刈り条件が有効になる範囲を示しています。木の各ノードは 1 つの選択を表し、根ノードから葉ノードまでの経路上の各ノードが 1 つの順列を構成することに注意してください。

図 13-9   2 種類の枝刈り条件の作用範囲

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/","level":1,"title":"13.3   部分和問題","text":"","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1331","level":2,"title":"13.3.1   重複しない要素の場合","text":"

Question

正整数配列 nums と目標の正整数 target が与えられたとき、要素の和が target に等しくなるすべての組合せを見つけてください。配列に重複要素はなく、各要素は複数回選択できます。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。

例えば、入力集合 \\(\\{3, 4, 5\\}\\) と目標整数 \\(9\\) に対する解は \\(\\{3, 3, 3\\}, \\{4, 5\\}\\) です。次の 2 点に注意してください。

  • 入力集合内の要素は何度でも繰り返し選択できます。
  • 部分集合では要素の順序を区別しません。例えば \\(\\{4, 5\\}\\) と \\(\\{5, 4\\}\\) は同じ部分集合です。
","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1","level":3,"title":"1.   全順列の解法を参考にする","text":"

全順列問題と同様に、部分集合の生成過程を一連の選択結果として捉え、選択の過程で「要素の和」を逐次更新できます。そして要素の和が target に等しくなった時点で、その部分集合を結果リストに記録します。

ただし全順列問題と異なるのは、**この問題では集合内の要素を無制限に選択できる**点です。そのため、要素がすでに選択されたかどうかを記録する selected ブール配列は不要です。全順列のコードに少し修正を加えると、まず次の解法コードが得られます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i_naive.py
def backtrack(\n    state: list[int],\n    target: int,\n    total: int,\n    choices: list[int],\n    res: list[list[int]],\n):\n    \"\"\"バックトラッキング:部分和 I\"\"\"\n    # 部分集合の和が target に等しければ、解を記録\n    if total == target:\n        res.append(list(state))\n        return\n    # すべての選択肢を走査\n    for i in range(len(choices)):\n        # 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if total + choices[i] > target:\n            continue\n        # 試行:選択を行い、要素と total を更新する\n        state.append(choices[i])\n        # 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res)\n        # バックトラック:選択を取り消し、前の状態に戻す\n        state.pop()\n\ndef subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"部分和 I を解く(重複部分集合を含む)\"\"\"\n    state = []  # 状態(部分集合)\n    total = 0  # 部分和\n    res = []  # 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res)\n    return res\n
subset_sum_i_naive.cpp
/* バックトラッキング:部分和 I */\nvoid backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total == target) {\n        res.push_back(state);\n        return;\n    }\n    // すべての選択肢を走査\n    for (size_t i = 0; i < choices.size(); i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.push_back(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop_back();\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nvector<vector<int>> subsetSumINaive(vector<int> &nums, int target) {\n    vector<int> state;       // 状態(部分集合)\n    int total = 0;           // 部分和\n    vector<vector<int>> res; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.java
/* バックトラッキング:部分和 I */\nvoid backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total == target) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choices.length; i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.add(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.remove(state.size() - 1);\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nList<List<Integer>> subsetSumINaive(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // 状態(部分集合)\n    int total = 0; // 部分和\n    List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.cs
/* バックトラッキング:部分和 I */\nvoid Backtrack(List<int> state, int target, int total, int[] choices, List<List<int>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total == target) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choices.Length; i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.Add(choices[i]);\n        // 次の選択へ進む\n        Backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nList<List<int>> SubsetSumINaive(int[] nums, int target) {\n    List<int> state = []; // 状態(部分集合)\n    int total = 0; // 部分和\n    List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n    Backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.go
/* バックトラッキング:部分和 I */\nfunc backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == total {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // すべての選択肢を走査\n    for i := 0; i < len(*choices); i++ {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if total+(*choices)[i] > target {\n            continue\n        }\n        // 試行:選択を行い、要素と total を更新する\n        *state = append(*state, (*choices)[i])\n        // 次の選択へ進む\n        backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfunc subsetSumINaive(nums []int, target int) [][]int {\n    state := make([]int, 0) // 状態(部分集合)\n    total := 0              // 部分和\n    res := make([][]int, 0) // 結果リスト(部分集合のリスト)\n    backtrackSubsetSumINaive(total, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_i_naive.swift
/* バックトラッキング:部分和 I */\nfunc backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) {\n    // 部分集合の和が target に等しければ、解を記録\n    if total == target {\n        res.append(state)\n        return\n    }\n    // すべての選択肢を走査\n    for i in choices.indices {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if total + choices[i] > target {\n            continue\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.append(choices[i])\n        // 次の選択へ進む\n        backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeLast()\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfunc subsetSumINaive(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // 状態(部分集合)\n    let total = 0 // 部分和\n    var res: [[Int]] = [] // 結果リスト(部分集合のリスト)\n    backtrack(state: &state, target: target, total: total, choices: nums, res: &res)\n    return res\n}\n
subset_sum_i_naive.js
/* バックトラッキング:部分和 I */\nfunction backtrack(state, target, total, choices, res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    for (let i = 0; i < choices.length; i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfunction subsetSumINaive(nums, target) {\n    const state = []; // 状態(部分集合)\n    const total = 0; // 部分和\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.ts
/* バックトラッキング:部分和 I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    total: number,\n    choices: number[],\n    res: number[][]\n): void {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    for (let i = 0; i < choices.length; i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfunction subsetSumINaive(nums: number[], target: number): number[][] {\n    const state = []; // 状態(部分集合)\n    const total = 0; // 部分和\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.dart
/* バックトラッキング:部分和 I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  int total,\n  List<int> choices,\n  List<List<int>> res,\n) {\n  // 部分集合の和が target に等しければ、解を記録\n  if (total == target) {\n    res.add(List.from(state));\n    return;\n  }\n  // すべての選択肢を走査\n  for (int i = 0; i < choices.length; i++) {\n    // 枝刈り:部分和が target を超える場合はその選択をスキップする\n    if (total + choices[i] > target) {\n      continue;\n    }\n    // 試行:選択を行い、要素と total を更新する\n    state.add(choices[i]);\n    // 次の選択へ進む\n    backtrack(state, target, total + choices[i], choices, res);\n    // バックトラック:選択を取り消し、前の状態に戻す\n    state.removeLast();\n  }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nList<List<int>> subsetSumINaive(List<int> nums, int target) {\n  List<int> state = []; // 状態(部分集合)\n  int total = 0; // 要素の合計\n  List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n  backtrack(state, target, total, nums, res);\n  return res;\n}\n
subset_sum_i_naive.rs
/* バックトラッキング:部分和 I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    total: i32,\n    choices: &[i32],\n    res: &mut Vec<Vec<i32>>,\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if total == target {\n        res.push(state.clone());\n        return;\n    }\n    // すべての選択肢を走査\n    for i in 0..choices.len() {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if total + choices[i] > target {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // 状態(部分集合)\n    let total = 0; // 部分和\n    let mut res = Vec::new(); // 結果リスト(部分集合のリスト)\n    backtrack(&mut state, target, total, nums, &mut res);\n    res\n}\n
subset_sum_i_naive.c
/* バックトラッキング:部分和 I */\nvoid backtrack(int target, int total, int *choices, int choicesSize) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total == target) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choicesSize; i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state[stateSize++] = choices[i];\n        // 次の選択へ進む\n        backtrack(target, total + choices[i], choices, choicesSize);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        stateSize--;\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nvoid subsetSumINaive(int *nums, int numsSize, int target) {\n    resSize = 0; // 解の個数を 0 に初期化する\n    backtrack(target, 0, nums, numsSize);\n}\n
subset_sum_i_naive.kt
/* バックトラッキング:部分和 I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    total: Int,\n    choices: IntArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total == target) {\n        res.add(state.toMutableList())\n        return\n    }\n    // すべての選択肢を走査\n    for (i in choices.indices) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.add(choices[i])\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfun subsetSumINaive(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // 状態(部分集合)\n    val total = 0 // 部分和\n    val res = mutableListOf<MutableList<Int>?>() // 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res)\n    return res\n}\n
subset_sum_i_naive.rb
### バックトラッキング: 部分和 I ###\ndef backtrack(state, target, total, choices, res)\n  # 部分集合の和が target に等しければ、解を記録\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # すべての選択肢を走査\n  for i in 0...choices.length\n    # 枝刈り:部分和が target を超える場合はその選択をスキップする\n    next if total + choices[i] > target\n    # 試行:選択を行い、要素と total を更新する\n    state << choices[i]\n    # 次の選択へ進む\n    backtrack(state, target, total + choices[i], choices, res)\n    # バックトラック:選択を取り消し、前の状態に戻す\n    state.pop\n  end\nend\n\n### バックトラッキング: 部分和 I ###\ndef backtrack(state, target, total, choices, res)\n  # 部分集合の和が target に等しければ、解を記録\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # すべての選択肢を走査\n  for i in 0...choices.length\n    # 枝刈り:部分和が target を超える場合はその選択をスキップする\n    next if total + choices[i] > target\n    # 試行:選択を行い、要素と total を更新する\n    state << choices[i]\n    # 次の選択へ進む\n    backtrack(state, target, total + choices[i], choices, res)\n    # バックトラック:選択を取り消し、前の状態に戻す\n    state.pop\n  end\nend\n\n# ## 部分和 I を解く(重複する部分集合を含む)###\ndef subset_sum_i_naive(nums, target)\n  state = [] # 状態(部分集合)\n  total = 0 # 部分和\n  res = [] # 結果リスト(部分集合のリスト)\n  backtrack(state, target, total, nums, res)\n  res\nend\n
コードの可視化

全画面で見る >

上のコードに配列 \\([3, 4, 5]\\) と目標値 \\(9\\) を入力すると、出力は \\([3, 3, 3], [4, 5], [5, 4]\\) となります。和が \\(9\\) となる部分集合はすべて見つかっていますが、重複する部分集合 \\([4, 5]\\) と \\([5, 4]\\) が含まれています。

これは、探索過程では選択順を区別する一方で、部分集合では選択順を区別しないためです。次の図のように、先に \\(4\\) を選んでから \\(5\\) を選ぶ場合と、先に \\(5\\) を選んでから \\(4\\) を選ぶ場合は別の分岐ですが、対応する部分集合は同じです。

図 13-10   部分集合探索と境界超過の枝刈り

重複する部分集合を取り除くために、**直接的な方法として結果リストの重複を除去する**ことが考えられます。しかし、この方法は効率が低く、その理由は次の 2 点です。

  • 配列要素が多い場合、特に target が大きい場合には、探索過程で大量の重複部分集合が生成されます。
  • 部分集合(配列)同士の違いを比較するのは非常に時間がかかり、まず配列をソートし、その後に各要素を比較する必要があります。
","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2","level":3,"title":"2.   重複部分集合の枝刈り","text":"

**探索過程で枝刈りを行って重複を除去する**ことを考えます。次の図を観察すると、重複部分集合は配列要素を異なる順序で選択したときに生じます。例えば次のような状況です。

  1. 1 回目と 2 回目でそれぞれ \\(3\\) と \\(4\\) を選ぶと、これら 2 要素を含むすべての部分集合、すなわち \\([3, 4, \\dots]\\) が生成されます。
  2. その後、1 回目で \\(4\\) を選んだ場合、**2 回目では \\(3\\) をスキップすべき**です。というのも、この選択で生成される部分集合 \\([4, 3, \\dots]\\) は、手順 1. で生成された部分集合と完全に重複するからです。

探索過程では、各階層の選択は左から右へ順に試されるため、右側にある分岐ほど多く枝刈りされます。

  1. 最初の 2 回で \\(3\\) と \\(5\\) を選ぶと、部分集合 \\([3, 5, \\dots]\\) が生成されます。
  2. 最初の 2 回で \\(4\\) と \\(5\\) を選ぶと、部分集合 \\([4, 5, \\dots]\\) が生成されます。
  3. もし 1 回目で \\(5\\) を選ぶなら、**2 回目では \\(3\\) と \\(4\\) をスキップすべき**です。なぜなら、部分集合 \\([5, 3, \\dots]\\) と \\([5, 4, \\dots]\\) は、手順 1. と手順 2. で述べた部分集合と完全に重複するからです。

図 13-11   異なる選択順によって生じる重複部分集合

まとめると、入力配列 \\([x_1, x_2, \\dots, x_n]\\) が与えられ、探索過程における選択列を \\([x_{i_1}, x_{i_2}, \\dots, x_{i_m}]\\) とすると、この選択列は \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\) を満たす必要があります。この条件を満たさない選択列は重複を生むため、枝刈りすべきです。

","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#3","level":3,"title":"3.   コード実装","text":"

この枝刈りを実現するために、走査の開始位置を示す変数 start を初期化します。**選択 \\(x_{i}\\) を行った後、次のラウンドはインデックス \\(i\\) から走査を開始する**ように設定します。これにより、選択列が \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\) を満たし、部分集合の一意性が保証されます。

これに加えて、コードには次の 2 つの最適化も施しています。

  • 探索を始める前に、まず配列 nums をソートします。すべての選択肢を走査するとき、**部分集合の和が target を超えたら直ちにループを終了**します。後続の要素はさらに大きいため、その和も必ず target を超えるからです。
  • 要素和を保持する変数 total は省略し、**target から減算することで要素和を管理**します。target が \\(0\\) になったときに解を記録します。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i.py
def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"バックトラッキング:部分和 I\"\"\"\n    # 部分集合の和が target に等しければ、解を記録\n    if target == 0:\n        res.append(list(state))\n        return\n    # すべての選択肢を走査\n    # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for i in range(start, len(choices)):\n        # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0:\n            break\n        # 試す:選択を行い、target と start を更新\n        state.append(choices[i])\n        # 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res)\n        # バックトラック:選択を取り消し、前の状態に戻す\n        state.pop()\n\ndef subset_sum_i(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"部分和 I を解く\"\"\"\n    state = []  # 状態(部分集合)\n    nums.sort()  # nums をソート\n    start = 0  # 開始点を走査\n    res = []  # 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res)\n    return res\n
subset_sum_i.cpp
/* バックトラッキング:部分和 I */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (int i = start; i < choices.size(); i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push_back(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop_back();\n    }\n}\n\n/* 部分和 I を解く */\nvector<vector<int>> subsetSumI(vector<int> &nums, int target) {\n    vector<int> state;              // 状態(部分集合)\n    sort(nums.begin(), nums.end()); // nums をソート\n    int start = 0;                  // 開始点を走査\n    vector<vector<int>> res;        // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.java
/* バックトラッキング:部分和 I */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (int i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.add(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.remove(state.size() - 1);\n    }\n}\n\n/* 部分和 I を解く */\nList<List<Integer>> subsetSumI(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // 状態(部分集合)\n    Arrays.sort(nums); // nums をソート\n    int start = 0; // 開始点を走査\n    List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.cs
/* バックトラッキング:部分和 I */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (int i = start; i < choices.Length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.Add(choices[i]);\n        // 次の選択へ進む\n        Backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* 部分和 I を解く */\nList<List<int>> SubsetSumI(int[] nums, int target) {\n    List<int> state = []; // 状態(部分集合)\n    Array.Sort(nums); // nums をソート\n    int start = 0; // 開始点を走査\n    List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.go
/* バックトラッキング:部分和 I */\nfunc backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for i := start; i < len(*choices); i++ {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // 試す:選択を行い、target と start を更新\n        *state = append(*state, (*choices)[i])\n        // 次の選択へ進む\n        backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* 部分和 I を解く */\nfunc subsetSumI(nums []int, target int) [][]int {\n    state := make([]int, 0) // 状態(部分集合)\n    sort.Ints(nums)         // nums をソート\n    start := 0              // 開始点を走査\n    res := make([][]int, 0) // 結果リスト(部分集合のリスト)\n    backtrackSubsetSumI(start, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_i.swift
/* バックトラッキング:部分和 I */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for i in choices.indices.dropFirst(start) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0 {\n            break\n        }\n        // 試す:選択を行い、target と start を更新\n        state.append(choices[i])\n        // 次の選択へ進む\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeLast()\n    }\n}\n\n/* 部分和 I を解く */\nfunc subsetSumI(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // 状態(部分集合)\n    let nums = nums.sorted() // nums をソート\n    let start = 0 // 開始点を走査\n    var res: [[Int]] = [] // 結果リスト(部分集合のリスト)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
subset_sum_i.js
/* バックトラッキング:部分和 I */\nfunction backtrack(state, target, choices, start, res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (let i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く */\nfunction subsetSumI(nums, target) {\n    const state = []; // 状態(部分集合)\n    nums.sort((a, b) => a - b); // nums をソート\n    const start = 0; // 開始点を走査\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.ts
/* バックトラッキング:部分和 I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (let i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く */\nfunction subsetSumI(nums: number[], target: number): number[][] {\n    const state = []; // 状態(部分集合)\n    nums.sort((a, b) => a - b); // nums をソート\n    const start = 0; // 開始点を走査\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.dart
/* バックトラッキング:部分和 I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // 部分集合の和が target に等しければ、解を記録\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // すべての選択肢を走査\n  // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n  for (int i = start; i < choices.length; i++) {\n    // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n    // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // 試す:選択を行い、target と start を更新\n    state.add(choices[i]);\n    // 次の選択へ進む\n    backtrack(state, target - choices[i], choices, i, res);\n    // バックトラック:選択を取り消し、前の状態に戻す\n    state.removeLast();\n  }\n}\n\n/* 部分和 I を解く */\nList<List<int>> subsetSumI(List<int> nums, int target) {\n  List<int> state = []; // 状態(部分集合)\n  nums.sort(); // nums をソート\n  int start = 0; // 開始点を走査\n  List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
subset_sum_i.rs
/* バックトラッキング:部分和 I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for i in start..choices.len() {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0 {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く */\nfn subset_sum_i(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // 状態(部分集合)\n    nums.sort(); // nums をソート\n    let start = 0; // 開始点を走査\n    let mut res = Vec::new(); // 結果リスト(部分集合のリスト)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
subset_sum_i.c
/* バックトラッキング:部分和 I */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        for (int i = 0; i < stateSize; ++i) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (int i = start; i < choicesSize; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state[stateSize] = choices[i];\n        stateSize++;\n        // 次の選択へ進む\n        backtrack(target - choices[i], choices, choicesSize, i);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        stateSize--;\n    }\n}\n\n/* 部分和 I を解く */\nvoid subsetSumI(int *nums, int numsSize, int target) {\n    qsort(nums, numsSize, sizeof(int), cmp); // nums をソート\n    int start = 0;                           // 開始点を走査\n    backtrack(target, nums, numsSize, start);\n}\n
subset_sum_i.kt
/* バックトラッキング:部分和 I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (i in start..<choices.size) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break\n        }\n        // 試す:選択を行い、target と start を更新\n        state.add(choices[i])\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* 部分和 I を解く */\nfun subsetSumI(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // 状態(部分集合)\n    nums.sort() // nums をソート\n    val start = 0 // 開始点を走査\n    val res = mutableListOf<MutableList<Int>?>() // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
subset_sum_i.rb
### バックトラッキング: 部分和 I ###\ndef backtrack(state, target, choices, start, res)\n  # 部分集合の和が target に等しければ、解を記録\n  if target.zero?\n    res << state.dup\n    return\n  end\n  # すべての選択肢を走査\n  # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n  for i in start...choices.length\n    # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n    # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n    break if target - choices[i] < 0\n    # 試す:選択を行い、target と start を更新\n    state << choices[i]\n    # 次の選択へ進む\n    backtrack(state, target - choices[i], choices, i, res)\n    # バックトラック:選択を取り消し、前の状態に戻す\n    state.pop\n  end\nend\n\n### 部分和 I を解く ###\ndef subset_sum_i(nums, target)\n  state = [] # 状態(部分集合)\n  nums.sort! # nums をソート\n  start = 0 # 開始点を走査\n  res = [] # 結果リスト(部分集合のリスト)\n  backtrack(state, target, nums, start, res)\n  res\nend\n
コードの可視化

全画面で見る >

次の図は、配列 \\([3, 4, 5]\\) と目標値 \\(9\\) を上のコードに入力したときの、全体のバックトラッキング過程を示しています。

図 13-12   部分和 I のバックトラッキング過程

","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1332","level":2,"title":"13.3.2   重複要素を考慮する場合","text":"

Question

正整数配列 nums と目標の正整数 target が与えられたとき、要素の和が target に等しくなるすべての組合せを見つけてください。与えられた配列には重複要素が含まれる可能性があり、各要素は 1 回しか選択できません。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。

前問と比べると、この問題の入力配列には重複要素が含まれる可能性があります。そのため、新たな問題が生じます。例えば、配列 \\([4, \\hat{4}, 5]\\) と目標値 \\(9\\) が与えられると、既存コードの出力は \\([4, 5], [\\hat{4}, 5]\\) となり、重複部分集合が現れます。

この重複が生じる原因は、同じ値の要素があるラウンドで複数回選ばれてしまうことにあります。次の図では、1 回目には 3 つの選択肢があり、そのうち 2 つはどちらも \\(4\\) です。これにより 2 本の重複した探索分岐が生じ、重複部分集合が出力されます。同様に、2 回目の 2 つの \\(4\\) も重複部分集合を生みます。

図 13-13   等しい要素によって生じる重複部分集合

","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1_1","level":3,"title":"1.   等しい要素の枝刈り","text":"

この問題を解決するには、各ラウンドで等しい要素が 1 回しか選ばれないように制限する必要があります。実装方法は巧妙です。配列はすでにソートされているため、等しい要素は必ず隣り合っています。したがって、あるラウンドの選択で現在の要素が左隣の要素と等しいなら、それはすでに選ばれたことを意味するので、その要素を直接スキップします。

同時に、**この問題では各配列要素を 1 回しか選択できない**という制約もあります。幸い、この制約も変数 start を使って満たせます。すなわち、選択 \\(x_{i}\\) を行った後、次のラウンドはインデックス \\(i + 1\\) から後ろへ走査するよう設定します。これにより、重複部分集合を除去できるだけでなく、同じ要素を繰り返し選ぶことも防げます。

","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2_1","level":3,"title":"2.   コード実装","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_ii.py
def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"バックトラッキング:部分和 II\"\"\"\n    # 部分集合の和が target に等しければ、解を記録\n    if target == 0:\n        res.append(list(state))\n        return\n    # すべての選択肢を走査\n    # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    # 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for i in range(start, len(choices)):\n        # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0:\n            break\n        # 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if i > start and choices[i] == choices[i - 1]:\n            continue\n        # 試す:選択を行い、target と start を更新\n        state.append(choices[i])\n        # 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        # バックトラック:選択を取り消し、前の状態に戻す\n        state.pop()\n\ndef subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"部分和 II を解く\"\"\"\n    state = []  # 状態(部分集合)\n    nums.sort()  # nums をソート\n    start = 0  # 開始点を走査\n    res = []  # 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res)\n    return res\n
subset_sum_ii.cpp
/* バックトラッキング:部分和 II */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (int i = start; i < choices.size(); i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push_back(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop_back();\n    }\n}\n\n/* 部分和 II を解く */\nvector<vector<int>> subsetSumII(vector<int> &nums, int target) {\n    vector<int> state;              // 状態(部分集合)\n    sort(nums.begin(), nums.end()); // nums をソート\n    int start = 0;                  // 開始点を走査\n    vector<vector<int>> res;        // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.java
/* バックトラッキング:部分和 II */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (int i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.add(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.remove(state.size() - 1);\n    }\n}\n\n/* 部分和 II を解く */\nList<List<Integer>> subsetSumII(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // 状態(部分集合)\n    Arrays.sort(nums); // nums をソート\n    int start = 0; // 開始点を走査\n    List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.cs
/* バックトラッキング:部分和 II */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (int i = start; i < choices.Length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.Add(choices[i]);\n        // 次の選択へ進む\n        Backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* 部分和 II を解く */\nList<List<int>> SubsetSumII(int[] nums, int target) {\n    List<int> state = []; // 状態(部分集合)\n    Array.Sort(nums); // nums をソート\n    int start = 0; // 開始点を走査\n    List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.go
/* バックトラッキング:部分和 II */\nfunc backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for i := start; i < len(*choices); i++ {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if i > start && (*choices)[i] == (*choices)[i-1] {\n            continue\n        }\n        // 試す:選択を行い、target と start を更新\n        *state = append(*state, (*choices)[i])\n        // 次の選択へ進む\n        backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* 部分和 II を解く */\nfunc subsetSumII(nums []int, target int) [][]int {\n    state := make([]int, 0) // 状態(部分集合)\n    sort.Ints(nums)         // nums をソート\n    start := 0              // 開始点を走査\n    res := make([][]int, 0) // 結果リスト(部分集合のリスト)\n    backtrackSubsetSumII(start, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_ii.swift
/* バックトラッキング:部分和 II */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for i in choices.indices.dropFirst(start) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0 {\n            break\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if i > start, choices[i] == choices[i - 1] {\n            continue\n        }\n        // 試す:選択を行い、target と start を更新\n        state.append(choices[i])\n        // 次の選択へ進む\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeLast()\n    }\n}\n\n/* 部分和 II を解く */\nfunc subsetSumII(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // 状態(部分集合)\n    let nums = nums.sorted() // nums をソート\n    let start = 0 // 開始点を走査\n    var res: [[Int]] = [] // 結果リスト(部分集合のリスト)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
subset_sum_ii.js
/* バックトラッキング:部分和 II */\nfunction backtrack(state, target, choices, start, res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (let i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 II を解く */\nfunction subsetSumII(nums, target) {\n    const state = []; // 状態(部分集合)\n    nums.sort((a, b) => a - b); // nums をソート\n    const start = 0; // 開始点を走査\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.ts
/* バックトラッキング:部分和 II */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (let i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 II を解く */\nfunction subsetSumII(nums: number[], target: number): number[][] {\n    const state = []; // 状態(部分集合)\n    nums.sort((a, b) => a - b); // nums をソート\n    const start = 0; // 開始点を走査\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.dart
/* バックトラッキング:部分和 II */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // 部分集合の和が target に等しければ、解を記録\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // すべての選択肢を走査\n  // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n  // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n  for (int i = start; i < choices.length; i++) {\n    // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n    // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n    if (i > start && choices[i] == choices[i - 1]) {\n      continue;\n    }\n    // 試す:選択を行い、target と start を更新\n    state.add(choices[i]);\n    // 次の選択へ進む\n    backtrack(state, target - choices[i], choices, i + 1, res);\n    // バックトラック:選択を取り消し、前の状態に戻す\n    state.removeLast();\n  }\n}\n\n/* 部分和 II を解く */\nList<List<int>> subsetSumII(List<int> nums, int target) {\n  List<int> state = []; // 状態(部分集合)\n  nums.sort(); // nums をソート\n  int start = 0; // 開始点を走査\n  List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
subset_sum_ii.rs
/* バックトラッキング:部分和 II */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for i in start..choices.len() {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0 {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if i > start && choices[i] == choices[i - 1] {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 II を解く */\nfn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // 状態(部分集合)\n    nums.sort(); // nums をソート\n    let start = 0; // 開始点を走査\n    let mut res = Vec::new(); // 結果リスト(部分集合のリスト)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
subset_sum_ii.c
/* バックトラッキング:部分和 II */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (int i = start; i < choicesSize; i++) {\n        // 枝刈り 1: 部分集合の和が target を超えたら、そのままスキップする\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state[stateSize] = choices[i];\n        stateSize++;\n        // 次の選択へ進む\n        backtrack(target - choices[i], choices, choicesSize, i + 1);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        stateSize--;\n    }\n}\n\n/* 部分和 II を解く */\nvoid subsetSumII(int *nums, int numsSize, int target) {\n    // nums をソート\n    qsort(nums, numsSize, sizeof(int), cmp);\n    // バックトラッキングを開始\n    backtrack(target, nums, numsSize, 0);\n}\n
subset_sum_ii.kt
/* バックトラッキング:部分和 II */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (i in start..<choices.size) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue\n        }\n        // 試す:選択を行い、target と start を更新\n        state.add(choices[i])\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* 部分和 II を解く */\nfun subsetSumII(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // 状態(部分集合)\n    nums.sort() // nums をソート\n    val start = 0 // 開始点を走査\n    val res = mutableListOf<MutableList<Int>?>() // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
subset_sum_ii.rb
### バックトラッキング法:部分和 II ###\ndef backtrack(state, target, choices, start, res)\n  # 部分集合の和が target に等しければ、解を記録\n  if target.zero?\n    res << state.dup\n    return\n  end\n\n  # すべての選択肢を走査\n  # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n  # 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n  for i in start...choices.length\n    # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n    # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n    break if target - choices[i] < 0\n    # 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n    next if i > start && choices[i] == choices[i - 1]\n    # 試す:選択を行い、target と start を更新\n    state << choices[i]\n    # 次の選択へ進む\n    backtrack(state, target - choices[i], choices, i + 1, res)\n    # バックトラック:選択を取り消し、前の状態に戻す\n    state.pop\n  end\nend\n\n### 部分和 II を解く ###\ndef subset_sum_ii(nums, target)\n  state = [] # 状態(部分集合)\n  nums.sort! # nums をソート\n  start = 0 # 開始点を走査\n  res = [] # 結果リスト(部分集合のリスト)\n  backtrack(state, target, nums, start, res)\n  res\nend\n
コードの可視化

全画面で見る >

次の図は、配列 \\([4, 4, 5]\\) と目標値 \\(9\\) に対するバックトラッキング過程を示しており、全部で 4 種類の枝刈り操作が含まれています。図とコードコメントを対応させながら、探索全体の流れと、各枝刈り操作がどのように機能するかを理解してください。

図 13-14   部分和 II のバックトラッキング過程

","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/summary/","level":1,"title":"13.5   まとめ","text":"","path":["第 13 章   バックトラッキング","13.5   まとめ"],"tags":[]},{"location":"chapter_backtracking/summary/#1","level":3,"title":"1.   重要なポイントの振り返り","text":"
  • バックトラッキングアルゴリズムの本質は全探索法であり、解空間を深さ優先で走査することで条件を満たす解を探索します。探索の過程で条件を満たす解に出会ったら記録し、すべての解を見つけるか探索が完了するまで続けます。
  • バックトラッキングアルゴリズムの探索過程は、試行と戻るという 2 つの部分から成ります。深さ優先探索によってさまざまな選択を試し、制約条件を満たさない状況に遭遇した場合は直前の選択を取り消して前の状態に戻り、ほかの選択を引き続き試します。試行と戻るは互いに逆方向の操作です。
  • バックトラッキング問題には通常複数の制約条件が含まれており、それらを枝刈りに利用できます。枝刈りによって不要な探索分岐を早期に打ち切り、探索効率を大幅に高められます。
  • バックトラッキングアルゴリズムは主に探索問題と制約充足問題の解決に用いられます。組合せ最適化問題もバックトラッキングで解けますが、より高効率またはより適した解法が存在することが少なくありません。
  • 全順列問題の目的は、与えられた集合要素のすべての可能な並べ方を探索することです。各要素が選択済みかどうかを配列で記録し、同じ要素を重複して選ぶ探索分岐を刈り取ることで、各要素が 1 度だけ選ばれるようにします。
  • 全順列問題では、集合内に重複要素があると最終結果にも重複した順列が現れます。各ラウンドで等しい要素は 1 回しか選べないように制約する必要があり、通常はハッシュ集合を用いて実現します。
  • 部分和問題の目標は、与えられた集合の中から和が目標値となるすべての部分集合を見つけることです。集合では要素順序を区別しませんが、探索過程では順序違いの結果も出力されるため、重複部分集合が生じます。そこで、バックトラッキング前にデータをソートし、各ラウンドの走査開始位置を示す変数を設定することで、重複部分集合を生成する探索分岐を枝刈りします。
  • 部分和問題では、配列中の等しい要素が重複集合を生みます。配列がソート済みであるという前提を利用し、隣接要素が等しいかどうかを判定して枝刈りすることで、等しい要素が各ラウンドで 1 回しか選ばれないようにします。
  • \\(n\\) クイーン問題の目的は、\\(n \\times n\\) の盤面に \\(n\\) 個のクイーンを配置する方法を見つけることであり、どの 2 つのクイーンも互いに攻撃できないことが条件です。この問題の制約には行制約、列制約、主対角線制約、副対角線制約があります。行制約を満たすため、行ごとに配置する戦略を採用し、各行に 1 個のクイーンを置くことを保証します。
  • 列制約と対角線制約の扱い方は似ています。列制約については、各列にクイーンが存在するかどうかを配列で記録し、選択したマスが有効かどうかを判定します。対角線制約については、主対角線と副対角線それぞれにクイーンが存在するかを 2 つの配列で記録します。難点は、同じ主対角線または副対角線上にあるマスが満たす行列インデックスの規則を見つけることにあります。
","path":["第 13 章   バックトラッキング","13.5   まとめ"],"tags":[]},{"location":"chapter_backtracking/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:バックトラッキングと再帰の関係はどのように理解すればよいですか?

全体として見ると、バックトラッキングは「アルゴリズム戦略」の一種であり、再帰はむしろ「道具」に近いものです。

  • バックトラッキングアルゴリズムは通常、再帰に基づいて実装されます。ただし、バックトラッキングは再帰の応用場面の 1 つであり、探索問題における再帰の応用です。
  • 再帰の構造は「部分問題への分解」という問題解決パラダイムを表しており、分割統治、バックトラッキング、動的計画法(メモ化再帰)などの問題によく用いられます。
","path":["第 13 章   バックトラッキング","13.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"第 2 章   計算量解析","text":"

Abstract

計算量解析は、広大なアルゴリズム宇宙における時空の案内人のようなものです。

それは、時間と空間という二つの次元で私たちをより深く探求へ導き、より洗練された解決策を見つけ出します。

","path":["第 2 章   計算量解析"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"章の内容","text":"
  • 2.1   アルゴリズム効率の評価
  • 2.2   反復と再帰
  • 2.3   時間計算量
  • 2.4   空間計算量
  • 2.5   まとめ
","path":["第 2 章   計算量解析"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   反復と再帰","text":"

アルゴリズムでは、ある処理を繰り返し実行することがよくあり、これは複雑度解析と密接に関係しています。そのため、時間計算量と空間計算量を紹介する前に、まずプログラム内で反復実行を実現する方法、つまり 2 つの基本的な制御構造である反復と再帰について見ていきます。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#221","level":2,"title":"2.2.1   反復","text":"

反復(iteration)は、ある処理を繰り返し実行するための制御構造です。反復では、プログラムは一定の条件を満たす間、あるコード片を繰り返し実行し、その条件を満たさなくなるまで続けます。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-for","level":3,"title":"1.   for ループ","text":"

for ループは最も一般的な反復形式の 1 つで、反復回数があらかじめ分かっている場合に適しています。

次の関数は for ループを用いて \\(1 + 2 + \\dots + n\\) の総和を計算しており、その結果は変数 res に記録されます。なお、Python の range(a, b) に対応する区間は「左閉右開」であり、走査範囲は \\(a, a + 1, \\dots, b-1\\) です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def for_loop(n: int) -> int:\n    \"\"\"for ループ\"\"\"\n    res = 0\n    # 1, 2, ..., n-1, n を順に加算する\n    for i in range(1, n + 1):\n        res += i\n    return res\n
iteration.cpp
/* for ループ */\nint forLoop(int n) {\n    int res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (int i = 1; i <= n; ++i) {\n        res += i;\n    }\n    return res;\n}\n
iteration.java
/* for ループ */\nint forLoop(int n) {\n    int res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.cs
/* for ループ */\nint ForLoop(int n) {\n    int res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.go
/* for ループ */\nfunc forLoop(n int) int {\n    res := 0\n    // 1, 2, ..., n-1, n を順に加算する\n    for i := 1; i <= n; i++ {\n        res += i\n    }\n    return res\n}\n
iteration.swift
/* for ループ */\nfunc forLoop(n: Int) -> Int {\n    var res = 0\n    // 1, 2, ..., n-1, n を順に加算する\n    for i in 1 ... n {\n        res += i\n    }\n    return res\n}\n
iteration.js
/* for ループ */\nfunction forLoop(n) {\n    let res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.ts
/* for ループ */\nfunction forLoop(n: number): number {\n    let res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.dart
/* for ループ */\nint forLoop(int n) {\n  int res = 0;\n  // 1, 2, ..., n-1, n を順に加算する\n  for (int i = 1; i <= n; i++) {\n    res += i;\n  }\n  return res;\n}\n
iteration.rs
/* for ループ */\nfn for_loop(n: i32) -> i32 {\n    let mut res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for i in 1..=n {\n        res += i;\n    }\n    res\n}\n
iteration.c
/* for ループ */\nint forLoop(int n) {\n    int res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.kt
/* for ループ */\nfun forLoop(n: Int): Int {\n    var res = 0\n    // 1, 2, ..., n-1, n を順に加算する\n    for (i in 1..n) {\n        res += i\n    }\n    return res\n}\n
iteration.rb
### for ループ ###\ndef for_loop(n)\n  res = 0\n\n  # 1, 2, ..., n-1, n を順に加算する\n  for i in 1..n\n    res += i\n  end\n\n  res\nend\n
コードの可視化

全画面で見る >

次の図は、この総和関数のフローチャートです。

図 2-1   総和関数のフローチャート

この総和関数の操作回数は入力データサイズ \\(n\\) に比例し、言い換えれば「線形関係」にあります。実際、時間計算量が記述するのはこの「線形関係」そのものです。関連内容は次節で詳しく説明します。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-while","level":3,"title":"2.   while ループ","text":"

for ループと同様に、while ループも反復を実現する方法の 1 つです。while ループでは、各反復のたびにまず条件を確認し、条件が真であれば実行を続け、そうでなければループを終了します。

次に、while ループを使って \\(1 + 2 + \\dots + n\\) の総和を求めてみましょう。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def while_loop(n: int) -> int:\n    \"\"\"while ループ\"\"\"\n    res = 0\n    i = 1  # 条件変数を初期化する\n    # 1, 2, ..., n-1, n を順に加算する\n    while i <= n:\n        res += i\n        i += 1  # 条件変数を更新する\n    return res\n
iteration.cpp
/* while ループ */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i++; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.java
/* while ループ */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i++; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.cs
/* while ループ */\nint WhileLoop(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i += 1; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.go
/* while ループ */\nfunc whileLoop(n int) int {\n    res := 0\n    // 条件変数を初期化する\n    i := 1\n    // 1, 2, ..., n-1, n を順に加算する\n    for i <= n {\n        res += i\n        // 条件変数を更新する\n        i++\n    }\n    return res\n}\n
iteration.swift
/* while ループ */\nfunc whileLoop(n: Int) -> Int {\n    var res = 0\n    var i = 1 // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while i <= n {\n        res += i\n        i += 1 // 条件変数を更新する\n    }\n    return res\n}\n
iteration.js
/* while ループ */\nfunction whileLoop(n) {\n    let res = 0;\n    let i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i++; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.ts
/* while ループ */\nfunction whileLoop(n: number): number {\n    let res = 0;\n    let i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i++; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.dart
/* while ループ */\nint whileLoop(int n) {\n  int res = 0;\n  int i = 1; // 条件変数を初期化する\n  // 1, 2, ..., n-1, n を順に加算する\n  while (i <= n) {\n    res += i;\n    i++; // 条件変数を更新する\n  }\n  return res;\n}\n
iteration.rs
/* while ループ */\nfn while_loop(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // 条件変数を初期化する\n\n    // 1, 2, ..., n-1, n を順に加算する\n    while i <= n {\n        res += i;\n        i += 1; // 条件変数を更新する\n    }\n    res\n}\n
iteration.c
/* while ループ */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i++; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.kt
/* while ループ */\nfun whileLoop(n: Int): Int {\n    var res = 0\n    var i = 1 // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i\n        i++ // 条件変数を更新する\n    }\n    return res\n}\n
iteration.rb
### while ループ ###\ndef while_loop(n)\n  res = 0\n  i = 1 # 条件変数を初期化する\n\n  # 1, 2, ..., n-1, n を順に加算する\n  while i <= n\n    res += i\n    i += 1 # 条件変数を更新する\n  end\n\n  res\nend\n
コードの可視化

全画面で見る >

**while ループは for ループより自由度が高い**です。while ループでは、条件変数の初期化や更新手順を柔軟に設計できます。

たとえば次のコードでは、条件変数 \\(i\\) が各反復で 2 回更新されており、このようなケースは for ループではあまり扱いやすくありません。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def while_loop_ii(n: int) -> int:\n    \"\"\"while ループ(2回更新)\"\"\"\n    res = 0\n    i = 1  # 条件変数を初期化する\n    # 1, 4, 10, ... を順に加算する\n    while i <= n:\n        res += i\n        # 条件変数を更新する\n        i += 1\n        i *= 2\n    return res\n
iteration.cpp
/* while ループ(2回更新) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.java
/* while ループ(2回更新) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.cs
/* while ループ(2回更新) */\nint WhileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i += 1; \n        i *= 2;\n    }\n    return res;\n}\n
iteration.go
/* while ループ(2回更新) */\nfunc whileLoopII(n int) int {\n    res := 0\n    // 条件変数を初期化する\n    i := 1\n    // 1, 4, 10, ... を順に加算する\n    for i <= n {\n        res += i\n        // 条件変数を更新する\n        i++\n        i *= 2\n    }\n    return res\n}\n
iteration.swift
/* while ループ(2回更新) */\nfunc whileLoopII(n: Int) -> Int {\n    var res = 0\n    var i = 1 // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while i <= n {\n        res += i\n        // 条件変数を更新する\n        i += 1\n        i *= 2\n    }\n    return res\n}\n
iteration.js
/* while ループ(2回更新) */\nfunction whileLoopII(n) {\n    let res = 0;\n    let i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.ts
/* while ループ(2回更新) */\nfunction whileLoopII(n: number): number {\n    let res = 0;\n    let i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.dart
/* while ループ(2回更新) */\nint whileLoopII(int n) {\n  int res = 0;\n  int i = 1; // 条件変数を初期化する\n  // 1, 4, 10, ... を順に加算する\n  while (i <= n) {\n    res += i;\n    // 条件変数を更新する\n    i++;\n    i *= 2;\n  }\n  return res;\n}\n
iteration.rs
/* while ループ(2回更新) */\nfn while_loop_ii(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // 条件変数を初期化する\n\n    // 1, 4, 10, ... を順に加算する\n    while i <= n {\n        res += i;\n        // 条件変数を更新する\n        i += 1;\n        i *= 2;\n    }\n    res\n}\n
iteration.c
/* while ループ(2回更新) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.kt
/* while ループ(2回更新) */\nfun whileLoopII(n: Int): Int {\n    var res = 0\n    var i = 1 // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i\n        // 条件変数を更新する\n        i++\n        i *= 2\n    }\n    return res\n}\n
iteration.rb
### while ループ ###\ndef while_loop(n)\n  res = 0\n  i = 1 # 条件変数を初期化する\n\n  # 1, 2, ..., n-1, n を順に加算する\n  while i <= n\n    res += i\n    i += 1 # 条件変数を更新する\n  end\n\n  res\nend\n\n# ## while ループ(2 回更新)###\ndef while_loop_ii(n)\n  res = 0\n  i = 1 # 条件変数を初期化する\n\n  # 1, 4, 10, ... を順に加算する\n  while i <= n\n    res += i\n    # 条件変数を更新する\n    i += 1\n    i *= 2\n  end\n\n  res\nend\n
コードの可視化

全画面で見る >

総じて、**for ループのコードはより簡潔で、while ループはより柔軟**です。どちらも反復構造を実現できますが、どちらを使うかは問題ごとの要件に応じて決めるべきです。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3","level":3,"title":"3.   ネストしたループ","text":"

1 つのループ構造の中に別のループ構造を入れ子にできます。以下では for ループを例にします。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def nested_for_loop(n: int) -> str:\n    \"\"\"二重 for ループ\"\"\"\n    res = \"\"\n    # i = 1, 2, ..., n-1, n とループする\n    for i in range(1, n + 1):\n        # j = 1, 2, ..., n-1, n とループする\n        for j in range(1, n + 1):\n            res += f\"({i}, {j}), \"\n    return res\n
iteration.cpp
/* 二重 for ループ */\nstring nestedForLoop(int n) {\n    ostringstream res;\n    // i = 1, 2, ..., n-1, n とループする\n    for (int i = 1; i <= n; ++i) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (int j = 1; j <= n; ++j) {\n            res << \"(\" << i << \", \" << j << \"), \";\n        }\n    }\n    return res.str();\n}\n
iteration.java
/* 二重 for ループ */\nString nestedForLoop(int n) {\n    StringBuilder res = new StringBuilder();\n    // i = 1, 2, ..., n-1, n とループする\n    for (int i = 1; i <= n; i++) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (int j = 1; j <= n; j++) {\n            res.append(\"(\" + i + \", \" + j + \"), \");\n        }\n    }\n    return res.toString();\n}\n
iteration.cs
/* 二重 for ループ */\nstring NestedForLoop(int n) {\n    StringBuilder res = new();\n    // i = 1, 2, ..., n-1, n とループする\n    for (int i = 1; i <= n; i++) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (int j = 1; j <= n; j++) {\n            res.Append($\"({i}, {j}), \");\n        }\n    }\n    return res.ToString();\n}\n
iteration.go
/* 二重 for ループ */\nfunc nestedForLoop(n int) string {\n    res := \"\"\n    // i = 1, 2, ..., n-1, n とループする\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= n; j++ {\n            // j = 1, 2, ..., n-1, n とループする\n            res += fmt.Sprintf(\"(%d, %d), \", i, j)\n        }\n    }\n    return res\n}\n
iteration.swift
/* 二重 for ループ */\nfunc nestedForLoop(n: Int) -> String {\n    var res = \"\"\n    // i = 1, 2, ..., n-1, n とループする\n    for i in 1 ... n {\n        // j = 1, 2, ..., n-1, n とループする\n        for j in 1 ... n {\n            res.append(\"(\\(i), \\(j)), \")\n        }\n    }\n    return res\n}\n
iteration.js
/* 二重 for ループ */\nfunction nestedForLoop(n) {\n    let res = '';\n    // i = 1, 2, ..., n-1, n とループする\n    for (let i = 1; i <= n; i++) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
iteration.ts
/* 二重 for ループ */\nfunction nestedForLoop(n: number): string {\n    let res = '';\n    // i = 1, 2, ..., n-1, n とループする\n    for (let i = 1; i <= n; i++) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
iteration.dart
/* 二重 for ループ */\nString nestedForLoop(int n) {\n  String res = \"\";\n  // i = 1, 2, ..., n-1, n とループする\n  for (int i = 1; i <= n; i++) {\n    // j = 1, 2, ..., n-1, n とループする\n    for (int j = 1; j <= n; j++) {\n      res += \"($i, $j), \";\n    }\n  }\n  return res;\n}\n
iteration.rs
/* 二重 for ループ */\nfn nested_for_loop(n: i32) -> String {\n    let mut res = vec![];\n    // i = 1, 2, ..., n-1, n とループする\n    for i in 1..=n {\n        // j = 1, 2, ..., n-1, n とループする\n        for j in 1..=n {\n            res.push(format!(\"({}, {}), \", i, j));\n        }\n    }\n    res.join(\"\")\n}\n
iteration.c
/* 二重 for ループ */\nchar *nestedForLoop(int n) {\n    // n * n は対応する点の個数であり、\"(i, j), \" に対応する文字列長の最大は 6+10*2 で、さらに末尾の空文字 \\0 のための追加領域が必要\n    int size = n * n * 26 + 1;\n    char *res = malloc(size * sizeof(char));\n    // i = 1, 2, ..., n-1, n とループする\n    for (int i = 1; i <= n; i++) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (int j = 1; j <= n; j++) {\n            char tmp[26];\n            snprintf(tmp, sizeof(tmp), \"(%d, %d), \", i, j);\n            strncat(res, tmp, size - strlen(res) - 1);\n        }\n    }\n    return res;\n}\n
iteration.kt
/* 二重 for ループ */\nfun nestedForLoop(n: Int): String {\n    val res = StringBuilder()\n    // i = 1, 2, ..., n-1, n とループする\n    for (i in 1..n) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (j in 1..n) {\n            res.append(\" ($i, $j), \")\n        }\n    }\n    return res.toString()\n}\n
iteration.rb
### 二重 for ループ ###\ndef nested_for_loop(n)\n  res = \"\"\n\n  # i = 1, 2, ..., n-1, n とループする\n  for i in 1..n\n    # j = 1, 2, ..., n-1, n とループする\n    for j in 1..n\n      res += \"(#{i}, #{j}), \"\n    end\n  end\n\n  res\nend\n
コードの可視化

全画面で見る >

次の図は、このネストしたループのフローチャートです。

図 2-2   ネストしたループのフローチャート

この場合、関数の操作回数は \\(n^2\\) に比例し、言い換えればアルゴリズムの実行時間は入力データサイズ \\(n\\) と「二次関係」にあります。

さらにネストしたループを追加することもできます。ネストが 1 段増えるたびに「次元が 1 つ上がる」ことになり、時間計算量は「三次関係」「四次関係」へと高くなっていきます。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222","level":2,"title":"2.2.2   再帰","text":"

再帰(recursion)は、関数が自分自身を呼び出すことで問題を解決するアルゴリズム戦略です。主に 2 つの段階から成ります。

  1. 再帰呼び出し:プログラムは自分自身をより深く呼び出し続け、通常はより小さい、またはより単純化された引数を渡し、「終了条件」に達するまで進みます。
  2. 復帰: 「終了条件」が満たされると、プログラムは最も深い再帰関数から 1 層ずつ戻り、各層の結果をまとめていきます。

実装の観点から見ると、再帰コードは主に 3 つの要素から成ります。

  1. 終了条件:いつ再帰呼び出しから復帰へ切り替わるかを決めます。
  2. 再帰呼び出し:再帰呼び出しに対応し、関数が自分自身を呼び出します。通常はより小さい、またはより単純化された引数を入力します。
  3. 結果の返却:復帰に対応し、現在の再帰レベルの結果を 1 つ上の層へ返します。

次のコードを見ると、関数 recur(n) を呼び出すだけで \\(1 + 2 + \\dots + n\\) を計算できます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def recur(n: int) -> int:\n    \"\"\"再帰\"\"\"\n    # 終了条件\n    if n == 1:\n        return 1\n    # 再帰:再帰呼び出し\n    res = recur(n - 1)\n    # 帰りがけ:結果を返す\n    return n + res\n
recursion.cpp
/* 再帰 */\nint recur(int n) {\n    // 終了条件\n    if (n == 1)\n        return 1;\n    // 再帰:再帰呼び出し\n    int res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.java
/* 再帰 */\nint recur(int n) {\n    // 終了条件\n    if (n == 1)\n        return 1;\n    // 再帰:再帰呼び出し\n    int res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.cs
/* 再帰 */\nint Recur(int n) {\n    // 終了条件\n    if (n == 1)\n        return 1;\n    // 再帰:再帰呼び出し\n    int res = Recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.go
/* 再帰 */\nfunc recur(n int) int {\n    // 終了条件\n    if n == 1 {\n        return 1\n    }\n    // 再帰:再帰呼び出し\n    res := recur(n - 1)\n    // 帰りがけ:結果を返す\n    return n + res\n}\n
recursion.swift
/* 再帰 */\nfunc recur(n: Int) -> Int {\n    // 終了条件\n    if n == 1 {\n        return 1\n    }\n    // 再帰:再帰呼び出し\n    let res = recur(n: n - 1)\n    // 帰りがけ:結果を返す\n    return n + res\n}\n
recursion.js
/* 再帰 */\nfunction recur(n) {\n    // 終了条件\n    if (n === 1) return 1;\n    // 再帰:再帰呼び出し\n    const res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.ts
/* 再帰 */\nfunction recur(n: number): number {\n    // 終了条件\n    if (n === 1) return 1;\n    // 再帰:再帰呼び出し\n    const res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.dart
/* 再帰 */\nint recur(int n) {\n  // 終了条件\n  if (n == 1) return 1;\n  // 再帰:再帰呼び出し\n  int res = recur(n - 1);\n  // 帰りがけ:結果を返す\n  return n + res;\n}\n
recursion.rs
/* 再帰 */\nfn recur(n: i32) -> i32 {\n    // 終了条件\n    if n == 1 {\n        return 1;\n    }\n    // 再帰:再帰呼び出し\n    let res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    n + res\n}\n
recursion.c
/* 再帰 */\nint recur(int n) {\n    // 終了条件\n    if (n == 1)\n        return 1;\n    // 再帰:再帰呼び出し\n    int res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.kt
/* 再帰 */\nfun recur(n: Int): Int {\n    // 終了条件\n    if (n == 1)\n        return 1\n    // 再帰: 再帰呼び出し\n    val res = recur(n - 1)\n    // 戻る: 結果を返す\n    return n + res\n}\n
recursion.rb
### 再帰 ###\ndef recur(n)\n  # 終了条件\n  return 1 if n == 1\n  # 再帰:再帰呼び出し\n  res = recur(n - 1)\n  # 帰りがけ:結果を返す\n  n + res\nend\n
コードの可視化

全画面で見る >

次の図は、この関数の再帰過程を示しています。

図 2-3   総和関数の再帰過程

計算の観点では、反復と再帰は同じ結果を得られますが、それらは問題を考え解決するためのまったく異なる 2 つのパラダイムを表しています。

  • 反復:「ボトムアップ」で問題を解決します。最も基本的な手順から始め、それらを繰り返したり積み上げたりして、処理が完了するまで進めます。
  • 再帰:「トップダウン」で問題を解決します。元の問題をより小さな部分問題に分解し、それらの部分問題は元の問題と同じ形を持ちます。さらに部分問題をより小さな部分問題へと分解し、基本ケースに達したところで停止します(基本ケースの解は既知です)。

前述の総和関数を例に、問題を \\(f(n) = 1 + 2 + \\dots + n\\) とします。

  • 反復:ループ内で総和の過程を模擬し、\\(1\\) から \\(n\\) まで走査して、各反復で加算を行えば \\(f(n)\\) を求められます。
  • 再帰:問題を部分問題 \\(f(n) = n + f(n-1)\\) に分解し、これを再帰的に分解し続け、基本ケース \\(f(1) = 1\\) に達したところで終了します。
","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1","level":3,"title":"1.   呼び出しスタック","text":"

再帰関数が自分自身を呼び出すたびに、システムは新たに開始された関数のためにメモリを割り当て、局所変数、呼び出し先アドレス、その他の情報を保存します。これにより 2 つの結果が生じます。

  • 関数のコンテキストデータは「スタックフレーム領域」と呼ばれるメモリ領域に保存され、関数が戻るまで解放されません。したがって、再帰は通常、反復より多くのメモリ空間を消費します。
  • 再帰による関数呼び出しには追加のオーバーヘッドが発生します。そのため再帰は通常、ループより時間効率が低くなります。

次の図のように、終了条件が発動する前には、まだ戻っていない再帰関数が同時に \\(n\\) 個存在し、再帰の深さは \\(n\\) になります。

図 2-4   再帰呼び出しの深さ

実際には、プログラミング言語が許容する再帰の深さには通常上限があり、深すぎる再帰はスタックオーバーフローを引き起こす可能性があります。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2","level":3,"title":"2.   末尾再帰","text":"

興味深いことに、関数が返る直前の最後の処理で再帰呼び出しを行う場合、その関数はコンパイラやインタプリタによって最適化され、空間効率が反復と同程度になることがあります。これを末尾再帰(tail recursion)と呼びます。

  • 通常の再帰:関数が 1 つ上の階層の関数へ戻った後も、引き続きコードを実行する必要があるため、システムは 1 つ上の呼び出しのコンテキストを保存しておく必要があります。
  • 末尾再帰:再帰呼び出しが関数の返却前の最後の操作であるため、1 つ上の階層へ戻った後に他の処理を続ける必要がなく、システムは 1 つ上の関数のコンテキストを保存する必要がありません。

\\(1 + 2 + \\dots + n\\) の計算を例にすると、結果変数 res を関数の引数にすることで、末尾再帰を実現できます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def tail_recur(n, res):\n    \"\"\"末尾再帰\"\"\"\n    # 終了条件\n    if n == 0:\n        return res\n    # 末尾再帰呼び出し\n    return tail_recur(n - 1, res + n)\n
recursion.cpp
/* 末尾再帰 */\nint tailRecur(int n, int res) {\n    // 終了条件\n    if (n == 0)\n        return res;\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n);\n}\n
recursion.java
/* 末尾再帰 */\nint tailRecur(int n, int res) {\n    // 終了条件\n    if (n == 0)\n        return res;\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n);\n}\n
recursion.cs
/* 末尾再帰 */\nint TailRecur(int n, int res) {\n    // 終了条件\n    if (n == 0)\n        return res;\n    // 末尾再帰呼び出し\n    return TailRecur(n - 1, res + n);\n}\n
recursion.go
/* 末尾再帰 */\nfunc tailRecur(n int, res int) int {\n    // 終了条件\n    if n == 0 {\n        return res\n    }\n    // 末尾再帰呼び出し\n    return tailRecur(n-1, res+n)\n}\n
recursion.swift
/* 末尾再帰 */\nfunc tailRecur(n: Int, res: Int) -> Int {\n    // 終了条件\n    if n == 0 {\n        return res\n    }\n    // 末尾再帰呼び出し\n    return tailRecur(n: n - 1, res: res + n)\n}\n
recursion.js
/* 末尾再帰 */\nfunction tailRecur(n, res) {\n    // 終了条件\n    if (n === 0) return res;\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n);\n}\n
recursion.ts
/* 末尾再帰 */\nfunction tailRecur(n: number, res: number): number {\n    // 終了条件\n    if (n === 0) return res;\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n);\n}\n
recursion.dart
/* 末尾再帰 */\nint tailRecur(int n, int res) {\n  // 終了条件\n  if (n == 0) return res;\n  // 末尾再帰呼び出し\n  return tailRecur(n - 1, res + n);\n}\n
recursion.rs
/* 末尾再帰 */\nfn tail_recur(n: i32, res: i32) -> i32 {\n    // 終了条件\n    if n == 0 {\n        return res;\n    }\n    // 末尾再帰呼び出し\n    tail_recur(n - 1, res + n)\n}\n
recursion.c
/* 末尾再帰 */\nint tailRecur(int n, int res) {\n    // 終了条件\n    if (n == 0)\n        return res;\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n);\n}\n
recursion.kt
/* 末尾再帰 */\ntailrec fun tailRecur(n: Int, res: Int): Int {\n    // `tailrec` キーワードを追加して末尾再帰最適化を有効にする\n    // 終了条件\n    if (n == 0)\n        return res\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n)\n}\n
recursion.rb
### 末尾再帰 ###\ndef tail_recur(n, res)\n  # 終了条件\n  return res if n == 0\n  # 末尾再帰呼び出し\n  tail_recur(n - 1, res + n)\nend\n
コードの可視化

全画面で見る >

末尾再帰の実行過程を次の図に示します。通常の再帰と末尾再帰を比べると、加算処理が実行されるタイミングが異なります。

  • 通常の再帰:加算処理は復帰の過程で実行され、各層が戻るたびにもう一度加算を行います。
  • 末尾再帰:加算処理は再帰呼び出しの過程で実行され、復帰の過程では各層が戻るだけで済みます。

図 2-5   末尾再帰の過程

Tip

多くのコンパイラやインタプリタは末尾再帰最適化をサポートしていない点に注意してください。たとえば、Python はデフォルトで末尾再帰最適化をサポートしていないため、関数が末尾再帰の形であっても、スタックオーバーフローが発生する可能性があります。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3_1","level":3,"title":"3.   再帰木","text":"

「分割統治」に関連するアルゴリズム問題を扱う際、再帰は反復よりも発想が直感的で、コードも読みやすいことがよくあります。「フィボナッチ数列」を例に見てみましょう。

Question

フィボナッチ数列 \\(0, 1, 1, 2, 3, 5, 8, 13, \\dots\\) が与えられたとき、この数列の第 \\(n\\) 項を求めてください。

フィボナッチ数列の第 \\(n\\) 項を \\(f(n)\\) とすると、次の 2 つが容易に分かります。

  • 数列の最初の 2 項は \\(f(1) = 0\\) と \\(f(2) = 1\\) です。
  • 数列中の各項は直前の 2 項の和であり、すなわち \\(f(n) = f(n - 1) + f(n - 2)\\) です。

漸化式に従って再帰呼び出しを行い、最初の 2 項を終了条件とすれば、再帰コードを書けます。fib(n) を呼び出すことでフィボナッチ数列の第 \\(n\\) 項を得られます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def fib(n: int) -> int:\n    \"\"\"フィボナッチ数列:再帰\"\"\"\n    # 終了条件 f(1) = 0, f(2) = 1\n    if n == 1 or n == 2:\n        return n - 1\n    # f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    res = fib(n - 1) + fib(n - 2)\n    # 結果 f(n) を返す\n    return res\n
recursion.cpp
/* フィボナッチ数列:再帰 */\nint fib(int n) {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    int res = fib(n - 1) + fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.java
/* フィボナッチ数列:再帰 */\nint fib(int n) {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    int res = fib(n - 1) + fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.cs
/* フィボナッチ数列:再帰 */\nint Fib(int n) {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    int res = Fib(n - 1) + Fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.go
/* フィボナッチ数列:再帰 */\nfunc fib(n int) int {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    res := fib(n-1) + fib(n-2)\n    // 結果 f(n) を返す\n    return res\n}\n
recursion.swift
/* フィボナッチ数列:再帰 */\nfunc fib(n: Int) -> Int {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    let res = fib(n: n - 1) + fib(n: n - 2)\n    // 結果 f(n) を返す\n    return res\n}\n
recursion.js
/* フィボナッチ数列:再帰 */\nfunction fib(n) {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    const res = fib(n - 1) + fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.ts
/* フィボナッチ数列:再帰 */\nfunction fib(n: number): number {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    const res = fib(n - 1) + fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.dart
/* フィボナッチ数列:再帰 */\nint fib(int n) {\n  // 終了条件 f(1) = 0, f(2) = 1\n  if (n == 1 || n == 2) return n - 1;\n  // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n  int res = fib(n - 1) + fib(n - 2);\n  // 結果 f(n) を返す\n  return res;\n}\n
recursion.rs
/* フィボナッチ数列:再帰 */\nfn fib(n: i32) -> i32 {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1;\n    }\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    let res = fib(n - 1) + fib(n - 2);\n    // 結果を返す\n    res\n}\n
recursion.c
/* フィボナッチ数列:再帰 */\nint fib(int n) {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    int res = fib(n - 1) + fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.kt
/* フィボナッチ数列:再帰 */\nfun fib(n: Int): Int {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    val res = fib(n - 1) + fib(n - 2)\n    // 結果 f(n) を返す\n    return res\n}\n
recursion.rb
### フィボナッチ数列:再帰 ###\ndef fib(n)\n  # 終了条件 f(1) = 0, f(2) = 1\n  return n - 1 if n == 1 || n == 2\n  # f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n  res = fib(n - 1) + fib(n - 2)\n  # 結果 f(n) を返す\n  res\nend\n
コードの可視化

全画面で見る >

上のコードを見ると、関数内で 2 回の再帰呼び出しを行っています。これは 1 回の呼び出しから 2 つの呼び出し分岐が生じることを意味します。次の図のように、この再帰呼び出しを繰り返していくと、最終的に深さ \\(n\\) の再帰木(recursion tree)が生成されます。

図 2-6   フィボナッチ数列の再帰木

本質的に見ると、再帰は「問題をより小さな部分問題へ分解する」という思考パラダイムを体現しており、この分割統治の戦略は非常に重要です。

  • アルゴリズムの観点では、探索、ソート、バックトラッキング、分割統治、動的計画法など、多くの重要な戦略が直接または間接にこの考え方を用いています。
  • データ構造の観点では、再帰は連結リスト、木、グラフに関する問題の処理に本質的に適しており、これらは分割統治の考え方で分析しやすいからです。
","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223","level":2,"title":"2.2.3   両者の比較","text":"

以上をまとめると、次の表のように、反復と再帰は実装、性能、適用性の面で違いがあります。

表 2-1   反復と再帰の特徴の比較

反復 再帰 実装方法 ループ構造 関数が自分自身を呼び出す 時間効率 通常は効率が高く、関数呼び出しの負荷がない 関数呼び出しのたびにオーバーヘッドが発生する メモリ使用 通常は固定サイズのメモリ空間を使う 関数呼び出しの蓄積により大量のスタックフレーム領域を使う可能性がある 適用対象 単純な反復処理に適し、コードが直感的で読みやすい 木、グラフ、分割統治、バックトラッキングなどの部分問題分解に適し、コード構造が簡潔で明快

Tip

以下の内容が難しいと感じる場合は、「スタック」の章を読み終えた後に改めて復習してください。

では、反復と再帰にはどのような内在的な関係があるのでしょうか。前述の再帰関数を例にすると、加算処理は再帰の復帰段階で行われます。これは、最初に呼び出された関数が実際には最後に加算を完了することを意味しており、この動作の仕組みはスタックの「後入れ先出し」の原則とよく似ています。

実際、「呼び出しスタック」や「スタックフレーム領域」といった再帰の用語自体が、再帰とスタックの密接な関係を示唆しています。

  1. 再帰呼び出し:関数が呼び出されると、システムは「呼び出しスタック」上にその関数のための新しいスタックフレームを割り当て、局所変数、引数、返却先アドレスなどのデータを保存します。
  2. 復帰:関数の実行が完了して戻ると、対応するスタックフレームは「呼び出しスタック」から取り除かれ、前の関数の実行環境が復元されます。

したがって、明示的なスタックを使って呼び出しスタックの振る舞いを模擬することができ、その結果として再帰を反復形式へ変換できます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def for_loop_recur(n: int) -> int:\n    \"\"\"反復で再帰を模擬する\"\"\"\n    # 明示的なスタックを使ってシステムコールスタックを模擬する\n    stack = []\n    res = 0\n    # 再帰:再帰呼び出し\n    for i in range(n, 0, -1):\n        # 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.append(i)\n    # 帰りがけ:結果を返す\n    while stack:\n        # 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop()\n    # res = 1+2+3+...+n\n    return res\n
recursion.cpp
/* 反復で再帰を模擬する */\nint forLoopRecur(int n) {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    stack<int> stack;\n    int res = 0;\n    // 再帰:再帰呼び出し\n    for (int i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i);\n    }\n    // 帰りがけ:結果を返す\n    while (!stack.empty()) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.top();\n        stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.java
/* 反復で再帰を模擬する */\nint forLoopRecur(int n) {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    Stack<Integer> stack = new Stack<>();\n    int res = 0;\n    // 再帰:再帰呼び出し\n    for (int i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i);\n    }\n    // 帰りがけ:結果を返す\n    while (!stack.isEmpty()) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.cs
/* 反復で再帰を模擬する */\nint ForLoopRecur(int n) {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    Stack<int> stack = new();\n    int res = 0;\n    // 再帰:再帰呼び出し\n    for (int i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.Push(i);\n    }\n    // 帰りがけ:結果を返す\n    while (stack.Count > 0) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.Pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.go
/* 反復で再帰を模擬する */\nfunc forLoopRecur(n int) int {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    stack := list.New()\n    res := 0\n    // 再帰:再帰呼び出し\n    for i := n; i > 0; i-- {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.PushBack(i)\n    }\n    // 帰りがけ:結果を返す\n    for stack.Len() != 0 {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.Back().Value.(int)\n        stack.Remove(stack.Back())\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.swift
/* 反復で再帰を模擬する */\nfunc forLoopRecur(n: Int) -> Int {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    var stack: [Int] = []\n    var res = 0\n    // 再帰:再帰呼び出し\n    for i in (1 ... n).reversed() {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.append(i)\n    }\n    // 帰りがけ:結果を返す\n    while !stack.isEmpty {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.removeLast()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.js
/* 反復で再帰を模擬する */\nfunction forLoopRecur(n) {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    const stack = [];\n    let res = 0;\n    // 再帰:再帰呼び出し\n    for (let i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i);\n    }\n    // 帰りがけ:結果を返す\n    while (stack.length) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.ts
/* 反復で再帰を模擬する */\nfunction forLoopRecur(n: number): number {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    const stack: number[] = [];\n    let res: number = 0;\n    // 再帰:再帰呼び出し\n    for (let i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i);\n    }\n    // 帰りがけ:結果を返す\n    while (stack.length) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.dart
/* 反復で再帰を模擬する */\nint forLoopRecur(int n) {\n  // 明示的なスタックを使ってシステムコールスタックを模擬する\n  List<int> stack = [];\n  int res = 0;\n  // 再帰:再帰呼び出し\n  for (int i = n; i > 0; i--) {\n    // 「スタックへのプッシュ」で「再帰」を模擬する\n    stack.add(i);\n  }\n  // 帰りがけ:結果を返す\n  while (!stack.isEmpty) {\n    // 「スタックから取り出す操作」で「帰り」をシミュレート\n    res += stack.removeLast();\n  }\n  // res = 1+2+3+...+n\n  return res;\n}\n
recursion.rs
/* 反復で再帰を模擬する */\nfn for_loop_recur(n: i32) -> i32 {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    let mut stack = Vec::new();\n    let mut res = 0;\n    // 再帰:再帰呼び出し\n    for i in (1..=n).rev() {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i);\n    }\n    // 帰りがけ:結果を返す\n    while !stack.is_empty() {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop().unwrap();\n    }\n    // res = 1+2+3+...+n\n    res\n}\n
recursion.c
/* 反復で再帰を模擬する */\nint forLoopRecur(int n) {\n    int stack[1000]; // 大きな配列を使ってスタックを実装する\n    int top = -1;    // スタックトップのインデックス\n    int res = 0;\n    // 再帰:再帰呼び出し\n    for (int i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack[1 + top++] = i;\n    }\n    // 帰りがけ:結果を返す\n    while (top >= 0) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack[top--];\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.kt
/* 反復で再帰を模擬する */\nfun forLoopRecur(n: Int): Int {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    val stack = Stack<Int>()\n    var res = 0\n    // 再帰: 再帰呼び出し\n    for (i in n downTo 0) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i)\n    }\n    // 戻る: 結果を返す\n    while (stack.isNotEmpty()) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.rb
### 反復で再帰をシミュレート ###\ndef for_loop_recur(n)\n  # 明示的なスタックを使ってシステムコールスタックを模擬する\n  stack = []\n  res = 0\n\n  # 再帰:再帰呼び出し\n  for i in n.downto(0)\n    # 「スタックへのプッシュ」で「再帰」を模擬する\n    stack << i\n  end\n  # 帰りがけ:結果を返す\n  while !stack.empty?\n    res += stack.pop\n  end\n\n  # res = 1+2+3+...+n\n  res\nend\n
コードの可視化

全画面で見る >

上のコードを見ると、再帰を反復へ変換すると、コードはより複雑になります。反復と再帰は多くの場合に相互変換できますが、常にそうする価値があるとは限りません。理由は次の 2 点です。

  • 変換後のコードは理解しにくくなり、可読性が下がる可能性があります。
  • 複雑な問題によっては、システムの呼び出しスタックの振る舞いを模擬すること自体が非常に難しい場合があります。

要するに、反復を選ぶか再帰を選ぶかは、対象となる問題の性質によって決まります。実際のプログラミングでは、両者の長所と短所を見極め、状況に応じて適切な方法を選ぶことが重要です。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   アルゴリズム効率の評価","text":"

アルゴリズム設計では、次の 2 つのレベルの目標を順に追求します。

  1. 問題の解法を見つける:アルゴリズムは、定められた入力範囲内で問題の正しい解を確実に求められる必要があります。
  2. 最適な解法を追求する:同じ問題に対して複数の解法が存在する場合があり、私たちはできるだけ効率的なアルゴリズムを見つけたいと考えます。

つまり、問題を解けることを前提として、アルゴリズム効率はその良し悪しを測る主要な評価指標となっており、次の 2 つの観点を含みます。

  • 時間効率:アルゴリズムの実行時間の長さ。
  • 空間効率:アルゴリズムが使用するメモリ空間の大きさ。

簡単に言えば、**私たちの目標は「高速で省メモリ」なデータ構造とアルゴリズムを設計すること**です。そして、アルゴリズム効率を効果的に評価することは非常に重要です。そうすることで初めて、さまざまなアルゴリズムを比較し、さらにアルゴリズム設計と最適化の過程を導けるからです。

効率の評価方法は主に 2 種類に分けられます。実測と理論的な見積もりです。

","path":["第 2 章   計算量解析","2.1   アルゴリズム効率の評価"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211","level":2,"title":"2.1.1   実測","text":"

いまアルゴリズム A とアルゴリズム B があり、どちらも同じ問題を解けるとします。この 2 つのアルゴリズムの効率を比較する必要がある場合、最も直接的な方法は 1 台のコンピュータで両者を実行し、その実行時間とメモリ使用量を監視して記録することです。この評価方法は実際の状況を反映できますが、大きな制約もあります。

一方では、**テスト環境による干渉要因を排除しにくい**という問題があります。ハードウェア構成はアルゴリズムの性能に影響します。たとえば、並列度の高いアルゴリズムはマルチコア CPU での実行により適しており、メモリアクセスが集中的なアルゴリズムは高性能メモリ上でより良い性能を示します。つまり、異なるマシンでのテスト結果は一致しない可能性があります。これは、さまざまなマシンでテストして平均効率を統計的に求める必要があることを意味しますが、それは現実的ではありません。

他方では、**完全なテストを実施するには非常に多くの資源が必要**です。入力データ量が変化すると、アルゴリズムは異なる効率を示します。たとえば、入力データ量が小さいときはアルゴリズム A の実行時間がアルゴリズム B より短くても、入力データ量が大きいときには結果がちょうど逆になるかもしれません。そのため、説得力のある結論を得るには、さまざまな規模の入力データでテストする必要があり、それには大量の計算資源を要します。

","path":["第 2 章   計算量解析","2.1   アルゴリズム効率の評価"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#212","level":2,"title":"2.1.2   理論的な見積もり","text":"

実測には大きな制約があるため、いくつかの計算だけによってアルゴリズムの効率を評価することを考えられます。この見積もり方法は漸近計算量解析(asymptotic complexity analysis)と呼ばれ、略して計算量解析といいます。

計算量解析は、アルゴリズムの実行に必要な時間資源と空間資源が入力データ規模とどのような関係にあるかを表します。これは、入力データ規模が増加するにつれて、アルゴリズムの実行に必要な時間と空間がどのように増加するかという傾向を記述するものです。この定義はややわかりにくいので、次の 3 つのポイントに分けて理解できます。

  • 「時間資源と空間資源」は、それぞれ時間計算量(time complexity)と空間計算量(space complexity)に対応します。
  • 「入力データ規模が増加するにつれて」とは、計算量がアルゴリズムの実行効率と入力データ規模との関係を反映していることを意味します。
  • 「時間と空間の増加傾向」とは、計算量解析が注目するのは実行時間や使用空間の具体的な値ではなく、時間や空間の増加の「速さ」であることを示します。

計算量解析は実測という方法の欠点を克服しています。その点は次のように表れます。

  • 実際にコードを動かす必要がなく、より環境にやさしく省エネルギーです。
  • テスト環境から独立しており、解析結果はすべての実行プラットフォームに適用できます。
  • 異なるデータ量におけるアルゴリズム効率を表せ、とくに大規模データ量での性能を反映できます。

Tip

それでも計算量の概念がまだわかりにくくても、心配はいりません。後続の章で詳しく説明します。

計算量解析は、アルゴリズム効率を評価するための「物差し」を私たちに与えてくれます。これにより、あるアルゴリズムの実行に必要な時間資源と空間資源を測り、異なるアルゴリズム同士の効率を比較できます。

計算量は数学的な概念であり、初学者にとってはやや抽象的で、学習の難度も比較的高いかもしれません。この観点から見ると、計算量解析は最初に紹介する内容としてはあまり適していない可能性があります。しかし、あるデータ構造やアルゴリズムの特徴を議論する際には、その実行速度や空間使用状況の分析を避けることはできません。

以上を踏まえると、データ構造とアルゴリズムを深く学ぶ前に、**まず計算量解析について初歩的な理解を持ち、簡単なアルゴリズムの計算量解析ができるようにしておくこと**を勧めます。

","path":["第 2 章   計算量解析","2.1   アルゴリズム効率の評価"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/","level":1,"title":"2.4   空間計算量","text":"

空間計算量(space complexity)は、アルゴリズムが占有するメモリ空間がデータ量の増加に伴ってどのように増えるかを測る指標です。この概念は時間計算量と非常によく似ており、「実行時間」を「占有メモリ空間」に置き換えるだけです。

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#241","level":2,"title":"2.4.1   アルゴリズムに関連する空間","text":"

アルゴリズムが実行中に使用するメモリ空間には、主に次の種類があります。

  • 入力空間:アルゴリズムの入力データを格納するための空間。
  • 一時空間:アルゴリズムの実行中に使用する変数、オブジェクト、関数コンテキストなどのデータを格納するための空間。
  • 出力空間:アルゴリズムの出力データを格納するための空間。

一般に、空間計算量の集計範囲は「一時空間」と「出力空間」を合わせたものです。

一時空間はさらに三つに分けられます。

  • 一時データ:アルゴリズム実行中の各種定数、変数、オブジェクトなどを保存するための空間。
  • スタックフレーム空間:呼び出された関数のコンテキストデータを保存するための空間。システムは関数を呼び出すたびにスタックの先頭にスタックフレームを作成し、関数が戻るとその空間を解放します。
  • 命令空間:コンパイル後のプログラム命令を保存するための空間で、実際の集計では通常無視されます。

プログラムの空間計算量を分析する際には、通常、一時データ、スタックフレーム空間、出力データの三つを数えます。以下の図に示すとおりです。

図 2-15   アルゴリズムで使用される関連空間

関連するコードを以下に示します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class Node:\n    \"\"\"クラス\"\"\"\n    def __init__(self, x: int):\n        self.val: int = x              # ノードの値\n        self.next: Node | None = None  # 次のノードへの参照\n\ndef function() -> int:\n    \"\"\"関数\"\"\"\n    # いくつかの処理を実行...\n    return 0\n\ndef algorithm(n) -> int:  # 入力データ\n    A = 0                 # 一時データ(定数。一般に大文字で表す)\n    b = 0                 # 一時データ(変数)\n    node = Node(0)        # 一時データ(オブジェクト)\n    c = function()        # スタックフレーム空間(関数呼び出し)\n    return A + b + c      # 出力データ\n
/* 構造体 */\nstruct Node {\n    int val;\n    Node *next;\n    Node(int x) : val(x), next(nullptr) {}\n};\n\n/* 関数 */\nint func() {\n    // いくつかの処理を実行...\n    return 0;\n}\n\nint algorithm(int n) {        // 入力データ\n    const int a = 0;          // 一時データ(定数)\n    int b = 0;                // 一時データ(変数)\n    Node* node = new Node(0); // 一時データ(オブジェクト)\n    int c = func();           // スタックフレーム空間(関数呼び出し)\n    return a + b + c;         // 出力データ\n}\n
/* クラス */\nclass Node {\n    int val;\n    Node next;\n    Node(int x) { val = x; }\n}\n\n/* 関数 */\nint function() {\n    // いくつかの処理を実行...\n    return 0;\n}\n\nint algorithm(int n) {        // 入力データ\n    final int a = 0;          // 一時データ(定数)\n    int b = 0;                // 一時データ(変数)\n    Node node = new Node(0);  // 一時データ(オブジェクト)\n    int c = function();       // スタックフレーム空間(関数呼び出し)\n    return a + b + c;         // 出力データ\n}\n
/* クラス */\nclass Node(int x) {\n    int val = x;\n    Node next;\n}\n\n/* 関数 */\nint Function() {\n    // いくつかの処理を実行...\n    return 0;\n}\n\nint Algorithm(int n) {        // 入力データ\n    const int a = 0;          // 一時データ(定数)\n    int b = 0;                // 一時データ(変数)\n    Node node = new(0);       // 一時データ(オブジェクト)\n    int c = Function();       // スタックフレーム空間(関数呼び出し)\n    return a + b + c;         // 出力データ\n}\n
/* 構造体 */\ntype node struct {\n    val  int\n    next *node\n}\n\n/* node 構造体を作成 */\nfunc newNode(val int) *node {\n    return &node{val: val}\n}\n\n/* 関数 */\nfunc function() int {\n    // いくつかの処理を実行...\n    return 0\n}\n\nfunc algorithm(n int) int { // 入力データ\n    const a = 0             // 一時データ(定数)\n    b := 0                  // 一時データ(変数)\n    newNode(0)              // 一時データ(オブジェクト)\n    c := function()         // スタックフレーム空間(関数呼び出し)\n    return a + b + c        // 出力データ\n}\n
/* クラス */\nclass Node {\n    var val: Int\n    var next: Node?\n\n    init(x: Int) {\n        val = x\n    }\n}\n\n/* 関数 */\nfunc function() -> Int {\n    // いくつかの処理を実行...\n    return 0\n}\n\nfunc algorithm(n: Int) -> Int { // 入力データ\n    let a = 0             // 一時データ(定数)\n    var b = 0             // 一時データ(変数)\n    let node = Node(x: 0) // 一時データ(オブジェクト)\n    let c = function()    // スタックフレーム空間(関数呼び出し)\n    return a + b + c      // 出力データ\n}\n
/* クラス */\nclass Node {\n    val;\n    next;\n    constructor(val) {\n        this.val = val === undefined ? 0 : val; // ノードの値\n        this.next = null;                       // 次のノードへの参照\n    }\n}\n\n/* 関数 */\nfunction constFunc() {\n    // いくつかの処理を実行\n    return 0;\n}\n\nfunction algorithm(n) {       // 入力データ\n    const a = 0;              // 一時データ(定数)\n    let b = 0;                // 一時データ(変数)\n    const node = new Node(0); // 一時データ(オブジェクト)\n    const c = constFunc();    // スタックフレーム空間(関数呼び出し)\n    return a + b + c;         // 出力データ\n}\n
/* クラス */\nclass Node {\n    val: number;\n    next: Node | null;\n    constructor(val?: number) {\n        this.val = val === undefined ? 0 : val; // ノードの値\n        this.next = null;                       // 次のノードへの参照\n    }\n}\n\n/* 関数 */\nfunction constFunc(): number {\n    // いくつかの処理を実行\n    return 0;\n}\n\nfunction algorithm(n: number): number { // 入力データ\n    const a = 0;                        // 一時データ(定数)\n    let b = 0;                          // 一時データ(変数)\n    const node = new Node(0);           // 一時データ(オブジェクト)\n    const c = constFunc();              // スタックフレーム空間(関数呼び出し)\n    return a + b + c;                   // 出力データ\n}\n
/* クラス */\nclass Node {\n  int val;\n  Node next;\n  Node(this.val, [this.next]);\n}\n\n/* 関数 */\nint function() {\n  // いくつかの処理を実行...\n  return 0;\n}\n\nint algorithm(int n) {  // 入力データ\n  const int a = 0;      // 一時データ(定数)\n  int b = 0;            // 一時データ(変数)\n  Node node = Node(0);  // 一時データ(オブジェクト)\n  int c = function();   // スタックフレーム空間(関数呼び出し)\n  return a + b + c;     // 出力データ\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 構造体 */\nstruct Node {\n    val: i32,\n    next: Option<Rc<RefCell<Node>>>,\n}\n\n/* Node 構造体を作成 */\nimpl Node {\n    fn new(val: i32) -> Self {\n        Self { val: val, next: None }\n    }\n}\n\n/* 関数 */\nfn function() -> i32 {      \n    // いくつかの処理を実行...\n    return 0;\n}\n\nfn algorithm(n: i32) -> i32 {       // 入力データ\n    const a: i32 = 0;               // 一時データ(定数)\n    let mut b = 0;                  // 一時データ(変数)\n    let node = Node::new(0);        // 一時データ(オブジェクト)\n    let c = function();             // スタックフレーム空間(関数呼び出し)\n    return a + b + c;               // 出力データ\n}\n
/* 関数 */\nint func() {\n    // いくつかの処理を実行...\n    return 0;\n}\n\nint algorithm(int n) { // 入力データ\n    const int a = 0;   // 一時データ(定数)\n    int b = 0;         // 一時データ(変数)\n    int c = func();    // スタックフレーム空間(関数呼び出し)\n    return a + b + c;  // 出力データ\n}\n
/* クラス */\nclass Node(var _val: Int) {\n    var next: Node? = null\n}\n\n/* 関数 */\nfun function(): Int {\n    // いくつかの処理を実行...\n    return 0\n}\n\nfun algorithm(n: Int): Int { // 入力データ\n    val a = 0                // 一時データ(定数)\n    var b = 0                // 一時データ(変数)\n    val node = Node(0)       // 一時データ(オブジェクト)\n    val c = function()       // スタックフレーム空間(関数呼び出し)\n    return a + b + c         // 出力データ\n}\n
### クラス ###\nclass Node\n    attr_accessor :val      # ノードの値\n    attr_accessor :next     # 次のノードへの参照\n\n    def initialize(x)\n        @val = x\n    end\nend\n\n### 関数 ###\ndef function\n    # いくつかの処理を実行...\n    0\nend\n\n### アルゴリズム ###\ndef algorithm(n)        # 入力データ\n    a = 0               # 一時データ(定数)\n    b = 0               # 一時データ(変数)\n    node = Node.new(0)  # 一時データ(オブジェクト)\n    c = function        # スタックフレーム空間(関数呼び出し)\n    a + b + c           # 出力データ\nend\n
","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#242","level":2,"title":"2.4.2   推定方法","text":"

空間計算量の推定方法は時間計算量とおおむね同じで、数える対象を「操作回数」から「使用空間の大きさ」に変えるだけです。

ただし時間計算量と異なり、通常は最悪空間計算量だけに注目します。メモリ空間は厳格な要件であり、どの入力データに対しても十分なメモリを確保できることを保証しなければならないからです。

以下のコードを見ると、最悪空間計算量における「最悪」には二つの意味があります。

  1. 最悪の入力データを基準にする:\\(n < 10\\) のとき空間計算量は \\(O(1)\\) ですが、\\(n > 10\\) のとき初期化される配列 nums が \\(O(n)\\) の空間を占有するため、最悪空間計算量は \\(O(n)\\) です。
  2. アルゴリズム実行中のメモリ使用量のピークを基準にする:例えば、プログラムは最後の行を実行する前までは \\(O(1)\\) の空間しか使いませんが、配列 nums を初期化するときには \\(O(n)\\) の空間を占有するため、最悪空間計算量は \\(O(n)\\) です。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 0               # O(1)\n    b = [0] * 10000     # O(1)\n    if n > 10:\n        nums = [0] * n  # O(n)\n
void algorithm(int n) {\n    int a = 0;               // O(1)\n    vector<int> b(10000);    // O(1)\n    if (n > 10)\n        vector<int> nums(n); // O(n)\n}\n
void algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10)\n        int[] nums = new int[n]; // O(n)\n}\n
void Algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10) {\n        int[] nums = new int[n]; // O(n)\n    }\n}\n
func algorithm(n int) {\n    a := 0                      // O(1)\n    b := make([]int, 10000)     // O(1)\n    var nums []int\n    if n > 10 {\n        nums := make([]int, n)  // O(n)\n    }\n    fmt.Println(a, b, nums)\n}\n
func algorithm(n: Int) {\n    let a = 0 // O(1)\n    let b = Array(repeating: 0, count: 10000) // O(1)\n    if n > 10 {\n        let nums = Array(repeating: 0, count: n) // O(n)\n    }\n}\n
function algorithm(n) {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
function algorithm(n: number): void {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
void algorithm(int n) {\n  int a = 0;                            // O(1)\n  List<int> b = List.filled(10000, 0);  // O(1)\n  if (n > 10) {\n    List<int> nums = List.filled(n, 0); // O(n)\n  }\n}\n
fn algorithm(n: i32) {\n    let a = 0;                              // O(1)\n    let b = [0; 10000];                     // O(1)\n    if n > 10 {\n        let nums = vec![0; n as usize];     // O(n)\n    }\n}\n
void algorithm(int n) {\n    int a = 0;               // O(1)\n    int b[10000];            // O(1)\n    if (n > 10)\n        int nums[n] = {0};   // O(n)\n}\n
fun algorithm(n: Int) {\n    val a = 0                    // O(1)\n    val b = IntArray(10000)      // O(1)\n    if (n > 10) {\n        val nums = IntArray(n)   // O(n)\n    }\n}\n
def algorithm(n)\n    a = 0                           # O(1)\n    b = Array.new(10000)            # O(1)\n    nums = Array.new(n) if n > 10   # O(n)\nend\n

再帰関数では、スタックフレーム空間の集計に注意が必要です。以下のコードを見てみましょう。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def function() -> int:\n    # いくつかの処理を実行\n    return 0\n\ndef loop(n: int):\n    \"\"\"ループの空間計算量は O(1)\"\"\"\n    for _ in range(n):\n        function()\n\ndef recur(n: int):\n    \"\"\"再帰の空間計算量は O(n)\"\"\"\n    if n == 1:\n        return\n    return recur(n - 1)\n
int func() {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
int function() {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
int Function() {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nvoid Loop(int n) {\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nint Recur(int n) {\n    if (n == 1) return 1;\n    return Recur(n - 1);\n}\n
func function() int {\n    // いくつかの処理を実行\n    return 0\n}\n\n/* ループの空間計算量は O(1) */\nfunc loop(n int) {\n    for i := 0; i < n; i++ {\n        function()\n    }\n}\n\n/* 再帰の空間計算量は O(n) */\nfunc recur(n int) {\n    if n == 1 {\n        return\n    }\n    recur(n - 1)\n}\n
@discardableResult\nfunc function() -> Int {\n    // いくつかの処理を実行\n    return 0\n}\n\n/* ループの空間計算量は O(1) */\nfunc loop(n: Int) {\n    for _ in 0 ..< n {\n        function()\n    }\n}\n\n/* 再帰の空間計算量は O(n) */\nfunc recur(n: Int) {\n    if n == 1 {\n        return\n    }\n    recur(n: n - 1)\n}\n
function constFunc() {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nfunction loop(n) {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nfunction recur(n) {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
function constFunc(): number {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nfunction loop(n: number): void {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nfunction recur(n: number): void {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
int function() {\n  // いくつかの処理を実行\n  return 0;\n}\n/* ループの空間計算量は O(1) */\nvoid loop(int n) {\n  for (int i = 0; i < n; i++) {\n    function();\n  }\n}\n/* 再帰の空間計算量は O(n) */\nvoid recur(int n) {\n  if (n == 1) return;\n  recur(n - 1);\n}\n
fn function() -> i32 {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nfn loop(n: i32) {\n    for i in 0..n {\n        function();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nfn recur(n: i32) {\n    if n == 1 {\n        return;\n    }\n    recur(n - 1);\n}\n
int func() {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
fun function(): Int {\n    // いくつかの処理を実行\n    return 0\n}\n/* ループの空間計算量は O(1) */\nfun loop(n: Int) {\n    for (i in 0..<n) {\n        function()\n    }\n}\n/* 再帰の空間計算量は O(n) */\nfun recur(n: Int) {\n    if (n == 1) return\n    return recur(n - 1)\n}\n
def function\n    # いくつかの処理を実行\n    0\nend\n\n### ループの空間計算量は O(1) ###\ndef loop(n)\n    (0...n).each { function }\nend\n\n### 再帰の空間計算量は O(n) ###\ndef recur(n)\n    return if n == 1\n    recur(n - 1)\nend\n

関数 loop()recur() はどちらも時間計算量は \\(O(n)\\) ですが、空間計算量は異なります。

  • 関数 loop() はループの中で function() を \\(n\\) 回呼び出しますが、各反復での function() は戻るたびにスタックフレーム空間が解放されるため、空間計算量は依然として \\(O(1)\\) です。
  • 再帰関数 recur() は実行中に未返却の recur() が同時に \\(n\\) 個存在するため、\\(O(n)\\) のスタックフレーム空間を占有します。
","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243","level":2,"title":"2.4.3   よくある型","text":"

入力データサイズを \\(n\\) とすると、以下の図はよくある空間計算量の型を低い順から高い順に示しています。

\\[ \\begin{aligned} O(1) < O(\\log n) < O(n) < O(n^2) < O(2^n) \\newline \\text{定数階} < \\text{対数階} < \\text{線形階} < \\text{平方階} < \\text{指数階} \\end{aligned} \\]

図 2-16   よくある空間計算量の型

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#1-o1","level":3,"title":"1.   定数階 \\(O(1)\\)","text":"

定数階は、個数が入力データサイズ \\(n\\) に依存しない定数、変数、オブジェクトなどによく現れます。

注意すべき点として、ループ内で変数を初期化したり関数を呼び出したりして使用されたメモリは、次の反復に入ると解放されるため、空間の占有は累積せず、空間計算量は依然として \\(O(1)\\) です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def function() -> int:\n    \"\"\"関数\"\"\"\n    # 何らかの処理を行う\n    return 0\n\ndef constant(n: int):\n    \"\"\"定数階\"\"\"\n    # 定数、変数、オブジェクトは O(1) の空間を占める\n    a = 0\n    nums = [0] * 10000\n    node = ListNode(0)\n    # ループ内の変数は O(1) の空間を占める\n    for _ in range(n):\n        c = 0\n    # ループ内の関数は O(1) の空間を占める\n    for _ in range(n):\n        function()\n
space_complexity.cpp
/* 関数 */\nint func() {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nvoid constant(int n) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const int a = 0;\n    int b = 0;\n    vector<int> nums(10000);\n    ListNode node(0);\n    // ループ内の変数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
space_complexity.java
/* 関数 */\nint function() {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nvoid constant(int n) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    final int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new ListNode(0);\n    // ループ内の変数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n
space_complexity.cs
/* 関数 */\nint Function() {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nvoid Constant(int n) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new(0);\n    // ループ内の変数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n
space_complexity.go
/* 関数 */\nfunc function() int {\n    // いくつかの操作を実行...\n    return 0\n}\n\n/* 定数階 */\nfunc spaceConstant(n int) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const a = 0\n    b := 0\n    nums := make([]int, 10000)\n    node := newNode(0)\n    // ループ内の変数は O(1) の空間を占める\n    var c int\n    for i := 0; i < n; i++ {\n        c = 0\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for i := 0; i < n; i++ {\n        function()\n    }\n    b += 0\n    c += 0\n    nums[0] = 0\n    node.val = 0\n}\n
space_complexity.swift
/* 関数 */\n@discardableResult\nfunc function() -> Int {\n    // 何らかの処理を行う\n    return 0\n}\n\n/* 定数階 */\nfunc constant(n: Int) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    let a = 0\n    var b = 0\n    let nums = Array(repeating: 0, count: 10000)\n    let node = ListNode(x: 0)\n    // ループ内の変数は O(1) の空間を占める\n    for _ in 0 ..< n {\n        let c = 0\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for _ in 0 ..< n {\n        function()\n    }\n}\n
space_complexity.js
/* 関数 */\nfunction constFunc() {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nfunction constant(n) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // ループ内の変数は O(1) の空間を占める\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
space_complexity.ts
/* 関数 */\nfunction constFunc(): number {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nfunction constant(n: number): void {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // ループ内の変数は O(1) の空間を占める\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
space_complexity.dart
/* 関数 */\nint function() {\n  // 何らかの処理を行う\n  return 0;\n}\n\n/* 定数階 */\nvoid constant(int n) {\n  // 定数、変数、オブジェクトは O(1) の空間を占める\n  final int a = 0;\n  int b = 0;\n  List<int> nums = List.filled(10000, 0);\n  ListNode node = ListNode(0);\n  // ループ内の変数は O(1) の空間を占める\n  for (var i = 0; i < n; i++) {\n    int c = 0;\n  }\n  // ループ内の関数は O(1) の空間を占める\n  for (var i = 0; i < n; i++) {\n    function();\n  }\n}\n
space_complexity.rs
/* 関数 */\nfn function() -> i32 {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\n#[allow(unused)]\nfn constant(n: i32) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const A: i32 = 0;\n    let b = 0;\n    let nums = vec![0; 10000];\n    let node = ListNode::new(0);\n    // ループ内の変数は O(1) の空間を占める\n    for i in 0..n {\n        let c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for i in 0..n {\n        function();\n    }\n}\n
space_complexity.c
/* 関数 */\nint func() {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nvoid constant(int n) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const int a = 0;\n    int b = 0;\n    int nums[1000];\n    ListNode *node = newListNode(0);\n    free(node);\n    // ループ内の変数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
space_complexity.kt
/* 関数 */\nfun function(): Int {\n    // 何らかの処理を行う\n    return 0\n}\n\n/* 定数階 */\nfun constant(n: Int) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    val a = 0\n    var b = 0\n    val nums = Array(10000) { 0 }\n    val node = ListNode(0)\n    // ループ内の変数は O(1) の空間を占める\n    for (i in 0..<n) {\n        val c = 0\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (i in 0..<n) {\n        function()\n    }\n}\n
space_complexity.rb
### 関数 ###\ndef function\n  # 何らかの処理を行う\n  0\nend\n\n### 定数階 ###\ndef constant(n)\n  # 定数、変数、オブジェクトは O(1) の空間を占める\n  a = 0\n  nums = [0] * 10000\n  node = ListNode.new\n\n  # ループ内の変数は O(1) の空間を占める\n  (0...n).each { c = 0 }\n  # ループ内の関数は O(1) の空間を占める\n  (0...n).each { function }\nend\n
コードの可視化

全画面で見る >

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#2-on","level":3,"title":"2.   線形階 \\(O(n)\\)","text":"

線形階は、要素数が \\(n\\) に比例する配列、連結リスト、スタック、キューなどによく現れます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def linear(n: int):\n    \"\"\"線形階\"\"\"\n    # 長さ n のリストは O(n) の空間を使用\n    nums = [0] * n\n    # 長さ n のハッシュテーブルは O(n) の空間を使用\n    hmap = dict[int, str]()\n    for i in range(n):\n        hmap[i] = str(i)\n
space_complexity.cpp
/* 線形階 */\nvoid linear(int n) {\n    // 長さ n の配列は O(n) の空間を使用\n    vector<int> nums(n);\n    // 長さ n のリストは O(n) の空間を使用\n    vector<ListNode> nodes;\n    for (int i = 0; i < n; i++) {\n        nodes.push_back(ListNode(i));\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    unordered_map<int, string> map;\n    for (int i = 0; i < n; i++) {\n        map[i] = to_string(i);\n    }\n}\n
space_complexity.java
/* 線形階 */\nvoid linear(int n) {\n    // 長さ n の配列は O(n) の空間を使用\n    int[] nums = new int[n];\n    // 長さ n のリストは O(n) の空間を使用\n    List<ListNode> nodes = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        nodes.add(new ListNode(i));\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    Map<Integer, String> map = new HashMap<>();\n    for (int i = 0; i < n; i++) {\n        map.put(i, String.valueOf(i));\n    }\n}\n
space_complexity.cs
/* 線形階 */\nvoid Linear(int n) {\n    // 長さ n の配列は O(n) の空間を使用\n    int[] nums = new int[n];\n    // 長さ n のリストは O(n) の空間を使用\n    List<ListNode> nodes = [];\n    for (int i = 0; i < n; i++) {\n        nodes.Add(new ListNode(i));\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    Dictionary<int, string> map = [];\n    for (int i = 0; i < n; i++) {\n        map.Add(i, i.ToString());\n    }\n}\n
space_complexity.go
/* 線形階 */\nfunc spaceLinear(n int) {\n    // 長さ n の配列は O(n) の空間を使用\n    _ = make([]int, n)\n    // 長さ n のリストは O(n) の空間を使用\n    var nodes []*node\n    for i := 0; i < n; i++ {\n        nodes = append(nodes, newNode(i))\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    m := make(map[int]string, n)\n    for i := 0; i < n; i++ {\n        m[i] = strconv.Itoa(i)\n    }\n}\n
space_complexity.swift
/* 線形階 */\nfunc linear(n: Int) {\n    // 長さ n の配列は O(n) の空間を使用\n    let nums = Array(repeating: 0, count: n)\n    // 長さ n のリストは O(n) の空間を使用\n    let nodes = (0 ..< n).map { ListNode(x: $0) }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, \"\\($0)\") })\n}\n
space_complexity.js
/* 線形階 */\nfunction linear(n) {\n    // 長さ n の配列は O(n) の空間を使用\n    const nums = new Array(n);\n    // 長さ n のリストは O(n) の空間を使用\n    const nodes = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
space_complexity.ts
/* 線形階 */\nfunction linear(n: number): void {\n    // 長さ n の配列は O(n) の空間を使用\n    const nums = new Array(n);\n    // 長さ n のリストは O(n) の空間を使用\n    const nodes: ListNode[] = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
space_complexity.dart
/* 線形階 */\nvoid linear(int n) {\n  // 長さ n の配列は O(n) の空間を使用\n  List<int> nums = List.filled(n, 0);\n  // 長さ n のリストは O(n) の空間を使用\n  List<ListNode> nodes = [];\n  for (var i = 0; i < n; i++) {\n    nodes.add(ListNode(i));\n  }\n  // 長さ n のハッシュテーブルは O(n) の空間を使用\n  Map<int, String> map = HashMap();\n  for (var i = 0; i < n; i++) {\n    map.putIfAbsent(i, () => i.toString());\n  }\n}\n
space_complexity.rs
/* 線形階 */\n#[allow(unused)]\nfn linear(n: i32) {\n    // 長さ n の配列は O(n) の空間を使用\n    let mut nums = vec![0; n as usize];\n    // 長さ n のリストは O(n) の空間を使用\n    let mut nodes = Vec::new();\n    for i in 0..n {\n        nodes.push(ListNode::new(i))\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    let mut map = HashMap::new();\n    for i in 0..n {\n        map.insert(i, i.to_string());\n    }\n}\n
space_complexity.c
/* ハッシュテーブル */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // uthash.h を用いて実装\n} HashTable;\n\n/* 線形階 */\nvoid linear(int n) {\n    // 長さ n の配列は O(n) の空間を使用\n    int *nums = malloc(sizeof(int) * n);\n    free(nums);\n\n    // 長さ n のリストは O(n) の空間を使用\n    ListNode **nodes = malloc(sizeof(ListNode *) * n);\n    for (int i = 0; i < n; i++) {\n        nodes[i] = newListNode(i);\n    }\n    // メモリを解放する\n    for (int i = 0; i < n; i++) {\n        free(nodes[i]);\n    }\n    free(nodes);\n\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    HashTable *h = NULL;\n    for (int i = 0; i < n; i++) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = i;\n        tmp->val = i;\n        HASH_ADD_INT(h, key, tmp);\n    }\n\n    // メモリを解放する\n    HashTable *curr, *tmp;\n    HASH_ITER(hh, h, curr, tmp) {\n        HASH_DEL(h, curr);\n        free(curr);\n    }\n}\n
space_complexity.kt
/* 線形階 */\nfun linear(n: Int) {\n    // 長さ n の配列は O(n) の空間を使用\n    val nums = Array(n) { 0 }\n    // 長さ n のリストは O(n) の空間を使用\n    val nodes = mutableListOf<ListNode>()\n    for (i in 0..<n) {\n        nodes.add(ListNode(i))\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    val map = mutableMapOf<Int, String>()\n    for (i in 0..<n) {\n        map[i] = i.toString()\n    }\n}\n
space_complexity.rb
### 線形階 ###\ndef linear(n)\n  # 長さ n のリストは O(n) の空間を使用\n  nums = Array.new(n, 0)\n\n  # 長さ n のハッシュテーブルは O(n) の空間を使用\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n
コードの可視化

全画面で見る >

以下の図に示すように、この関数の再帰の深さは \\(n\\) であり、同時に \\(n\\) 個の未返却 linear_recur() 関数が存在するため、\\(O(n)\\) のスタックフレーム空間を使用します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def linear_recur(n: int):\n    \"\"\"線形時間(再帰実装)\"\"\"\n    print(\"再帰 n =\", n)\n    if n == 1:\n        return\n    linear_recur(n - 1)\n
space_complexity.cpp
/* 線形時間(再帰実装) */\nvoid linearRecur(int n) {\n    cout << \"再帰 n = \" << n << endl;\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.java
/* 線形時間(再帰実装) */\nvoid linearRecur(int n) {\n    System.out.println(\"再帰 n = \" + n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.cs
/* 線形時間(再帰実装) */\nvoid LinearRecur(int n) {\n    Console.WriteLine(\"再帰 n = \" + n);\n    if (n == 1) return;\n    LinearRecur(n - 1);\n}\n
space_complexity.go
/* 線形時間(再帰実装) */\nfunc spaceLinearRecur(n int) {\n    fmt.Println(\"再帰 n =\", n)\n    if n == 1 {\n        return\n    }\n    spaceLinearRecur(n - 1)\n}\n
space_complexity.swift
/* 線形時間(再帰実装) */\nfunc linearRecur(n: Int) {\n    print(\"再帰 n = \\(n)\")\n    if n == 1 {\n        return\n    }\n    linearRecur(n: n - 1)\n}\n
space_complexity.js
/* 線形時間(再帰実装) */\nfunction linearRecur(n) {\n    console.log(`再帰 n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
space_complexity.ts
/* 線形時間(再帰実装) */\nfunction linearRecur(n: number): void {\n    console.log(`再帰 n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
space_complexity.dart
/* 線形時間(再帰実装) */\nvoid linearRecur(int n) {\n  print('再帰 n = $n');\n  if (n == 1) return;\n  linearRecur(n - 1);\n}\n
space_complexity.rs
/* 線形時間(再帰実装) */\nfn linear_recur(n: i32) {\n    println!(\"再帰 n = {}\", n);\n    if n == 1 {\n        return;\n    };\n    linear_recur(n - 1);\n}\n
space_complexity.c
/* 線形時間(再帰実装) */\nvoid linearRecur(int n) {\n    printf(\"再帰 n = %d\\r\\n\", n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.kt
/* 線形時間(再帰実装) */\nfun linearRecur(n: Int) {\n    println(\"再帰 n = $n\")\n    if (n == 1)\n        return\n    linearRecur(n - 1)\n}\n
space_complexity.rb
### 線形階 ###\ndef linear(n)\n  # 長さ n のリストは O(n) の空間を使用\n  nums = Array.new(n, 0)\n\n  # 長さ n のハッシュテーブルは O(n) の空間を使用\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n\n# ## 線形階(再帰実装)###\ndef linear_recur(n)\n  puts \"再帰 n = #{n}\"\n  return if n == 1\n  linear_recur(n - 1)\nend\n
コードの可視化

全画面で見る >

図 2-17   再帰関数が生み出す線形階の空間計算量

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#3-on2","level":3,"title":"3.   平方階 \\(O(n^2)\\)","text":"

平方階は、要素数が \\(n\\) の二乗に比例する行列やグラフによく現れます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def quadratic(n: int):\n    \"\"\"二乗階\"\"\"\n    # 二次元リストは O(n^2) の空間を使用\n    num_matrix = [[0] * n for _ in range(n)]\n
space_complexity.cpp
/* 二乗階 */\nvoid quadratic(int n) {\n    // 二次元リストは O(n^2) の空間を使用\n    vector<vector<int>> numMatrix;\n    for (int i = 0; i < n; i++) {\n        vector<int> tmp;\n        for (int j = 0; j < n; j++) {\n            tmp.push_back(0);\n        }\n        numMatrix.push_back(tmp);\n    }\n}\n
space_complexity.java
/* 二乗階 */\nvoid quadratic(int n) {\n    // 行列は O(n^2) の空間を使用する\n    int[][] numMatrix = new int[n][n];\n    // 二次元リストは O(n^2) の空間を使用\n    List<List<Integer>> numList = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<Integer> tmp = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            tmp.add(0);\n        }\n        numList.add(tmp);\n    }\n}\n
space_complexity.cs
/* 二乗階 */\nvoid Quadratic(int n) {\n    // 行列は O(n^2) の空間を使用する\n    int[,] numMatrix = new int[n, n];\n    // 二次元リストは O(n^2) の空間を使用\n    List<List<int>> numList = [];\n    for (int i = 0; i < n; i++) {\n        List<int> tmp = [];\n        for (int j = 0; j < n; j++) {\n            tmp.Add(0);\n        }\n        numList.Add(tmp);\n    }\n}\n
space_complexity.go
/* 二乗階 */\nfunc spaceQuadratic(n int) {\n    // 行列は O(n^2) の空間を使用する\n    numMatrix := make([][]int, n)\n    for i := 0; i < n; i++ {\n        numMatrix[i] = make([]int, n)\n    }\n}\n
space_complexity.swift
/* 二乗階 */\nfunc quadratic(n: Int) {\n    // 二次元リストは O(n^2) の空間を使用\n    let numList = Array(repeating: Array(repeating: 0, count: n), count: n)\n}\n
space_complexity.js
/* 二乗階 */\nfunction quadratic(n) {\n    // 行列は O(n^2) の空間を使用する\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 二次元リストは O(n^2) の空間を使用\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
space_complexity.ts
/* 二乗階 */\nfunction quadratic(n: number): void {\n    // 行列は O(n^2) の空間を使用する\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 二次元リストは O(n^2) の空間を使用\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
space_complexity.dart
/* 二乗階 */\nvoid quadratic(int n) {\n  // 行列は O(n^2) の空間を使用する\n  List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));\n  // 二次元リストは O(n^2) の空間を使用\n  List<List<int>> numList = [];\n  for (var i = 0; i < n; i++) {\n    List<int> tmp = [];\n    for (int j = 0; j < n; j++) {\n      tmp.add(0);\n    }\n    numList.add(tmp);\n  }\n}\n
space_complexity.rs
/* 二乗階 */\n#[allow(unused)]\nfn quadratic(n: i32) {\n    // 行列は O(n^2) の空間を使用する\n    let num_matrix = vec![vec![0; n as usize]; n as usize];\n    // 二次元リストは O(n^2) の空間を使用\n    let mut num_list = Vec::new();\n    for i in 0..n {\n        let mut tmp = Vec::new();\n        for j in 0..n {\n            tmp.push(0);\n        }\n        num_list.push(tmp);\n    }\n}\n
space_complexity.c
/* 二乗階 */\nvoid quadratic(int n) {\n    // 二次元リストは O(n^2) の空間を使用\n    int **numMatrix = malloc(sizeof(int *) * n);\n    for (int i = 0; i < n; i++) {\n        int *tmp = malloc(sizeof(int) * n);\n        for (int j = 0; j < n; j++) {\n            tmp[j] = 0;\n        }\n        numMatrix[i] = tmp;\n    }\n\n    // メモリを解放する\n    for (int i = 0; i < n; i++) {\n        free(numMatrix[i]);\n    }\n    free(numMatrix);\n}\n
space_complexity.kt
/* 二乗階 */\nfun quadratic(n: Int) {\n    // 行列は O(n^2) の空間を使用する\n    val numMatrix = arrayOfNulls<Array<Int>?>(n)\n    // 二次元リストは O(n^2) の空間を使用\n    val numList = mutableListOf<MutableList<Int>>()\n    for (i in 0..<n) {\n        val tmp = mutableListOf<Int>()\n        for (j in 0..<n) {\n            tmp.add(0)\n        }\n        numList.add(tmp)\n    }\n}\n
space_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  # 二次元リストは O(n^2) の空間を使用\n  Array.new(n) { Array.new(n, 0) }\nend\n
コードの可視化

全画面で見る >

以下の図に示すように、この関数の再帰の深さは \\(n\\) であり、各再帰関数の中で長さがそれぞれ \\(n\\)、\\(n-1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) の配列を初期化しています。平均長は \\(n / 2\\) なので、全体では \\(O(n^2)\\) の空間を占有します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def quadratic_recur(n: int) -> int:\n    \"\"\"二次時間(再帰実装)\"\"\"\n    if n <= 0:\n        return 0\n    # 配列 nums の長さは n, n-1, ..., 2, 1\n    nums = [0] * n\n    return quadratic_recur(n - 1)\n
space_complexity.cpp
/* 二次時間(再帰実装) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    vector<int> nums(n);\n    cout << \"再帰 n = \" << n << \" における nums の長さ = \" << nums.size() << endl;\n    return quadraticRecur(n - 1);\n}\n
space_complexity.java
/* 二次時間(再帰実装) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    // 配列 nums の長さは n, n-1, ..., 2, 1\n    int[] nums = new int[n];\n    System.out.println(\"再帰 n = \" + n + \" における nums の長さ = \" + nums.length);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.cs
/* 二次時間(再帰実装) */\nint QuadraticRecur(int n) {\n    if (n <= 0) return 0;\n    int[] nums = new int[n];\n    Console.WriteLine(\"再帰 n = \" + n + \" における nums の長さ = \" + nums.Length);\n    return QuadraticRecur(n - 1);\n}\n
space_complexity.go
/* 二次時間(再帰実装) */\nfunc spaceQuadraticRecur(n int) int {\n    if n <= 0 {\n        return 0\n    }\n    nums := make([]int, n)\n    fmt.Printf(\"再帰 n = %d における nums の長さ = %d \\n\", n, len(nums))\n    return spaceQuadraticRecur(n - 1)\n}\n
space_complexity.swift
/* 二次時間(再帰実装) */\n@discardableResult\nfunc quadraticRecur(n: Int) -> Int {\n    if n <= 0 {\n        return 0\n    }\n    // 配列 nums の長さは n, n-1, ..., 2, 1\n    let nums = Array(repeating: 0, count: n)\n    print(\"再帰 n = \\(n) における nums の長さ = \\(nums.count)\")\n    return quadraticRecur(n: n - 1)\n}\n
space_complexity.js
/* 二次時間(再帰実装) */\nfunction quadraticRecur(n) {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`再帰 n = ${n} における nums の長さ = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.ts
/* 二次時間(再帰実装) */\nfunction quadraticRecur(n: number): number {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`再帰 n = ${n} における nums の長さ = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.dart
/* 二次時間(再帰実装) */\nint quadraticRecur(int n) {\n  if (n <= 0) return 0;\n  List<int> nums = List.filled(n, 0);\n  print('再帰 n = $n における nums の長さ = ${nums.length}');\n  return quadraticRecur(n - 1);\n}\n
space_complexity.rs
/* 二次時間(再帰実装) */\nfn quadratic_recur(n: i32) -> i32 {\n    if n <= 0 {\n        return 0;\n    };\n    // 配列 nums の長さは n, n-1, ..., 2, 1\n    let nums = vec![0; n as usize];\n    println!(\"再帰 n = {} における nums の長さ = {}\", n, nums.len());\n    return quadratic_recur(n - 1);\n}\n
space_complexity.c
/* 二次時間(再帰実装) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    int *nums = malloc(sizeof(int) * n);\n    printf(\"再帰 n = %d における nums の長さ = %d\\r\\n\", n, n);\n    int res = quadraticRecur(n - 1);\n    free(nums);\n    return res;\n}\n
space_complexity.kt
/* 二次時間(再帰実装) */\ntailrec fun quadraticRecur(n: Int): Int {\n    if (n <= 0)\n        return 0\n    // 配列 nums の長さは n, n-1, ..., 2, 1\n    val nums = Array(n) { 0 }\n    println(\"再帰 n = $n における nums の長さ = ${nums.size}\")\n    return quadraticRecur(n - 1)\n}\n
space_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  # 二次元リストは O(n^2) の空間を使用\n  Array.new(n) { Array.new(n, 0) }\nend\n\n# ## 平方階(再帰実装)###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # 配列 nums の長さは n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n
コードの可視化

全画面で見る >

図 2-18   再帰関数が生み出す平方階の空間計算量

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#4-o2n","level":3,"title":"4.   指数階 \\(O(2^n)\\)","text":"

指数階は二分木によく現れます。以下の図を見ると、高さが \\(n\\) の「満二分木」のノード数は \\(2^n - 1\\) であり、\\(O(2^n)\\) の空間を占有します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def build_tree(n: int) -> TreeNode | None:\n    \"\"\"指数時間(完全二分木の構築)\"\"\"\n    if n == 0:\n        return None\n    root = TreeNode(0)\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n    return root\n
space_complexity.cpp
/* 指数時間(完全二分木の構築) */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return nullptr;\n    TreeNode *root = new TreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.java
/* 指数時間(完全二分木の構築) */\nTreeNode buildTree(int n) {\n    if (n == 0)\n        return null;\n    TreeNode root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.cs
/* 指数時間(完全二分木の構築) */\nTreeNode? BuildTree(int n) {\n    if (n == 0) return null;\n    TreeNode root = new(0) {\n        left = BuildTree(n - 1),\n        right = BuildTree(n - 1)\n    };\n    return root;\n}\n
space_complexity.go
/* 指数時間(完全二分木の構築) */\nfunc buildTree(n int) *TreeNode {\n    if n == 0 {\n        return nil\n    }\n    root := NewTreeNode(0)\n    root.Left = buildTree(n - 1)\n    root.Right = buildTree(n - 1)\n    return root\n}\n
space_complexity.swift
/* 指数時間(完全二分木の構築) */\nfunc buildTree(n: Int) -> TreeNode? {\n    if n == 0 {\n        return nil\n    }\n    let root = TreeNode(x: 0)\n    root.left = buildTree(n: n - 1)\n    root.right = buildTree(n: n - 1)\n    return root\n}\n
space_complexity.js
/* 指数時間(完全二分木の構築) */\nfunction buildTree(n) {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.ts
/* 指数時間(完全二分木の構築) */\nfunction buildTree(n: number): TreeNode | null {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.dart
/* 指数時間(完全二分木の構築) */\nTreeNode? buildTree(int n) {\n  if (n == 0) return null;\n  TreeNode root = TreeNode(0);\n  root.left = buildTree(n - 1);\n  root.right = buildTree(n - 1);\n  return root;\n}\n
space_complexity.rs
/* 指数時間(完全二分木の構築) */\nfn build_tree(n: i32) -> Option<Rc<RefCell<TreeNode>>> {\n    if n == 0 {\n        return None;\n    };\n    let root = TreeNode::new(0);\n    root.borrow_mut().left = build_tree(n - 1);\n    root.borrow_mut().right = build_tree(n - 1);\n    return Some(root);\n}\n
space_complexity.c
/* 指数時間(完全二分木の構築) */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return NULL;\n    TreeNode *root = newTreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.kt
/* 指数時間(完全二分木の構築) */\nfun buildTree(n: Int): TreeNode? {\n    if (n == 0)\n        return null\n    val root = TreeNode(0)\n    root.left = buildTree(n - 1)\n    root.right = buildTree(n - 1)\n    return root\n}\n
space_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  # 二次元リストは O(n^2) の空間を使用\n  Array.new(n) { Array.new(n, 0) }\nend\n\n# ## 平方階(再帰実装)###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # 配列 nums の長さは n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n\n# ## 指数階(満二分木を構築)###\ndef build_tree(n)\n  return if n == 0\n\n  TreeNode.new.tap do |root|\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n  end\nend\n
コードの可視化

全画面で見る >

図 2-19   満二分木が生み出す指数階の空間計算量

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#5-olog-n","level":3,"title":"5.   対数階 \\(O(\\log n)\\)","text":"

対数階は分割統治アルゴリズムによく現れます。例えばマージソートでは、長さ \\(n\\) の配列を入力として、各再帰で配列を中央から二つに分割するため、高さ \\(\\log n\\) の再帰木が形成され、\\(O(\\log n)\\) のスタックフレーム空間を使用します。

また、数値を文字列に変換する場合を考えると、正の整数 \\(n\\) の桁数は \\(\\lfloor \\log_{10} n \\rfloor + 1\\) であり、対応する文字列長も \\(\\lfloor \\log_{10} n \\rfloor + 1\\) です。したがって空間計算量は \\(O(\\log_{10} n + 1) = O(\\log n)\\) となります。

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#244","level":2,"title":"2.4.4   時間と空間のトレードオフ","text":"

理想的には、アルゴリズムの時間計算量と空間計算量の両方を最適にしたいところです。しかし実際には、この二つを同時に最適化するのは通常きわめて困難です。

時間計算量を下げるには、通常、空間計算量を増やす代償が必要であり、その逆も同様です。メモリ空間を犠牲にして実行速度を上げる考え方を「空間を時間と引き換えにする」と呼び、その逆を「時間を空間と引き換えにする」と呼びます。

どちらの考え方を選ぶかは、何をより重視するかによって決まります。多くの場合、空間より時間のほうが貴重なので、「空間を時間と引き換えにする」戦略のほうが一般的です。もちろん、データ量が非常に大きい場合には、空間計算量を抑えることも同じくらい重要です。

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/summary/","level":1,"title":"2.5   まとめ","text":"","path":["第 2 章   計算量解析","2.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/summary/#1","level":3,"title":"1.   要点の振り返り","text":"

アルゴリズム効率の評価

  • 時間効率と空間効率は、アルゴリズムの良し悪しを測る二つの主要な評価指標です。
  • 実測によってアルゴリズム効率を評価できますが、テスト環境の影響を排除しにくく、多くの計算資源も消費します。
  • 複雑度分析は実測の欠点を補い、分析結果はすべての実行プラットフォームに適用でき、データ規模ごとの効率も明らかにできます。

時間計算量

  • 時間計算量は、アルゴリズムの実行時間がデータ量の増加に伴ってどう変化するかを測るためのものであり、効率評価に有効です。ただし、入力データ量が小さい場合や時間計算量が同じ場合などには、効率の優劣を正確に比較できないことがあります。
  • 最悪時間計算量はビッグオー記法 \\(O\\) で表され、関数の漸近上界に対応し、\\(n\\) が正の無限大に近づくときの操作回数 \\(T(n)\\) の増加の度合いを表します。
  • 時間計算量の推定は二段階に分かれ、まず操作回数を数え、次に漸近上界を判断します。
  • 一般的な時間計算量を低い順から並べると、\\(O(1)\\)、\\(O(\\log n)\\)、\\(O(n)\\)、\\(O(n \\log n)\\)、\\(O(n^2)\\)、\\(O(2^n)\\)、\\(O(n!)\\) などがあります。
  • 一部のアルゴリズムの時間計算量は固定ではなく、入力データの分布に関係します。時間計算量には最悪、最良、平均時間計算量がありますが、最良時間計算量は入力データが厳しい条件を満たす必要があるため、ほとんど使われません。
  • 平均時間計算量は、ランダムな入力データに対するアルゴリズムの実行効率を表し、実運用時の性能に最も近い指標です。平均時間計算量を求めるには、入力データの分布と、それを踏まえた数学的期待値を統計する必要があります。

空間計算量

  • 空間計算量の役割は時間計算量に似ており、アルゴリズムが使用するメモリ空間がデータ量の増加に伴ってどう変化するかを測ります。
  • アルゴリズム実行中に関係するメモリ空間は、入力空間、一時空間、出力空間に分けられます。通常、入力空間は空間計算量の計算に含めません。一時空間は一時データ、スタックフレーム空間、命令空間に分けられ、このうちスタックフレーム空間は通常、再帰関数でのみ空間計算量に影響します。
  • 私たちは通常、最悪空間計算量のみに注目し、最悪の入力データと最悪の実行時点における空間計算量を数えます。
  • 一般的な空間計算量を低い順から並べると、\\(O(1)\\)、\\(O(\\log n)\\)、\\(O(n)\\)、\\(O(n^2)\\)、\\(O(2^n)\\) などがあります。
","path":["第 2 章   計算量解析","2.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:尾再帰の空間計算量は \\(O(1)\\) ですか?

理論上、尾再帰関数の空間計算量は \\(O(1)\\) まで最適化できます。ただし、ほとんどのプログラミング言語(Java、Python、C++、Go、C# など)は尾再帰の自動最適化をサポートしていないため、通常は空間計算量を \\(O(n)\\) と見なします。

Q:関数とメソッドという二つの用語の違いは何ですか?

関数(function)は独立して実行でき、すべての引数は明示的に渡されます。メソッド(method)はオブジェクトに関連付けられ、それを呼び出すオブジェクトが暗黙的に渡され、クラスのインスタンスに含まれるデータを操作できます。

以下では、いくつかの一般的なプログラミング言語を例に説明します。

  • C 言語は手続き型プログラミング言語であり、オブジェクト指向の概念がないため、関数しかありません。ただし、構造体(struct)を作成してオブジェクト指向プログラミングを模倣でき、構造体に関連付けられた関数は、他のプログラミング言語におけるメソッドに相当します。
  • Java と C# はオブジェクト指向のプログラミング言語であり、コードブロック(メソッド)は通常あるクラスの一部です。静的メソッドの振る舞いは関数に似ており、クラスに束縛され、特定のインスタンス変数にはアクセスできません。
  • C++ と Python は、手続き型プログラミング(関数)にもオブジェクト指向プログラミング(メソッド)にも対応しています。

Q:「一般的な空間計算量の種類」の図が表しているのは、使用空間の絶対量ですか?

いいえ。この図が示しているのは空間計算量であり、表しているのは増加傾向であって、使用空間の絶対量ではありません。

\\(n = 8\\) と仮定すると、各曲線の値が対応する関数と一致していないように見えるかもしれません。これは、各曲線に定数項が含まれており、値の範囲を視覚的に見やすい範囲へ圧縮しているためです。

実際には、各手法の「定数項」の複雑度がどれほどか通常は分からないため、一般に複雑度だけを根拠に \\(n = 8\\) 以下で最適解を選ぶことはできません。ただし、\\(n = 8^5\\) であれば選びやすく、このときは増加傾向がすでに支配的になっています。

Q 実際の利用場面に応じて、時間(または空間)を犠牲にしてアルゴリズムを設計することはありますか?

実際の応用では、多くの場合、空間を犠牲にして時間を得る選択をします。たとえばデータベースのインデックスでは、通常 B+ 木やハッシュインデックスを構築し、大量のメモリ空間を使う代わりに、\\(O(\\log n)\\) あるいは \\(O(1)\\) の高速な検索を実現します。

空間資源が貴重な場面では、時間を犠牲にして空間を得ることもあります。たとえば組み込み開発では、デバイスのメモリが非常に貴重なため、エンジニアはハッシュテーブルの使用をやめ、配列による順次探索を選んでメモリ使用量を節約することがあります。その代償として探索は遅くなります。

","path":["第 2 章   計算量解析","2.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   時間計算量","text":"

実行時間はアルゴリズムの効率を直感的かつ正確に反映します。あるコードの実行時間を正確に見積もりたい場合、どのようにすればよいでしょうか?

  1. 実行プラットフォームを特定する。ハードウェア構成、プログラミング言語、システム環境などが含まれ、これらの要因はいずれもコードの実行効率に影響します。
  2. 各種計算操作に必要な実行時間を評価する。例えば加算 + には 1 ns 、乗算 * には 10 ns 、出力 print() には 5 ns などが必要です。
  3. コード中のすべての計算操作を数える。そして各操作の実行時間を合計することで、実行時間を得ます。

例えば次のコードでは、入力データサイズを \\(n\\) とします:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# ある実行プラットフォーム上で\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # n 回ループ\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
// ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // n 回ループ\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // n 回ループ\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // n 回ループ\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // n 回ループ\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // n 回ループ\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
// ある実行プラットフォーム上で\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // n 回ループ\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // n 回ループ\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
# ある実行プラットフォーム上で\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # n 回ループ\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

上記の方法に基づくと、アルゴリズムの実行時間は \\((6n + 12)\\) ns になります:

\\[ 1 + 1 + 10 + (1 + 5) \\times n = 6n + 12 \\]

しかし実際には、アルゴリズムの実行時間を統計的に求めることは合理的でも現実的でもありません。まず、見積もり時間を実行プラットフォームに縛りたくありません。アルゴリズムはさまざまな異なるプラットフォームで動作する必要があるからです。次に、各種操作の実行時間を知ること自体が難しく、見積もりの難易度を大きく引き上げます。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#231","level":2,"title":"2.3.1   実行時間の増加傾向を捉える","text":"

時間計算量の分析で扱うのはアルゴリズムの実行時間そのものではなく、**データ量が増えたときに実行時間がどう増加するかという傾向**です。

「実行時間の増加傾向」という概念はやや抽象的なので、例を通して理解してみましょう。入力データサイズを \\(n\\) とし、3 つのアルゴリズム ABC を考えます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# アルゴリズム A の時間計算量:定数階\ndef algorithm_A(n: int):\n    print(0)\n# アルゴリズム B の時間計算量:線形階\ndef algorithm_B(n: int):\n    for _ in range(n):\n        print(0)\n# アルゴリズム C の時間計算量:定数階\ndef algorithm_C(n: int):\n    for _ in range(1000000):\n        print(0)\n
// アルゴリズム A の時間計算量:定数階\nvoid algorithm_A(int n) {\n    cout << 0 << endl;\n}\n// アルゴリズム B の時間計算量:線形階\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        cout << 0 << endl;\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        cout << 0 << endl;\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nvoid algorithm_A(int n) {\n    System.out.println(0);\n}\n// アルゴリズム B の時間計算量:線形階\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        System.out.println(0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        System.out.println(0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nvoid AlgorithmA(int n) {\n    Console.WriteLine(0);\n}\n// アルゴリズム B の時間計算量:線形階\nvoid AlgorithmB(int n) {\n    for (int i = 0; i < n; i++) {\n        Console.WriteLine(0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nvoid AlgorithmC(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        Console.WriteLine(0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nfunc algorithm_A(n int) {\n    fmt.Println(0)\n}\n// アルゴリズム B の時間計算量:線形階\nfunc algorithm_B(n int) {\n    for i := 0; i < n; i++ {\n        fmt.Println(0)\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nfunc algorithm_C(n int) {\n    for i := 0; i < 1000000; i++ {\n        fmt.Println(0)\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nfunc algorithmA(n: Int) {\n    print(0)\n}\n\n// アルゴリズム B の時間計算量:線形階\nfunc algorithmB(n: Int) {\n    for _ in 0 ..< n {\n        print(0)\n    }\n}\n\n// アルゴリズム C の時間計算量:定数階\nfunc algorithmC(n: Int) {\n    for _ in 0 ..< 1_000_000 {\n        print(0)\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nfunction algorithm_A(n) {\n    console.log(0);\n}\n// アルゴリズム B の時間計算量:線形階\nfunction algorithm_B(n) {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nfunction algorithm_C(n) {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nfunction algorithm_A(n: number): void {\n    console.log(0);\n}\n// アルゴリズム B の時間計算量:線形階\nfunction algorithm_B(n: number): void {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nfunction algorithm_C(n: number): void {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nvoid algorithmA(int n) {\n  print(0);\n}\n// アルゴリズム B の時間計算量:線形階\nvoid algorithmB(int n) {\n  for (int i = 0; i < n; i++) {\n    print(0);\n  }\n}\n// アルゴリズム C の時間計算量:定数階\nvoid algorithmC(int n) {\n  for (int i = 0; i < 1000000; i++) {\n    print(0);\n  }\n}\n
// アルゴリズム A の時間計算量:定数階\nfn algorithm_A(n: i32) {\n    println!(\"{}\", 0);\n}\n// アルゴリズム B の時間計算量:線形階\nfn algorithm_B(n: i32) {\n    for _ in 0..n {\n        println!(\"{}\", 0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nfn algorithm_C(n: i32) {\n    for _ in 0..1000000 {\n        println!(\"{}\", 0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nvoid algorithm_A(int n) {\n    printf(\"%d\", 0);\n}\n// アルゴリズム B の時間計算量:線形階\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        printf(\"%d\", 0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        printf(\"%d\", 0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nfun algoritm_A(n: Int) {\n    println(0)\n}\n// アルゴリズム B の時間計算量:線形階\nfun algorithm_B(n: Int) {\n    for (i in 0..<n){\n        println(0)\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nfun algorithm_C(n: Int) {\n    for (i in 0..<1000000) {\n        println(0)\n    }\n}\n
# アルゴリズム A の時間計算量:定数階\ndef algorithm_A(n)\n    puts 0\nend\n\n# アルゴリズム B の時間計算量:線形階\ndef algorithm_B(n)\n    (0...n).each { puts 0 }\nend\n\n# アルゴリズム C の時間計算量:定数階\ndef algorithm_C(n)\n    (0...1_000_000).each { puts 0 }\nend\n

以下の図は、上記 3 つのアルゴリズム関数の時間計算量を示しています。

  • アルゴリズム A には出力操作が \\(1\\) 回しかなく、実行時間は \\(n\\) が大きくなっても増加しません。このアルゴリズムの時間計算量を「定数階」と呼びます。
  • アルゴリズム B の出力操作は \\(n\\) 回ループする必要があり、実行時間は \\(n\\) の増加に対して線形に増加します。このアルゴリズムの時間計算量は「線形階」と呼ばれます。
  • アルゴリズム C の出力操作は \\(1000000\\) 回ループする必要があり、実行時間は長いものの、入力データサイズ \\(n\\) とは無関係です。したがって C の時間計算量は A と同じく、依然として「定数階」です。

図 2-7   アルゴリズム A、B、C の時間増加傾向

アルゴリズムの実行時間を直接数える方法と比べて、時間計算量分析にはどのような特徴があるでしょうか?

  • 時間計算量はアルゴリズム効率を有効に評価できます。例えばアルゴリズム B の実行時間は線形に増加するため、\\(n > 1\\) ではアルゴリズム A より遅く、\\(n > 1000000\\) ではアルゴリズム C より遅くなります。実際、入力データサイズ \\(n\\) が十分に大きければ、「定数階」のアルゴリズムは必ず「線形階」のアルゴリズムより優れます。これが実行時間の増加傾向の意味です。
  • 時間計算量の見積もり方法はより簡潔です。実行プラットフォームや計算操作の種類は、アルゴリズム実行時間の増加傾向とは無関係です。そのため時間計算量分析では、すべての計算操作の実行時間を同じ「単位時間」とみなしてよく、「計算操作の実行時間を数える」作業を「計算操作の個数を数える」作業へ簡略化できます。これにより見積もりの難易度は大きく下がります。
  • 時間計算量には一定の限界もあります。例えばアルゴリズム AC の時間計算量は同じでも、実際の実行時間には大きな差があります。同様に、アルゴリズム B の時間計算量は C より高いものの、入力データサイズ \\(n\\) が小さい場合にはアルゴリズム B のほうが明らかに優れます。このような場合、時間計算量だけでアルゴリズム効率の高低を判断するのは難しいことがあります。もっとも、こうした問題があっても、複雑度分析は依然としてアルゴリズム効率を評価する最も有効で一般的な方法です。
","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#232","level":2,"title":"2.3.2   関数の漸近上界","text":"

入力サイズが \\(n\\) の次の関数を考えます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 1      # +1\n    a = a + 1  # +1\n    a = a * 2  # +1\n    # n 回ループ\n    for i in range(n):  # +1\n        print(0)        # +1\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // n 回ループ\n    for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行)\n        cout << 0 << endl;    // +1\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // n 回ループ\n    for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行)\n        System.out.println(0);    // +1\n    }\n}\n
void Algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // n 回ループ\n    for (int i = 0; i < n; i++) {   // +1(各反復で i ++ を実行)\n        Console.WriteLine(0);   // +1\n    }\n}\n
func algorithm(n int) {\n    a := 1      // +1\n    a = a + 1   // +1\n    a = a * 2   // +1\n    // n 回ループ\n    for i := 0; i < n; i++ {   // +1\n        fmt.Println(a)         // +1\n    }\n}\n
func algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // n 回ループ\n    for _ in 0 ..< n { // +1\n        print(0) // +1\n    }\n}\n
function algorithm(n) {\n    var a = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // n 回ループ\n    for(let i = 0; i < n; i++){ // +1(各反復で i ++ を実行)\n        console.log(0); // +1\n    }\n}\n
function algorithm(n: number): void{\n    var a: number = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // n 回ループ\n    for(let i = 0; i < n; i++){ // +1(各反復で i ++ を実行)\n        console.log(0); // +1\n    }\n}\n
void algorithm(int n) {\n  int a = 1; // +1\n  a = a + 1; // +1\n  a = a * 2; // +1\n  // n 回ループ\n  for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行)\n    print(0); // +1\n  }\n}\n
fn algorithm(n: i32) {\n    let mut a = 1;   // +1\n    a = a + 1;      // +1\n    a = a * 2;      // +1\n\n    // n 回ループ\n    for _ in 0..n { // +1(各反復で i ++ を実行)\n        println!(\"{}\", 0); // +1\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // n 回ループ\n    for (int i = 0; i < n; i++) {   // +1(各反復で i ++ を実行)\n        printf(\"%d\", 0);            // +1\n    }\n}\n
fun algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // n 回ループ\n    for (i in 0..<n) { // +1(各反復で i ++ を実行)\n        println(0) // +1\n    }\n}\n
def algorithm(n)\n    a = 1       # +1\n    a = a + 1   # +1\n    a = a * 2   # +1\n    # n 回ループ\n    (0...n).each do # +1\n        puts 0      # +1\n    end\nend\n

アルゴリズムの操作回数を入力データサイズ \\(n\\) の関数とし、\\(T(n)\\) と表すと、上の関数の操作回数は次のようになります:

\\[ T(n) = 3 + 2n \\]

\\(T(n)\\) は一次関数であり、実行時間の増加傾向が線形であることを示しています。したがってその時間計算量は線形階です。

線形階の時間計算量を \\(O(n)\\) と表します。この数学記号はビッグ \\(O\\) 記法(big-\\(O\\) notation)と呼ばれ、関数 \\(T(n)\\) の漸近上界(asymptotic upper bound)を表します。

時間計算量の分析は本質的に「操作回数 \\(T(n)\\)」の漸近上界を求めることであり、明確な数学的定義があります。

関数の漸近上界

正の実数 \\(c\\) と実数 \\(n_0\\) が存在し、すべての \\(n > n_0\\) について \\(T(n) \\leq c \\cdot f(n)\\) が成り立つならば、\\(f(n)\\) は \\(T(n)\\) の漸近上界の 1 つであるとみなせます。これを \\(T(n) = O(f(n))\\) と記します。

下図のように、漸近上界を求めるとは関数 \\(f(n)\\) を探すことであり、\\(n\\) が無限大へ近づくときに \\(T(n)\\) と \\(f(n)\\) が同じ増加オーダーにあり、定数係数 \\(c\\) だけが異なる状態を表します。

図 2-8   関数の漸近上界

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#233","level":2,"title":"2.3.3   求め方","text":"

漸近上界はやや数学色が強い概念ですが、完全に理解できていなくても心配はいりません。まずは求め方を押さえ、実践を重ねる中で徐々にその数学的意味をつかめば十分です。

定義より、\\(f(n)\\) が定まれば時間計算量 \\(O(f(n))\\) が得られます。では、漸近上界 \\(f(n)\\) をどのように決めればよいのでしょうか。大きく 2 段階あります。まず操作回数を数え、その後で漸近上界を判断します。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-1","level":3,"title":"1.   第 1 ステップ:操作回数を数える","text":"

コードについては、上から下へ 1 行ずつ数えれば十分です。しかし、前述の \\(c \\cdot f(n)\\) における定数係数 \\(c\\) は任意に大きく取れるため、操作回数 \\(T(n)\\) に含まれるさまざまな係数や定数項は無視できます。この原則から、次のような簡略化のコツが得られます。

  1. \\(T(n)\\) 中の定数を無視する。それらはすべて \\(n\\) と無関係なので、時間計算量には影響しません。
  2. すべての係数を省略する。例えば \\(2n\\) 回や \\(5n + 1\\) 回のループは、いずれも \\(n\\) 回と簡略化できます。\\(n\\) の前の係数は時間計算量に影響しないからです。
  3. ループが入れ子のときは乗算を使う。総操作回数は外側のループと内側のループの操作回数の積に等しく、各ループ層には引き続き 1.2. のコツをそれぞれ適用できます。

次の関数では、上記のコツを使って操作回数を数えられます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 1      # +0(コツ 1)\n    a = a + n  # +0(コツ 1)\n    # +n(コツ 2)\n    for i in range(5 * n + 1):\n        print(0)\n    # +n*n(コツ 3)\n    for i in range(2 * n):\n        for j in range(n + 1):\n            print(0)\n
void algorithm(int n) {\n    int a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        cout << 0 << endl;\n    }\n    // +n*n(コツ 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            cout << 0 << endl;\n        }\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        System.out.println(0);\n    }\n    // +n*n(コツ 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            System.out.println(0);\n        }\n    }\n}\n
void Algorithm(int n) {\n    int a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        Console.WriteLine(0);\n    }\n    // +n*n(コツ 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            Console.WriteLine(0);\n        }\n    }\n}\n
func algorithm(n int) {\n    a := 1     // +0(コツ 1)\n    a = a + n  // +0(コツ 1)\n    // +n(コツ 2)\n    for i := 0; i < 5 * n + 1; i++ {\n        fmt.Println(0)\n    }\n    // +n*n(コツ 3)\n    for i := 0; i < 2 * n; i++ {\n        for j := 0; j < n + 1; j++ {\n            fmt.Println(0)\n        }\n    }\n}\n
func algorithm(n: Int) {\n    var a = 1 // +0(コツ 1)\n    a = a + n // +0(コツ 1)\n    // +n(コツ 2)\n    for _ in 0 ..< (5 * n + 1) {\n        print(0)\n    }\n    // +n*n(コツ 3)\n    for _ in 0 ..< (2 * n) {\n        for _ in 0 ..< (n + 1) {\n            print(0)\n        }\n    }\n}\n
function algorithm(n) {\n    let a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n(コツ 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
function algorithm(n: number): void {\n    let a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n(コツ 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
void algorithm(int n) {\n  int a = 1; // +0(コツ 1)\n  a = a + n; // +0(コツ 1)\n  // +n(コツ 2)\n  for (int i = 0; i < 5 * n + 1; i++) {\n    print(0);\n  }\n  // +n*n(コツ 3)\n  for (int i = 0; i < 2 * n; i++) {\n    for (int j = 0; j < n + 1; j++) {\n      print(0);\n    }\n  }\n}\n
fn algorithm(n: i32) {\n    let mut a = 1;     // +0(コツ 1)\n    a = a + n;        // +0(コツ 1)\n\n    // +n(コツ 2)\n    for i in 0..(5 * n + 1) {\n        println!(\"{}\", 0);\n    }\n\n    // +n*n(コツ 3)\n    for i in 0..(2 * n) {\n        for j in 0..(n + 1) {\n            println!(\"{}\", 0);\n        }\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        printf(\"%d\", 0);\n    }\n    // +n*n(コツ 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            printf(\"%d\", 0);\n        }\n    }\n}\n
fun algorithm(n: Int) {\n    var a = 1   // +0(コツ 1)\n    a = a + n   // +0(コツ 1)\n    // +n(コツ 2)\n    for (i in 0..<5 * n + 1) {\n        println(0)\n    }\n    // +n*n(コツ 3)\n    for (i in 0..<2 * n) {\n        for (j in 0..<n + 1) {\n            println(0)\n        }\n    }\n}\n
def algorithm(n)\n    a = 1       # +0(コツ 1)\n    a = a + n   # +0(コツ 1)\n    # +n(コツ 2)\n    (0...(5 * n + 1)).each do { puts 0 }\n    # +n*n(コツ 3)\n    (0...(2 * n)).each do\n        (0...(n + 1)).each do { puts 0 }\n    end\nend\n

次の式は、上記のコツを使う前後の集計結果を示したもので、どちらから求めても時間計算量は \\(O(n^2)\\) です。

\\[ \\begin{aligned} T(n) & = 2n(n + 1) + (5n + 1) + 2 & \\text{厳密集計 (-.-|||)} \\newline & = 2n^2 + 7n + 3 \\newline T(n) & = n^2 + n & \\text{手抜き集計 (o.O)} \\end{aligned} \\]","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-2","level":3,"title":"2.   第 2 ステップ:漸近上界を判断する","text":"

時間計算量は \\(T(n)\\) の最高次の項によって決まります。これは、\\(n\\) が無限大に近づくとき、最高次の項が支配的となり、他の項の影響は無視できるからです。

以下の表はその例です。いくつか極端な値を入れているのは、「係数では次数は変わらない」という結論を強調するためです。\\(n\\) が無限大に近づくと、これらの定数は重要でなくなります。

表 2-2   異なる操作回数に対応する時間計算量

操作回数 \\(T(n)\\) 時間計算量 \\(O(f(n))\\) \\(100000\\) \\(O(1)\\) \\(3n + 2\\) \\(O(n)\\) \\(2n^2 + 3n + 2\\) \\(O(n^2)\\) \\(n^3 + 10000n^2\\) \\(O(n^3)\\) \\(2^n + 10000n^{10000}\\) \\(O(2^n)\\)","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#234","level":2,"title":"2.3.4   よくある種類","text":"

入力データサイズを \\(n\\) とすると、よくある時間計算量の種類は次図のとおりです(小さい順に並べています)。

\\[ \\begin{aligned} O(1) < O(\\log n) < O(n) < O(n \\log n) < O(n^2) < O(2^n) < O(n!) \\newline \\text{定数階} < \\text{対数階} < \\text{線形階} < \\text{線形対数階} < \\text{平方階} < \\text{指数階} < \\text{階乗階} \\end{aligned} \\]

図 2-9   よくある時間計算量の種類

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-o1","level":3,"title":"1.   定数階 \\(O(1)\\)","text":"

定数階の操作回数は入力データサイズ \\(n\\) と無関係であり、\\(n\\) が変化しても増減しません。

次の関数では、操作回数 size が大きい可能性はありますが、入力データサイズ \\(n\\) とは無関係なので、時間計算量は依然として \\(O(1)\\) です:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def constant(n: int) -> int:\n    \"\"\"定数階\"\"\"\n    count = 0\n    size = 100000\n    for _ in range(size):\n        count += 1\n    return count\n
time_complexity.cpp
/* 定数階 */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.java
/* 定数階 */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.cs
/* 定数階 */\nint Constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.go
/* 定数階 */\nfunc constant(n int) int {\n    count := 0\n    size := 100000\n    for i := 0; i < size; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 定数階 */\nfunc constant(n: Int) -> Int {\n    var count = 0\n    let size = 100_000\n    for _ in 0 ..< size {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 定数階 */\nfunction constant(n) {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
time_complexity.ts
/* 定数階 */\nfunction constant(n: number): number {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
time_complexity.dart
/* 定数階 */\nint constant(int n) {\n  int count = 0;\n  int size = 100000;\n  for (var i = 0; i < size; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 定数階 */\nfn constant(n: i32) -> i32 {\n    _ = n;\n    let mut count = 0;\n    let size = 100_000;\n    for _ in 0..size {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 定数階 */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    int i = 0;\n    for (int i = 0; i < size; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 定数階 */\nfun constant(n: Int): Int {\n    var count = 0\n    val size = 100000\n    for (i in 0..<size)\n        count++\n    return count\n}\n
time_complexity.rb
### 定数階 ###\ndef constant(n)\n  count = 0\n  size = 100000\n\n  (0...size).each { count += 1 }\n\n  count\nend\n
コードの可視化

全画面で見る >

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-on","level":3,"title":"2.   線形階 \\(O(n)\\)","text":"

線形階の操作回数は入力データサイズ \\(n\\) に対して線形に増加します。線形階は通常、単一ループに現れます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def linear(n: int) -> int:\n    \"\"\"線形階\"\"\"\n    count = 0\n    for _ in range(n):\n        count += 1\n    return count\n
time_complexity.cpp
/* 線形階 */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.java
/* 線形階 */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.cs
/* 線形階 */\nint Linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.go
/* 線形階 */\nfunc linear(n int) int {\n    count := 0\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 線形階 */\nfunc linear(n: Int) -> Int {\n    var count = 0\n    for _ in 0 ..< n {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 線形階 */\nfunction linear(n) {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
time_complexity.ts
/* 線形階 */\nfunction linear(n: number): number {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
time_complexity.dart
/* 線形階 */\nint linear(int n) {\n  int count = 0;\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 線形階 */\nfn linear(n: i32) -> i32 {\n    let mut count = 0;\n    for _ in 0..n {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 線形階 */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 線形階 */\nfun linear(n: Int): Int {\n    var count = 0\n    for (i in 0..<n)\n        count++\n    return count\n}\n
time_complexity.rb
### 線形階 ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n
コードの可視化

全画面で見る >

配列走査や連結リスト走査などの操作の時間計算量はいずれも \\(O(n)\\) であり、ここでの \\(n\\) は配列または連結リストの長さです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def array_traversal(nums: list[int]) -> int:\n    \"\"\"線形時間(配列を走査)\"\"\"\n    count = 0\n    # ループ回数は配列長に比例する\n    for num in nums:\n        count += 1\n    return count\n
time_complexity.cpp
/* 線形時間(配列を走査) */\nint arrayTraversal(vector<int> &nums) {\n    int count = 0;\n    // ループ回数は配列長に比例する\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* 線形時間(配列を走査) */\nint arrayTraversal(int[] nums) {\n    int count = 0;\n    // ループ回数は配列長に比例する\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* 線形時間(配列を走査) */\nint ArrayTraversal(int[] nums) {\n    int count = 0;\n    // ループ回数は配列長に比例する\n    foreach (int num in nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* 線形時間(配列を走査) */\nfunc arrayTraversal(nums []int) int {\n    count := 0\n    // ループ回数は配列長に比例する\n    for range nums {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 線形時間(配列を走査) */\nfunc arrayTraversal(nums: [Int]) -> Int {\n    var count = 0\n    // ループ回数は配列長に比例する\n    for _ in nums {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 線形時間(配列を走査) */\nfunction arrayTraversal(nums) {\n    let count = 0;\n    // ループ回数は配列長に比例する\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* 線形時間(配列を走査) */\nfunction arrayTraversal(nums: number[]): number {\n    let count = 0;\n    // ループ回数は配列長に比例する\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* 線形時間(配列を走査) */\nint arrayTraversal(List<int> nums) {\n  int count = 0;\n  // ループ回数は配列長に比例する\n  for (var _num in nums) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 線形時間(配列を走査) */\nfn array_traversal(nums: &[i32]) -> i32 {\n    let mut count = 0;\n    // ループ回数は配列長に比例する\n    for _ in nums {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 線形時間(配列を走査) */\nint arrayTraversal(int *nums, int n) {\n    int count = 0;\n    // ループ回数は配列長に比例する\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 線形時間(配列を走査) */\nfun arrayTraversal(nums: IntArray): Int {\n    var count = 0\n    // ループ回数は配列長に比例する\n    for (num in nums) {\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### 線形階 ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n\n# ## 線形階(配列を走査)###\ndef array_traversal(nums)\n  count = 0\n\n  # ループ回数は配列長に比例する\n  for num in nums\n    count += 1\n  end\n\n  count\nend\n
コードの可視化

全画面で見る >

注意すべきなのは、**入力データサイズ \\(n\\) は入力データの型に応じて具体的に定める必要がある**ということです。例えば 1 つ目の例では変数 \\(n\\) が入力データサイズであり、2 つ目の例では配列長 \\(n\\) がデータサイズです。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#3-on2","level":3,"title":"3.   平方階 \\(O(n^2)\\)","text":"

平方階の操作回数は入力データサイズ \\(n\\) に対して二乗のオーダーで増加します。平方階は通常、入れ子ループに現れ、外側のループと内側のループの時間計算量がともに \\(O(n)\\) であるため、全体の時間計算量は \\(O(n^2)\\) になります:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def quadratic(n: int) -> int:\n    \"\"\"二乗階\"\"\"\n    count = 0\n    # ループ回数はデータサイズ n の二乗に比例する\n    for i in range(n):\n        for j in range(n):\n            count += 1\n    return count\n
time_complexity.cpp
/* 二乗階 */\nint quadratic(int n) {\n    int count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.java
/* 二乗階 */\nint quadratic(int n) {\n    int count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.cs
/* 二乗階 */\nint Quadratic(int n) {\n    int count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.go
/* 二乗階 */\nfunc quadratic(n int) int {\n    count := 0\n    // ループ回数はデータサイズ n の二乗に比例する\n    for i := 0; i < n; i++ {\n        for j := 0; j < n; j++ {\n            count++\n        }\n    }\n    return count\n}\n
time_complexity.swift
/* 二乗階 */\nfunc quadratic(n: Int) -> Int {\n    var count = 0\n    // ループ回数はデータサイズ n の二乗に比例する\n    for _ in 0 ..< n {\n        for _ in 0 ..< n {\n            count += 1\n        }\n    }\n    return count\n}\n
time_complexity.js
/* 二乗階 */\nfunction quadratic(n) {\n    let count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.ts
/* 二乗階 */\nfunction quadratic(n: number): number {\n    let count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.dart
/* 二乗階 */\nint quadratic(int n) {\n  int count = 0;\n  // ループ回数はデータサイズ n の二乗に比例する\n  for (int i = 0; i < n; i++) {\n    for (int j = 0; j < n; j++) {\n      count++;\n    }\n  }\n  return count;\n}\n
time_complexity.rs
/* 二乗階 */\nfn quadratic(n: i32) -> i32 {\n    let mut count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for _ in 0..n {\n        for _ in 0..n {\n            count += 1;\n        }\n    }\n    count\n}\n
time_complexity.c
/* 二乗階 */\nint quadratic(int n) {\n    int count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.kt
/* 二乗階 */\nfun quadratic(n: Int): Int {\n    var count = 0\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (i in 0..<n) {\n        for (j in 0..<n) {\n            count++\n        }\n    }\n    return count\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n
コードの可視化

全画面で見る >

以下の図は、定数階・線形階・平方階の 3 種類の時間計算量を比較したものです。

図 2-10   定数階、線形階、平方階の時間計算量

バブルソートを例にとると、外側のループは \\(n - 1\\) 回実行され、内側のループは \\(n-1\\)、\\(n-2\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) 回実行され、平均すると \\(n / 2\\) 回です。したがって時間計算量は \\(O((n - 1) n / 2) = O(n^2)\\) となります:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def bubble_sort(nums: list[int]) -> int:\n    \"\"\"二次時間(バブルソート)\"\"\"\n    count = 0  # カウンタ\n    # 外側のループ:未ソート区間は [0, i]\n    for i in range(len(nums) - 1, 0, -1):\n        # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # nums[j] と nums[j + 1] を交換\n                tmp: int = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3  # 要素交換には 3 回の単位操作が含まれる\n    return count\n
time_complexity.cpp
/* 二次時間(バブルソート) */\nint bubbleSort(vector<int> &nums) {\n    int count = 0; // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.java
/* 二次時間(バブルソート) */\nint bubbleSort(int[] nums) {\n    int count = 0; // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.cs
/* 二次時間(バブルソート) */\nint BubbleSort(int[] nums) {\n    int count = 0;  // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                count += 3;  // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.go
/* 二次時間(バブルソート) */\nfunc bubbleSort(nums []int) int {\n    count := 0 // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // nums[j] と nums[j + 1] を交換\n                tmp := nums[j]\n                nums[j] = nums[j+1]\n                nums[j+1] = tmp\n                count += 3 // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count\n}\n
time_complexity.swift
/* 二次時間(バブルソート) */\nfunc bubbleSort(nums: inout [Int]) -> Int {\n    var count = 0 // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3 // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count\n}\n
time_complexity.js
/* 二次時間(バブルソート) */\nfunction bubbleSort(nums) {\n    let count = 0; // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.ts
/* 二次時間(バブルソート) */\nfunction bubbleSort(nums: number[]): number {\n    let count = 0; // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.dart
/* 二次時間(バブルソート) */\nint bubbleSort(List<int> nums) {\n  int count = 0; // カウンタ\n  // 外側のループ:未ソート区間は [0, i]\n  for (var i = nums.length - 1; i > 0; i--) {\n    // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for (var j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // nums[j] と nums[j + 1] を交換\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        count += 3; // 要素交換には 3 回の単位操作が含まれる\n      }\n    }\n  }\n  return count;\n}\n
time_complexity.rs
/* 二次時間(バブルソート) */\nfn bubble_sort(nums: &mut [i32]) -> i32 {\n    let mut count = 0; // カウンタ\n\n    // 外側のループ:未ソート区間は [0, i]\n    for i in (1..nums.len()).rev() {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    count\n}\n
time_complexity.c
/* 二次時間(バブルソート) */\nint bubbleSort(int *nums, int n) {\n    int count = 0; // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = n - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.kt
/* 二次時間(バブルソート) */\nfun bubbleSort(nums: IntArray): Int {\n    var count = 0 // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                count += 3 // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## 平方階(バブルソート)###\ndef bubble_sort(nums)\n  count = 0  # カウンタ\n\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (nums.length - 1).downto(0)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # 要素交換には 3 回の単位操作が含まれる\n      end\n    end\n  end\n\n  count\nend\n
コードの可視化

全画面で見る >

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#4-o2n","level":3,"title":"4.   指数階 \\(O(2^n)\\)","text":"

生物学における「細胞分裂」は指数階増加の典型例です。初期状態では細胞が \\(1\\) 個あり、1 回分裂すると \\(2\\) 個、2 回分裂すると \\(4\\) 個となり、以下同様に、\\(n\\) 回分裂すると \\(2^n\\) 個の細胞になります。

以下の図とコードは細胞分裂の過程を模擬したもので、時間計算量は \\(O(2^n)\\) です。なお、入力の \\(n\\) は分裂回数を表し、戻り値 count は総分裂回数を表します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def exponential(n: int) -> int:\n    \"\"\"指数時間(ループ実装)\"\"\"\n    count = 0\n    base = 1\n    # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for _ in range(n):\n        for _ in range(base):\n            count += 1\n        base *= 2\n    # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n
time_complexity.cpp
/* 指数時間(ループ実装) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.java
/* 指数時間(ループ実装) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.cs
/* 指数時間(ループ実装) */\nint Exponential(int n) {\n    int count = 0, bas = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.go
/* 指数時間(ループ実装) */\nfunc exponential(n int) int {\n    count, base := 0, 1\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for i := 0; i < n; i++ {\n        for j := 0; j < base; j++ {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.swift
/* 指数時間(ループ実装) */\nfunc exponential(n: Int) -> Int {\n    var count = 0\n    var base = 1\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for _ in 0 ..< n {\n        for _ in 0 ..< base {\n            count += 1\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.js
/* 指数時間(ループ実装) */\nfunction exponential(n) {\n    let count = 0,\n        base = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.ts
/* 指数時間(ループ実装) */\nfunction exponential(n: number): number {\n    let count = 0,\n        base = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.dart
/* 指数時間(ループ実装) */\nint exponential(int n) {\n  int count = 0, base = 1;\n  // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n  for (var i = 0; i < n; i++) {\n    for (var j = 0; j < base; j++) {\n      count++;\n    }\n    base *= 2;\n  }\n  // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  return count;\n}\n
time_complexity.rs
/* 指数時間(ループ実装) */\nfn exponential(n: i32) -> i32 {\n    let mut count = 0;\n    let mut base = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for _ in 0..n {\n        for _ in 0..base {\n            count += 1\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    count\n}\n
time_complexity.c
/* 指数時間(ループ実装) */\nint exponential(int n) {\n    int count = 0;\n    int bas = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.kt
/* 指数時間(ループ実装) */\nfun exponential(n: Int): Int {\n    var count = 0\n    var base = 1\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (i in 0..<n) {\n        for (j in 0..<base) {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## 平方階(バブルソート)###\ndef bubble_sort(nums)\n  count = 0  # カウンタ\n\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (nums.length - 1).downto(0)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # 要素交換には 3 回の単位操作が含まれる\n      end\n    end\n  end\n\n  count\nend\n\n# ## 指数階(ループ実装)###\ndef exponential(n)\n  count, base = 0, 1\n\n  # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n
コードの可視化

全画面で見る >

図 2-11   指数階の時間計算量

実際のアルゴリズムでも、指数階は再帰関数によく現れます。例えば次のコードでは、再帰的に 2 つへ分岐し、\\(n\\) 回分裂した後に停止します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def exp_recur(n: int) -> int:\n    \"\"\"指数時間(再帰実装)\"\"\"\n    if n == 1:\n        return 1\n    return exp_recur(n - 1) + exp_recur(n - 1) + 1\n
time_complexity.cpp
/* 指数時間(再帰実装) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.java
/* 指数時間(再帰実装) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.cs
/* 指数時間(再帰実装) */\nint ExpRecur(int n) {\n    if (n == 1) return 1;\n    return ExpRecur(n - 1) + ExpRecur(n - 1) + 1;\n}\n
time_complexity.go
/* 指数時間(再帰実装) */\nfunc expRecur(n int) int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n-1) + expRecur(n-1) + 1\n}\n
time_complexity.swift
/* 指数時間(再帰実装) */\nfunc expRecur(n: Int) -> Int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n: n - 1) + expRecur(n: n - 1) + 1\n}\n
time_complexity.js
/* 指数時間(再帰実装) */\nfunction expRecur(n) {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.ts
/* 指数時間(再帰実装) */\nfunction expRecur(n: number): number {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.dart
/* 指数時間(再帰実装) */\nint expRecur(int n) {\n  if (n == 1) return 1;\n  return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.rs
/* 指数時間(再帰実装) */\nfn exp_recur(n: i32) -> i32 {\n    if n == 1 {\n        return 1;\n    }\n    exp_recur(n - 1) + exp_recur(n - 1) + 1\n}\n
time_complexity.c
/* 指数時間(再帰実装) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.kt
/* 指数時間(再帰実装) */\nfun expRecur(n: Int): Int {\n    if (n == 1) {\n        return 1\n    }\n    return expRecur(n - 1) + expRecur(n - 1) + 1\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## 平方階(バブルソート)###\ndef bubble_sort(nums)\n  count = 0  # カウンタ\n\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (nums.length - 1).downto(0)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # 要素交換には 3 回の単位操作が含まれる\n      end\n    end\n  end\n\n  count\nend\n\n# ## 指数階(ループ実装)###\ndef exponential(n)\n  count, base = 0, 1\n\n  # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## 指数階(再帰実装)###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n
コードの可視化

全画面で見る >

指数階の増加は非常に速く、全探索法(ブルートフォース、バックトラッキングなど)によく見られます。データ規模が大きい問題では、指数階は受け入れられず、通常は動的計画法や貪欲法などを使って解く必要があります。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#5-olog-n","level":3,"title":"5.   対数階 \\(O(\\log n)\\)","text":"

指数階とは逆に、対数階は「各ラウンドで半分になる」状況を表します。入力データサイズを \\(n\\) とすると、各ラウンドで半減するため、ループ回数は \\(\\log_2 n\\)、すなわち \\(2^n\\) の逆関数になります。

以下の図とコードは、「各ラウンドで半分になる」過程を模擬したもので、時間計算量は \\(O(\\log_2 n)\\)、簡潔には \\(O(\\log n)\\) と書きます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def logarithmic(n: int) -> int:\n    \"\"\"対数時間(ループ実装)\"\"\"\n    count = 0\n    while n > 1:\n        n = n / 2\n        count += 1\n    return count\n
time_complexity.cpp
/* 対数時間(ループ実装) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* 対数時間(ループ実装) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* 対数時間(ループ実装) */\nint Logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n /= 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* 対数時間(ループ実装) */\nfunc logarithmic(n int) int {\n    count := 0\n    for n > 1 {\n        n = n / 2\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 対数時間(ループ実装) */\nfunc logarithmic(n: Int) -> Int {\n    var count = 0\n    var n = n\n    while n > 1 {\n        n = n / 2\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 対数時間(ループ実装) */\nfunction logarithmic(n) {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* 対数時間(ループ実装) */\nfunction logarithmic(n: number): number {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* 対数時間(ループ実装) */\nint logarithmic(int n) {\n  int count = 0;\n  while (n > 1) {\n    n = n ~/ 2;\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 対数時間(ループ実装) */\nfn logarithmic(mut n: i32) -> i32 {\n    let mut count = 0;\n    while n > 1 {\n        n = n / 2;\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 対数時間(ループ実装) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 対数時間(ループ実装) */\nfun logarithmic(n: Int): Int {\n    var n1 = n\n    var count = 0\n    while (n1 > 1) {\n        n1 /= 2\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## 平方階(バブルソート)###\ndef bubble_sort(nums)\n  count = 0  # カウンタ\n\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (nums.length - 1).downto(0)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # 要素交換には 3 回の単位操作が含まれる\n      end\n    end\n  end\n\n  count\nend\n\n# ## 指数階(ループ実装)###\ndef exponential(n)\n  count, base = 0, 1\n\n  # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## 指数階(再帰実装)###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n\n# ## 対数階(ループ実装)###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n
コードの可視化

全画面で見る >

図 2-12   対数階の時間計算量

指数階と同様に、対数階も再帰関数によく現れます。次のコードは高さ \\(\\log_2 n\\) の再帰木を形成します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def log_recur(n: int) -> int:\n    \"\"\"対数時間(再帰実装)\"\"\"\n    if n <= 1:\n        return 0\n    return log_recur(n / 2) + 1\n
time_complexity.cpp
/* 対数時間(再帰実装) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.java
/* 対数時間(再帰実装) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.cs
/* 対数時間(再帰実装) */\nint LogRecur(int n) {\n    if (n <= 1) return 0;\n    return LogRecur(n / 2) + 1;\n}\n
time_complexity.go
/* 対数時間(再帰実装) */\nfunc logRecur(n int) int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n/2) + 1\n}\n
time_complexity.swift
/* 対数時間(再帰実装) */\nfunc logRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n: n / 2) + 1\n}\n
time_complexity.js
/* 対数時間(再帰実装) */\nfunction logRecur(n) {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.ts
/* 対数時間(再帰実装) */\nfunction logRecur(n: number): number {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.dart
/* 対数時間(再帰実装) */\nint logRecur(int n) {\n  if (n <= 1) return 0;\n  return logRecur(n ~/ 2) + 1;\n}\n
time_complexity.rs
/* 対数時間(再帰実装) */\nfn log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 0;\n    }\n    log_recur(n / 2) + 1\n}\n
time_complexity.c
/* 対数時間(再帰実装) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.kt
/* 対数時間(再帰実装) */\nfun logRecur(n: Int): Int {\n    if (n <= 1)\n        return 0\n    return logRecur(n / 2) + 1\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## 平方階(バブルソート)###\ndef bubble_sort(nums)\n  count = 0  # カウンタ\n\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (nums.length - 1).downto(0)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # 要素交換には 3 回の単位操作が含まれる\n      end\n    end\n  end\n\n  count\nend\n\n# ## 指数階(ループ実装)###\ndef exponential(n)\n  count, base = 0, 1\n\n  # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## 指数階(再帰実装)###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n\n# ## 対数階(ループ実装)###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n\n# ## 対数階(再帰実装)###\ndef log_recur(n)\n  return 0 unless n > 1\n  log_recur(n / 2) + 1\nend\n
コードの可視化

全画面で見る >

対数階は分割統治に基づくアルゴリズムによく現れ、「1 つを複数に分ける」「複雑なものを単純化する」という考え方を体現しています。増加は緩やかで、定数階に次いで理想的な時間計算量です。

\\(O(\\log n)\\) の底は何か?

正確には、「\\(m\\) 個に分ける」場合に対応する時間計算量は \\(O(\\log_m n)\\) です。そして対数の底の変換公式により、底が異なっても同値な時間計算量が得られます:

\\[ O(\\log_m n) = O(\\log_k n / \\log_k m) = O(\\log_k n) \\]

つまり、底 \\(m\\) は複雑度に影響を与えずに変換できます。そのため通常は底 \\(m\\) を省略し、対数階を単に \\(O(\\log n)\\) と記します。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#6-on-log-n","level":3,"title":"6.   線形対数階 \\(O(n \\log n)\\)","text":"

線形対数階は入れ子ループによく現れ、2 層のループの時間計算量はそれぞれ \\(O(\\log n)\\) と \\(O(n)\\) です。関連するコードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def linear_log_recur(n: int) -> int:\n    \"\"\"線形対数時間\"\"\"\n    if n <= 1:\n        return 1\n    # 二つに分割すると、部分問題の規模は半分になる\n    count = linear_log_recur(n // 2) + linear_log_recur(n // 2)\n    # 現在の部分問題には n 個の操作が含まれる\n    for _ in range(n):\n        count += 1\n    return count\n
time_complexity.cpp
/* 線形対数時間 */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* 線形対数時間 */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* 線形対数時間 */\nint LinearLogRecur(int n) {\n    if (n <= 1) return 1;\n    int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* 線形対数時間 */\nfunc linearLogRecur(n int) int {\n    if n <= 1 {\n        return 1\n    }\n    count := linearLogRecur(n/2) + linearLogRecur(n/2)\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 線形対数時間 */\nfunc linearLogRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 1\n    }\n    var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2)\n    for _ in stride(from: 0, to: n, by: 1) {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 線形対数時間 */\nfunction linearLogRecur(n) {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* 線形対数時間 */\nfunction linearLogRecur(n: number): number {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* 線形対数時間 */\nint linearLogRecur(int n) {\n  if (n <= 1) return 1;\n  int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2);\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 線形対数時間 */\nfn linear_log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 1;\n    }\n    let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2);\n    for _ in 0..n {\n        count += 1;\n    }\n    return count;\n}\n
time_complexity.c
/* 線形対数時間 */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 線形対数時間 */\nfun linearLogRecur(n: Int): Int {\n    if (n <= 1)\n        return 1\n    var count = linearLogRecur(n / 2) + linearLogRecur(n / 2)\n    for (i in 0..<n) {\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### 線形対数時間 ###\ndef linear_log_recur(n)\n  return 1 unless n > 1\n\n  count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n  (0...n).each { count += 1 }\n\n  count\nend\n
コードの可視化

全画面で見る >

下図は線形対数階がどのように生じるかを示しています。二分木の各層の操作総数はすべて \\(n\\) であり、木全体は \\(\\log_2 n + 1\\) 層あるため、時間計算量は \\(O(n \\log n)\\) です。

図 2-13   線形対数階の時間計算量

主なソートアルゴリズムの時間計算量は通常 \\(O(n \\log n)\\) であり、例えばクイックソート、マージソート、ヒープソートなどがあります。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#7-on","level":3,"title":"7.   階乗階 \\(O(n!)\\)","text":"

階乗階は、数学における「全順列」の問題に対応します。互いに重複しない \\(n\\) 個の要素が与えられたとき、そのすべての並べ方を求めると、通り数は次のようになります:

\\[ n! = n \\times (n - 1) \\times (n - 2) \\times \\dots \\times 2 \\times 1 \\]

階乗は通常、再帰で実装されます。以下の図とコードのように、第 1 層では \\(n\\) 個に分岐し、第 2 層では \\(n - 1\\) 個に分岐し、以下同様に、第 \\(n\\) 層で分岐が停止します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def factorial_recur(n: int) -> int:\n    \"\"\"階乗時間(再帰実装)\"\"\"\n    if n == 0:\n        return 1\n    count = 0\n    # 1個から n 個に分裂\n    for _ in range(n):\n        count += factorial_recur(n - 1)\n    return count\n
time_complexity.cpp
/* 階乗時間(再帰実装) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // 1個から n 個に分裂\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.java
/* 階乗時間(再帰実装) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // 1個から n 個に分裂\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.cs
/* 階乗時間(再帰実装) */\nint FactorialRecur(int n) {\n    if (n == 0) return 1;\n    int count = 0;\n    // 1個から n 個に分裂\n    for (int i = 0; i < n; i++) {\n        count += FactorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.go
/* 階乗時間(再帰実装) */\nfunc factorialRecur(n int) int {\n    if n == 0 {\n        return 1\n    }\n    count := 0\n    // 1個から n 個に分裂\n    for i := 0; i < n; i++ {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
time_complexity.swift
/* 階乗時間(再帰実装) */\nfunc factorialRecur(n: Int) -> Int {\n    if n == 0 {\n        return 1\n    }\n    var count = 0\n    // 1個から n 個に分裂\n    for _ in 0 ..< n {\n        count += factorialRecur(n: n - 1)\n    }\n    return count\n}\n
time_complexity.js
/* 階乗時間(再帰実装) */\nfunction factorialRecur(n) {\n    if (n === 0) return 1;\n    let count = 0;\n    // 1個から n 個に分裂\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.ts
/* 階乗時間(再帰実装) */\nfunction factorialRecur(n: number): number {\n    if (n === 0) return 1;\n    let count = 0;\n    // 1個から n 個に分裂\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.dart
/* 階乗時間(再帰実装) */\nint factorialRecur(int n) {\n  if (n == 0) return 1;\n  int count = 0;\n  // 1個から n 個に分裂\n  for (var i = 0; i < n; i++) {\n    count += factorialRecur(n - 1);\n  }\n  return count;\n}\n
time_complexity.rs
/* 階乗時間(再帰実装) */\nfn factorial_recur(n: i32) -> i32 {\n    if n == 0 {\n        return 1;\n    }\n    let mut count = 0;\n    // 1個から n 個に分裂\n    for _ in 0..n {\n        count += factorial_recur(n - 1);\n    }\n    count\n}\n
time_complexity.c
/* 階乗時間(再帰実装) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.kt
/* 階乗時間(再帰実装) */\nfun factorialRecur(n: Int): Int {\n    if (n == 0)\n        return 1\n    var count = 0\n    // 1個から n 個に分裂\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
time_complexity.rb
### 線形対数時間 ###\ndef linear_log_recur(n)\n  return 1 unless n > 1\n\n  count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n  (0...n).each { count += 1 }\n\n  count\nend\n\n# ## 階乗階(再帰実装)###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # 1個から n 個に分裂\n  (0...n).each { count += factorial_recur(n - 1) }\n\n  count\nend\n
コードの可視化

全画面で見る >

図 2-14   階乗階の時間計算量

注意すべき点として、\\(n \\geq 4\\) なら常に \\(n! > 2^n\\) なので、階乗階は指数階よりもさらに速く増加し、\\(n\\) が大きい場合にはやはり受け入れられません。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#235","level":2,"title":"2.3.5   最悪・最良・平均時間計算量","text":"

アルゴリズムの時間効率は固定ではなく、入力データの分布に左右されることが多いです。長さ \\(n\\) の配列 nums を考えます。nums は \\(1\\) から \\(n\\) までの数字で構成され、各数字は 1 回だけ現れます。ただし要素の順序はランダムにシャッフルされており、目標は要素 \\(1\\) のインデックスを返すことです。ここから次の結論が得られます。

  • nums = [?, ?, ..., 1]、つまり末尾の要素が \\(1\\) の場合は、配列全体を最後まで走査する必要があり、最悪時間計算量 \\(O(n)\\) になります。
  • nums = [1, ?, ?, ...]、つまり先頭要素が \\(1\\) の場合は、配列がどれだけ長くてもそれ以上走査する必要がなく、最良時間計算量 \\(\\Omega(1)\\) になります。

「最悪時間計算量」は関数の漸近上界に対応し、ビッグ \\(O\\) 記法で表します。同様に、「最良時間計算量」は関数の漸近下界に対応し、\\(\\Omega\\) 記法で表します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby worst_best_time_complexity.py
def random_numbers(n: int) -> list[int]:\n    \"\"\"要素が 1, 2, ..., n で順序がシャッフルされた配列を生成する\"\"\"\n    # 配列 nums =: 1, 2, 3, ..., n を生成する\n    nums = [i for i in range(1, n + 1)]\n    # 配列要素をランダムにシャッフル\n    random.shuffle(nums)\n    return nums\n\ndef find_one(nums: list[int]) -> int:\n    \"\"\"配列 nums 内で数値 1 のインデックスを探す\"\"\"\n    for i in range(len(nums)):\n        # 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        # 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if nums[i] == 1:\n            return i\n    return -1\n
worst_best_time_complexity.cpp
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nvector<int> randomNumbers(int n) {\n    vector<int> nums(n);\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // システム時刻を使って乱数シードを生成する\n    unsigned seed = chrono::system_clock::now().time_since_epoch().count();\n    // 配列要素をランダムにシャッフル\n    shuffle(nums.begin(), nums.end(), default_random_engine(seed));\n    return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nint findOne(vector<int> &nums) {\n    for (int i = 0; i < nums.size(); i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.java
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nint[] randomNumbers(int n) {\n    Integer[] nums = new Integer[n];\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 配列要素をランダムにシャッフル\n    Collections.shuffle(Arrays.asList(nums));\n    // Integer[] -> int[]\n    int[] res = new int[n];\n    for (int i = 0; i < n; i++) {\n        res[i] = nums[i];\n    }\n    return res;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nint findOne(int[] nums) {\n    for (int i = 0; i < nums.length; i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.cs
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nint[] RandomNumbers(int n) {\n    int[] nums = new int[n];\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n\n    // 配列要素をランダムにシャッフル\n    for (int i = 0; i < nums.Length; i++) {\n        int index = new Random().Next(i, nums.Length);\n        (nums[i], nums[index]) = (nums[index], nums[i]);\n    }\n    return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nint FindOne(int[] nums) {\n    for (int i = 0; i < nums.Length; i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.go
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfunc randomNumbers(n int) []int {\n    nums := make([]int, n)\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for i := 0; i < n; i++ {\n        nums[i] = i + 1\n    }\n    // 配列要素をランダムにシャッフル\n    rand.Shuffle(len(nums), func(i, j int) {\n        nums[i], nums[j] = nums[j], nums[i]\n    })\n    return nums\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfunc findOne(nums []int) int {\n    for i := 0; i < len(nums); i++ {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
worst_best_time_complexity.swift
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfunc randomNumbers(n: Int) -> [Int] {\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    var nums = Array(1 ... n)\n    // 配列要素をランダムにシャッフル\n    nums.shuffle()\n    return nums\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfunc findOne(nums: [Int]) -> Int {\n    for i in nums.indices {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
worst_best_time_complexity.js
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfunction randomNumbers(n) {\n    const nums = Array(n);\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 配列要素をランダムにシャッフル\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfunction findOne(nums) {\n    for (let i = 0; i < nums.length; i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
worst_best_time_complexity.ts
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfunction randomNumbers(n: number): number[] {\n    const nums = Array(n);\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 配列要素をランダムにシャッフル\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfunction findOne(nums: number[]): number {\n    for (let i = 0; i < nums.length; i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
worst_best_time_complexity.dart
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nList<int> randomNumbers(int n) {\n  final nums = List.filled(n, 0);\n  // 配列 nums = { 1, 2, 3, ..., n } を生成\n  for (var i = 0; i < n; i++) {\n    nums[i] = i + 1;\n  }\n  // 配列要素をランダムにシャッフル\n  nums.shuffle();\n\n  return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nint findOne(List<int> nums) {\n  for (var i = 0; i < nums.length; i++) {\n    // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n    // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n    if (nums[i] == 1) return i;\n  }\n\n  return -1;\n}\n
worst_best_time_complexity.rs
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfn random_numbers(n: i32) -> Vec<i32> {\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    let mut nums = (1..=n).collect::<Vec<i32>>();\n    // 配列要素をランダムにシャッフル\n    nums.shuffle(&mut thread_rng());\n    nums\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfn find_one(nums: &[i32]) -> Option<usize> {\n    for i in 0..nums.len() {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if nums[i] == 1 {\n            return Some(i);\n        }\n    }\n    None\n}\n
worst_best_time_complexity.c
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nint *randomNumbers(int n) {\n    // ヒープ領域にメモリを確保する(要素数 n、要素型 int の一次元可変長配列を作成)\n    int *nums = (int *)malloc(n * sizeof(int));\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 配列要素をランダムにシャッフル\n    for (int i = n - 1; i > 0; i--) {\n        int j = rand() % (i + 1);\n        int temp = nums[i];\n        nums[i] = nums[j];\n        nums[j] = temp;\n    }\n    return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nint findOne(int *nums, int n) {\n    for (int i = 0; i < n; i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.kt
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfun randomNumbers(n: Int): Array<Int?> {\n    val nums = IntArray(n)\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (i in 0..<n) {\n        nums[i] = i + 1\n    }\n    // 配列要素をランダムにシャッフル\n    nums.shuffle()\n    val res = arrayOfNulls<Int>(n)\n    for (i in 0..<n) {\n        res[i] = nums[i]\n    }\n    return res\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfun findOne(nums: Array<Int?>): Int {\n    for (i in nums.indices) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] == 1)\n            return i\n    }\n    return -1\n}\n
worst_best_time_complexity.rb
### 1, 2, ..., n を要素とする配列を生成し、順序をシャッフルする ###\ndef random_numbers(n)\n  # 配列 nums =: 1, 2, 3, ..., n を生成する\n  nums = Array.new(n) { |i| i + 1 }\n  # 配列要素をランダムにシャッフル\n  nums.shuffle!\nend\n\n### 配列 nums 内の数値 1 のインデックスを探す ###\ndef find_one(nums)\n  for i in 0...nums.length\n    # 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n    # 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n    return i if nums[i] == 1\n  end\n\n  -1\nend\n
コードの可視化

全画面で見る >

実際には、最良時間計算量を使うことはあまりありません。通常それが実現する確率はごく低く、誤解を招く可能性があるからです。**一方で最悪時間計算量はより実用的で、効率の安全側の目安を与えてくれる**ため、安心してアルゴリズムを使えます。

上の例から分かるように、最悪時間計算量と最良時間計算量は「特殊なデータ分布」でのみ現れ、その発生確率は低いことが多く、アルゴリズムの実行効率をそのまま正確に反映するわけではありません。それに対して、**平均時間計算量はランダム入力におけるアルゴリズムの実行効率を表せる**ため、\\(\\Theta\\) 記法で表します。

一部のアルゴリズムでは、ランダムなデータ分布における平均的な状況を比較的簡単に求められます。例えば上の例では、入力配列はシャッフルされているため、要素 \\(1\\) が任意のインデックスに現れる確率は等しいです。したがってアルゴリズムの平均ループ回数は配列長の半分 \\(n / 2\\) となり、平均時間計算量は \\(\\Theta(n / 2) = \\Theta(n)\\) です。

しかし、より複雑なアルゴリズムでは、平均時間計算量を計算するのはしばしば困難です。データ分布に対する全体の数学的期待値を分析するのが難しいからです。そのような場合、通常は最悪時間計算量をアルゴリズム効率の評価基準として用います。

なぜ \\(\\Theta\\) 記号をあまり見かけないのか?

おそらく \\(O\\) 記号のほうが口にしやすいため、平均時間計算量を表すのにもよく使われます。ただし厳密には、この用法は正確ではありません。本書や他の資料で「平均時間計算量 \\(O(n)\\)」のような表現を見かけた場合は、そのまま \\(\\Theta(n)\\) と理解してください。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_data_structure/","level":1,"title":"第 3 章   データ構造","text":"

Abstract

データ構造は、堅固で多様な枠組みのようなものである。

それはデータを秩序立てて組織するための青写真を示し、アルゴリズムはその上で生き生きと動き出す。

","path":["第 3 章   データ構造"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"章の内容","text":"
  • 3.1   データ構造の分類
  • 3.2   基本データ型
  • 3.3   数値エンコーディング *
  • 3.4   文字エンコーディング *
  • 3.5   まとめ
","path":["第 3 章   データ構造"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   基本データ型","text":"

コンピュータ内のデータについて考えるとき、テキスト、画像、動画、音声、3D モデルなど、さまざまな形態を思い浮かべます。これらのデータの構成形式はそれぞれ異なりますが、いずれも各種の基本データ型によって成り立っています。

**基本データ型は CPU が直接演算できる型**であり、アルゴリズムの中で直接使われます。主なものは次のとおりです。

  • 整数型 byteshortintlong
  • 浮動小数点数型 floatdouble ,小数を表すために使います。
  • 文字型 char ,各言語の文字、句読点、さらには絵文字などを表すために使います。
  • 真偽値型 bool ,真か偽かの判定を表すために使います。

基本データ型はコンピュータ内で 2 進数の形で格納されます。1 つの二進桁は \\(1\\) ビットです。現代のほとんどのオペレーティングシステムでは、\\(1\\) バイト(byte)は \\(8\\) ビット(bit)で構成されます。

基本データ型の値域は、その型が占める領域の大きさによって決まります。以下では Java を例に取ります。

  • 整数型 byte は \\(1\\) バイト = \\(8\\) ビットを占め、\\(2^{8}\\) 個の数を表せます。
  • 整数型 int は \\(4\\) バイト = \\(32\\) ビットを占め、\\(2^{32}\\) 個の数を表せます。

下表は、Java における各種基本データ型の使用領域、値域、デフォルト値を示したものです。この表を丸暗記する必要はなく、大まかに理解しておけば十分であり、必要になったときに参照すればかまいません。

表 3-1   基本データ型の使用領域と値域

型 記号 使用領域 最小値 最大値 デフォルト値 整数 byte 1 バイト \\(-2^7\\) (\\(-128\\)) \\(2^7 - 1\\) (\\(127\\)) \\(0\\) short 2 バイト \\(-2^{15}\\) \\(2^{15} - 1\\) \\(0\\) int 4 バイト \\(-2^{31}\\) \\(2^{31} - 1\\) \\(0\\) long 8 バイト \\(-2^{63}\\) \\(2^{63} - 1\\) \\(0\\) 浮動小数点数 float 4 バイト \\(1.175 \\times 10^{-38}\\) \\(3.403 \\times 10^{38}\\) \\(0.0\\text{f}\\) double 8 バイト \\(2.225 \\times 10^{-308}\\) \\(1.798 \\times 10^{308}\\) \\(0.0\\) 文字 char 2 バイト \\(0\\) \\(2^{16} - 1\\) \\(0\\) 真偽値 bool 1 バイト \\(\\text{false}\\) \\(\\text{true}\\) \\(\\text{false}\\)

注意してください。上表は Java の基本データ型に対するものです。各プログラミング言語にはそれぞれ独自のデータ型定義があり、使用領域、値域、デフォルト値は異なる場合があります。

  • Python では、整数型 int は利用可能なメモリに制限されるだけで任意の大きさを取れます。浮動小数点数 float は倍精度 64 ビットです。char 型はなく、1 文字は実際には長さ 1 の文字列 str です。
  • C と C++ では基本データ型の大きさは明確に規定されておらず、実装やプラットフォームによって異なります。上表は LP64 データモデル に従っており、Linux や macOS を含む Unix 系 64 ビット OS で用いられています。
  • char の大きさは C と C++ では 1 バイトですが、多くのプログラミング言語では採用する文字エンコーディング方式によって決まります。詳しくは「文字エンコーディング」の章を参照してください。
  • 真偽値を表すのに必要なのは 1 ビット(\\(0\\) または \\(1\\))だけですが、メモリ上では通常 1 バイトとして格納されます。これは、現代のコンピュータ CPU が通常 1 バイトを最小のアドレス指定可能なメモリ単位としているためです。

では、基本データ型とデータ構造の間にはどのような関係があるのでしょうか。データ構造とは、コンピュータ内でデータを組織し格納する方法のことです。この言葉で主役なのは「データ」ではなく「構造」です。

「数字の並び」を表したいなら、自然に配列の使用を思い浮かべるでしょう。これは、配列の線形構造が数字どうしの隣接関係や順序関係を表せるからです。しかし、格納する内容が整数 int なのか、小数 float なのか、文字 char なのかは、「データ構造」とは関係ありません。

言い換えると、基本データ型はデータの「内容の型」を提供し、データ構造はデータの「組織方法」を提供します。たとえば次のコードでは、同じデータ構造(配列)を使って intfloatcharbool など異なる基本データ型を格納・表現しています。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# さまざまな基本データ型で配列を初期化する\nnumbers: list[int] = [0] * 5\ndecimals: list[float] = [0.0] * 5\n# Python の文字は実際には長さ 1 の文字列\ncharacters: list[str] = ['0'] * 5\nbools: list[bool] = [False] * 5\n# Python のリストはさまざまな基本データ型とオブジェクト参照を自由に格納できる\ndata = [0, 0.0, 'a', False, ListNode(0)]\n
// さまざまな基本データ型で配列を初期化する\nint numbers[5];\nfloat decimals[5];\nchar characters[5];\nbool bools[5];\n
// さまざまな基本データ型で配列を初期化する\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nboolean[] bools = new boolean[5];\n
// さまざまな基本データ型で配列を初期化する\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nbool[] bools = new bool[5];\n
// さまざまな基本データ型で配列を初期化する\nvar numbers = [5]int{}\nvar decimals = [5]float64{}\nvar characters = [5]byte{}\nvar bools = [5]bool{}\n
// さまざまな基本データ型で配列を初期化する\nlet numbers = Array(repeating: 0, count: 5)\nlet decimals = Array(repeating: 0.0, count: 5)\nlet characters: [Character] = Array(repeating: \"a\", count: 5)\nlet bools = Array(repeating: false, count: 5)\n
// JavaScript の配列はさまざまな基本データ型とオブジェクトを自由に格納できる\nconst array = [0, 0.0, 'a', false];\n
// さまざまな基本データ型で配列を初期化する\nconst numbers: number[] = [];\nconst characters: string[] = [];\nconst bools: boolean[] = [];\n
// さまざまな基本データ型で配列を初期化する\nList<int> numbers = List.filled(5, 0);\nList<double> decimals = List.filled(5, 0.0);\nList<String> characters = List.filled(5, 'a');\nList<bool> bools = List.filled(5, false);\n
// さまざまな基本データ型で配列を初期化する\nlet numbers: Vec<i32> = vec![0; 5];\nlet decimals: Vec<f32> = vec![0.0; 5];\nlet characters: Vec<char> = vec!['0'; 5];\nlet bools: Vec<bool> = vec![false; 5];\n
// さまざまな基本データ型で配列を初期化する\nint numbers[10];\nfloat decimals[10];\nchar characters[10];\nbool bools[10];\n
// さまざまな基本データ型で配列を初期化する\nval numbers = IntArray(5)\nval decinals = FloatArray(5)\nval characters = CharArray(5)\nval bools = BooleanArray(5)\n
# Ruby のリストはさまざまな基本データ型とオブジェクト参照を自由に格納できる\ndata = [0, 0.0, 'a', false, ListNode(0)]\n
実行の可視化

https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 3 章   データ構造","3.2   基本データ型"],"tags":[]},{"location":"chapter_data_structure/character_encoding/","level":1,"title":"3.4   文字エンコーディング *","text":"

コンピュータでは、すべてのデータは二進数の形で保存されており、文字 char も例外ではありません。文字を表すためには、「文字セット」を定義し、各文字と二進数の間の一対一の対応関係を定める必要があります。文字セットがあれば、コンピュータは対応表を参照して二進数から文字への変換を行えます。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#341-ascii","level":2,"title":"3.4.1   ASCII 文字セット","text":"

ASCII コードは最も早く登場した文字セットで、その正式名称は American Standard Code for Information Interchange(米国標準情報交換コード)です。これは 7 ビットの二進数(1 バイトの下位 7 ビット)で 1 文字を表し、最大で 128 種類の異なる文字を表現できます。下図のように、ASCII コードには英字の大文字と小文字、数字 0 ~ 9、いくつかの句読点、そしていくつかの制御文字(改行やタブなど)が含まれます。

図 3-6   ASCII コード

しかし、ASCII コードで表現できるのは英語だけです。コンピュータのグローバル化に伴い、より多くの言語を表せる EASCII 文字セットが生まれました。これは ASCII の 7 ビットを 8 ビットへ拡張したもので、256 種類の異なる文字を表現できます。

世界では、さまざまな地域に適した EASCII 文字セットが次々に登場しました。これらの文字セットでは、前半の 128 文字は ASCII コードで統一され、後半の 128 文字は各言語の要件に合わせて個別に定義されています。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#342-gbk","level":2,"title":"3.4.2   GBK 文字セット","text":"

その後、人々は**EASCII コードでも多くの言語に必要な文字数を満たせない**ことに気づきました。たとえば漢字は 10 万字近くあり、日常的に使うものだけでも数千字あります。中国国家標準総局は 1980 年に GB2312 文字セットを公開し、6763 字の漢字を収録して、漢字のコンピュータ処理の基本的な需要を満たしました。

しかし、GB2312 では一部の珍しい字や繁体字を扱えません。GBK 文字セットは GB2312 を基に拡張されたもので、合計 21886 字の漢字を収録しています。GBK のエンコーディング方式では、ASCII 文字は 1 バイト、漢字は 2 バイトで表されます。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode","level":2,"title":"3.4.3   Unicode 文字セット","text":"

コンピュータ技術が急速に発展するにつれて、文字セットと符号化規格は百花繚乱の状態となり、それに伴って多くの問題も生じました。一方では、これらの文字セットは通常、特定の言語の文字しか定義しておらず、多言語環境では正常に動作できませんでした。もう一方では、同じ言語にも複数の文字セット規格が存在し、2 台のコンピュータが異なる符号化規格を使っていると、情報伝達の際に文字化けが発生しました。

当時の研究者たちはこう考えました。十分に完全な文字セットを打ち出して、世界中のあらゆる言語と記号をそこに収録すれば、多言語環境や文字化けの問題を解決できるのではないか。この発想に後押しされて、大規模で包括的な文字セット Unicode が誕生しました。

Unicode の中国語名は「統一コード」であり、理論上は 100 万を超える文字を収容できます。Unicode は世界中の文字を 1 つの文字セットに統合することを目指し、さまざまな言語の文字を処理・表示できる汎用文字セットを提供することで、符号化規格の違いによる文字化けを減らそうとしています。

1991 年の公開以来、Unicode は新しい言語と文字を継続的に拡充してきました。2022 年 9 月時点で、Unicode にはすでに 149186 文字が含まれており、各種言語の文字、記号、さらには絵文字まで収録されています。巨大な Unicode 文字セットでは、よく使われる文字は 2 バイトを占め、一部の珍しい文字は 3 バイト、さらには 4 バイトを占めます。

Unicode は汎用文字セットであり、本質的には各文字に番号(「コードポイント」)を割り当てるものですが、それらのコードポイントをコンピュータ内でどのように保存するかまでは規定していません。ここで疑問が生じます。長さの異なる Unicode コードポイントが同じテキストに現れたとき、システムはどのように文字を解析するのでしょうか。たとえば長さ 2 バイトの符号が与えられたとき、それが 2 バイトの 1 文字なのか、1 バイトの 2 文字なのかをどう判定するのでしょうか。

この問題に対して、**すべての文字を固定長の符号として保存する**という直接的な解決策があります。下図のように、「Hello」の各文字は 1 バイト、「アルゴリズム」の各文字は 2 バイトを占めます。上位ビットを 0 で埋めることで、「Hello アルゴリズム」のすべての文字を 2 バイト長にエンコードできます。こうすれば、システムは 2 バイトごとに 1 文字を解析して、この語句の内容を復元できます。

図 3-7   Unicode エンコーディングの例

しかし ASCII コードはすでに、英語の符号化には 1 バイトで十分であることを示しています。上記の方式を採用すると、英語のテキストが占める空間は ASCII エンコーディング時の 2 倍になり、メモリ空間の浪費が大きくなります。そのため、より効率的な Unicode エンコーディング方式が必要です。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#344-utf-8","level":2,"title":"3.4.4   UTF-8 エンコーディング","text":"

現在、UTF-8 は国際的に最も広く使われている Unicode エンコーディング方式になっています。**これは可変長のエンコーディング**であり、1 文字を 1 〜 4 バイトで表し、文字の複雑さに応じて長さが変わります。ASCII 文字は 1 バイト、ラテン文字とギリシャ文字は 2 バイト、一般的な漢字は 3 バイト、そのほかの一部の珍しい文字は 4 バイト必要です。

UTF-8 の符号化規則はそれほど複雑ではなく、次の 2 つのケースに分けられます。

  • 長さ 1 バイトの文字では、最上位ビットを \\(0\\) にし、残りの 7 ビットを Unicode コードポイントに設定します。ここで注意すべきなのは、ASCII 文字が Unicode 文字セットの先頭 128 個のコードポイントを占めていることです。つまり、UTF-8 エンコーディングは ASCII コードと下位互換性があります。このため、UTF-8 を使って古い ASCII コードのテキストを解析できます。
  • 長さ \\(n\\) バイトの文字(ただし \\(n > 1\\))では、先頭バイトの上位 \\(n\\) ビットをすべて \\(1\\) にし、第 \\(n + 1\\) ビットを \\(0\\) に設定します。2 バイト目以降では、各バイトの上位 2 ビットをいずれも \\(10\\) にし、残りのすべてのビットで文字の Unicode コードポイントを埋めます。

下図は「Helloアルゴリズム」に対応する UTF-8 エンコーディングを示しています。観察すると、上位 \\(n\\) ビットがすべて \\(1\\) に設定されているため、システムは先頭から連続する \\(1\\) の個数を読むことで、その文字の長さが \\(n\\) であると解析できます。

では、なぜ残りのすべてのバイトの上位 2 ビットを \\(10\\) にするのでしょうか。実は、この \\(10\\) は検査用の印として機能します。システムが誤ったバイト位置からテキストを解析し始めたとしても、バイト先頭の \\(10\\) によって異常を素早く判定できます。

この \\(10\\) を検査用の印とする理由は、UTF-8 の符号化規則では上位 2 ビットが \\(10\\) になる文字は存在しないからです。この結論は背理法で証明できます。ある文字の上位 2 ビットが \\(10\\) だと仮定すると、その文字の長さは \\(1\\) であり、ASCII コードに対応することになります。しかし ASCII コードの最上位ビットは \\(0\\) であるはずなので、仮定と矛盾します。

図 3-8   UTF-8 エンコーディングの例

UTF-8 以外にも、一般的なエンコーディング方式として次の 2 つがあります。

  • UTF-16 エンコーディング:1 文字を 2 バイトまたは 4 バイトで表します。すべての ASCII 文字と一般的な非英語文字は 2 バイトで表し、一部の文字だけが 4 バイトを必要とします。2 バイトの文字については、UTF-16 エンコーディングは Unicode コードポイントと等しくなります。
  • UTF-32 エンコーディング:各文字を必ず 4 バイトで表します。つまり UTF-32 は UTF-8 や UTF-16 よりも多くの領域を消費し、とくに ASCII 文字の比率が高いテキストでその傾向が顕著です。

記憶領域の使用量という観点では、UTF-8 は英語文字の表現に非常に効率的で、必要なのは 1 バイトだけです。一方、UTF-16 は一部の非英語文字(たとえば中国語の文字)の符号化でより効率的になることがあり、必要なのは 2 バイトだけで、UTF-8 では 3 バイト必要になる場合があります。

互換性という観点では、UTF-8 の汎用性が最も高く、多くのツールやライブラリが UTF-8 を優先的にサポートしています。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#345","level":2,"title":"3.4.5   プログラミング言語の文字エンコーディング","text":"

従来の多くのプログラミング言語では、実行中の文字列に UTF-16 や UTF-32 のような固定長エンコーディングが使われています。固定長エンコーディングでは、文字列を配列のように扱えるため、次のような利点があります。

  • ランダムアクセス:UTF-16 で符号化された文字列はランダムアクセスが容易です。UTF-8 は可変長エンコーディングなので、第 \\(i\\) 文字を見つけるには文字列の先頭から第 \\(i\\) 文字まで走査する必要があり、\\(O(n)\\) の時間がかかります。
  • 文字数の計算:ランダムアクセスと同様に、UTF-16 で符号化された文字列の長さを計算するのも \\(O(1)\\) の操作です。しかし、UTF-8 で符号化された文字列の長さを計算するには、文字列全体を走査する必要があります。
  • 文字列操作:UTF-16 で符号化された文字列では、多くの文字列操作(分割、連結、挿入、削除など)をより簡単に行えます。UTF-8 で符号化された文字列では、これらの操作を行う際に、無効な UTF-8 エンコーディングを生じさせないための追加計算が通常必要になります。

実際、プログラミング言語における文字エンコーディング方式の設計は、とても興味深い話題であり、多くの要因が関わっています。

  • Java の String 型は UTF-16 エンコーディングを使用し、各文字は 2 バイトを占めます。これは Java 言語の設計当初、人々が 16 ビットあればあらゆる文字を表現するのに十分だと考えていたためです。しかし、これは誤った判断でした。その後 Unicode 規格は 16 ビットを超える範囲へ拡張されたため、現在の Java では 1 文字が 16 ビット値の組(「サロゲートペア」)で表されることがあります。
  • JavaScript と TypeScript の文字列が UTF-16 エンコーディングを使う理由も Java と似ています。1995 年に Netscape 社が初めて JavaScript 言語を公開した当時、Unicode はまだ発展初期にあり、16 ビットの符号化で十分すべての Unicode 文字を表せると考えられていました。
  • C# が UTF-16 エンコーディングを使う主な理由は、.NET プラットフォームが Microsoft によって設計され、Microsoft の多くの技術(Windows オペレーティングシステムを含む)で UTF-16 エンコーディングが広く使われているためです。

以上のプログラミング言語は文字数を過小評価していたため、16 ビットを超える長さの Unicode 文字を表すために「サロゲートペア」を採用せざるを得ませんでした。これはやむを得ない妥協策です。一方では、サロゲートペアを含む文字列では、1 文字が 2 バイトまたは 4 バイトを占める可能性があり、固定長エンコーディングの利点が失われます。もう一方では、サロゲートペアの処理には追加のコードが必要となり、プログラミングの複雑さとデバッグの難しさが増します。

こうした理由から、一部のプログラミング言語では別のエンコーディング方式が採用されました。

  • Python の str は Unicode エンコーディングを使用し、柔軟な文字列表現を採用しています。保存される文字の長さは、その文字列中で最大の Unicode コードポイントに依存します。文字列がすべて ASCII 文字であれば各文字は 1 バイト、ASCII の範囲を超える文字があってもすべてが基本多言語面(BMP)内であれば各文字は 2 バイト、BMP を超える文字があれば各文字は 4 バイトを占めます。
  • Go 言語の string 型は内部で UTF-8 エンコーディングを使用します。Go 言語には単一の Unicode コードポイントを表す rune 型も用意されています。
  • Rust 言語の strString 型は内部で UTF-8 エンコーディングを使用します。Rust にも単一の Unicode コードポイントを表す char 型があります。

注意すべきなのは、ここまでの議論はすべて、プログラミング言語内での文字列の保存方法についてであり、**文字列をファイルに保存したりネットワークで転送したりする方法とは別の問題である**ということです。ファイル保存やネットワーク転送では、通常、互換性と空間効率を最適化するために文字列を UTF-8 形式にエンコードします。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   データ構造の分類","text":"

代表的なデータ構造には、配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフがあり、これらは「論理構造」と「物理構造」の 2 つの観点から分類できます。

","path":["第 3 章   データ構造","3.1   データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311","level":2,"title":"3.1.1   論理構造:線形と非線形","text":"

論理構造はデータ要素間の論理的な関係を示します。配列と連結リストでは、データは一定の順序で並び、データ間の線形関係を表します。一方、木ではデータは上から下へ階層的に並び、「祖先」と「子孫」の派生関係を示します。グラフはノードと辺で構成され、複雑なネットワーク関係を反映します。

以下の図に示すように、論理構造は「線形」と「非線形」の 2 つに大別できます。線形構造は比較的直感的で、データが論理関係において線形に並ぶことを指します。非線形構造はその逆で、非線形に配置されます。

  • 線形データ構造:配列、連結リスト、スタック、キュー、ハッシュテーブルであり、要素間は 1 対 1 の順序関係です。
  • 非線形データ構造:木、ヒープ、グラフ、ハッシュテーブル。

非線形データ構造は、さらに木構造と網状構造に分けられます。

  • 木構造:木、ヒープ、ハッシュテーブルであり、要素間は 1 対多の関係です。
  • 網状構造:グラフであり、要素間は多対多の関係です。

図 3-1   線形データ構造と非線形データ構造

","path":["第 3 章   データ構造","3.1   データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312","level":2,"title":"3.1.2   物理構造:連続と分散","text":"

アルゴリズムのプログラムが実行されるとき、処理中のデータは主にメモリに格納されます。下図はコンピュータのメモリモジュールを示しており、各黒い四角はそれぞれ 1 つのメモリ空間を表しています。メモリは巨大な Excel の表のようなものだと考えることができ、各セルには一定量のデータを格納できます。

システムはメモリアドレスを通じて目的の位置にあるデータへアクセスします。下図に示すように、コンピュータは特定の規則に従って表内の各セルに番号を割り当て、各メモリ空間が一意のメモリアドレスを持つようにします。これらのアドレスがあれば、プログラムはメモリ内のデータにアクセスできます。

図 3-2   メモリモジュール、メモリ空間、メモリアドレス

Tip

補足すると、メモリを Excel の表にたとえるのは単純化した比喩であり、実際のメモリの動作機構はより複雑で、アドレス空間、メモリ管理、キャッシュ機構、仮想メモリ、物理メモリなどの概念が関わります。

メモリはすべてのプログラムで共有される資源であり、あるメモリ領域が 1 つのプログラムに占有されると、通常は他のプログラムが同時に利用できません。したがって、データ構造とアルゴリズムの設計では、メモリ資源は重要な考慮要素です。たとえば、アルゴリズムが使用するメモリ使用量のピークは、システムに残っている空きメモリを超えてはなりません。大きな連続メモリ領域が不足している場合、選択するデータ構造は分散したメモリ空間に格納できる必要があります。

下図に示すように、物理構造はデータがコンピュータメモリ内にどのように格納されるかを表します。これは連続空間への格納(配列)と分散空間への格納(連結リスト)に分けられます。物理構造は低レベルでデータのアクセス、更新、追加、削除などの操作方法を決定し、2 種類の物理構造は時間効率と空間効率の面で相補的な特徴を持ちます。

図 3-3   連続空間格納と分散空間格納

補足すると、すべてのデータ構造は配列、連結リスト、またはその両者の組み合わせに基づいて実装されます。たとえば、スタックとキューは配列でも連結リストでも実装できます。一方、ハッシュテーブルの実装には配列と連結リストの両方が含まれる場合があります。

  • 配列に基づいて実装可能:スタック、キュー、ハッシュテーブル、木、ヒープ、グラフ、行列、テンソル(次元 \\(\\geq 3\\) の配列)など。
  • 連結リストに基づいて実装可能:スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなど。

連結リストは初期化後も、プログラムの実行中に長さを調整できるため、「動的データ構造」とも呼ばれます。配列は初期化後に長さを変更できないため、「静的データ構造」とも呼ばれます。なお、配列もメモリを再割り当てすることで長さを変更でき、ある程度の「動的性」を持たせることができます。

Tip

物理構造の理解が難しいと感じる場合は、先に次の章を読んでから本節を振り返ることを勧めます。

","path":["第 3 章   データ構造","3.1   データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/number_encoding/","level":1,"title":"3.3   数値エンコーディング *","text":"

Tip

本書では、タイトルに * 記号が付いている章は選読です。時間が限られている場合や理解が難しいと感じる場合は、いったん読み飛ばし、必読章を終えてから個別に取り組んでください。

","path":["第 3 章   データ構造","3.3   数値エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#331-1-2","level":2,"title":"3.3.1   符号付き絶対値表現、1 の補数、2 の補数","text":"

前節の表を見ると、すべての整数型で表せる負数の個数は正数より 1 つ多く、たとえば byte の値域は \\([-128, 127]\\) です。この現象は直感に反するように見えますが、その背景には符号付き絶対値表現、1 の補数、2 の補数に関する知識があります。

まず押さえておくべきなのは、**数値はコンピュータ内で「2 の補数」の形で保存される**ということです。その理由を説明する前に、まずはこの 3 つの定義を示します。

  • 符号付き絶対値表現:数値の二進表現の最上位ビットを符号ビットとみなし、\\(0\\) は正数、\\(1\\) は負数を表し、残りのビットが数値の値を表します。
  • 1 の補数:正数の 1 の補数は符号付き絶対値表現と同じで、負数の 1 の補数は符号ビットを除くすべてのビットを反転したものです。
  • 2 の補数:正数の 2 の補数は符号付き絶対値表現と同じで、負数の 2 の補数は 1 の補数に \\(1\\) を加えたものです。

下図は、符号付き絶対値表現、1 の補数、2 の補数の変換方法を示しています。

図 3-4   符号付き絶対値表現、1 の補数、2 の補数の相互変換

符号付き絶対値表現(sign-magnitude)は最も直感的ですが、いくつかの制約があります。まず、負数の符号付き絶対値表現はそのまま演算に使えません。たとえば符号付き絶対値表現で \\(1 + (-2)\\) を計算すると、結果は \\(-3\\) になってしまい、これは明らかに誤りです。

\\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline & = 1000 \\; 0011 \\newline & \\rightarrow -3 \\end{aligned} \\]

この問題を解決するために、コンピュータには1 の補数(1's complement)が導入されました。まず符号付き絶対値表現を 1 の補数に変換し、1 の補数で \\(1 + (-2)\\) を計算してから、結果を 1 の補数から符号付き絶対値表現へ戻すと、正しい結果 \\(-1\\) が得られます。

\\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 \\; \\text{(符号付き絶対値表現)} + 1000 \\; 0010 \\; \\text{(符号付き絶対値表現)} \\newline & = 0000 \\; 0001 \\; \\text{(1 の補数)} + 1111 \\; 1101 \\; \\text{(1 の補数)} \\newline & = 1111 \\; 1110 \\; \\text{(1 の補数)} \\newline & = 1000 \\; 0001 \\; \\text{(符号付き絶対値表現)} \\newline & \\rightarrow -1 \\end{aligned} \\]

一方、数値 0 の符号付き絶対値表現には \\(+0\\) と \\(-0\\) の 2 つの表し方があります。つまり、数値 0 に対して異なる 2 つの二進コードが対応しており、これは曖昧さの原因になります。たとえば条件判定で正のゼロと負のゼロを区別しないと、誤った判定結果になる可能性があります。また、この曖昧さを解消しようとすると追加の判定処理が必要になり、計算効率が下がるおそれがあります。

\\[ \\begin{aligned} +0 & \\rightarrow 0000 \\; 0000 \\newline -0 & \\rightarrow 1000 \\; 0000 \\end{aligned} \\]

符号付き絶対値表現と同様に、1 の補数にも正負のゼロの曖昧さがあります。そこでコンピュータはさらに2 の補数(2's complement)を導入しました。まずは負のゼロについて、符号付き絶対値表現、1 の補数、2 の補数の変換を見てみましょう。

\\[ \\begin{aligned} -0 \\rightarrow \\; & 1000 \\; 0000 \\; \\text{(符号付き絶対値表現)} \\newline = \\; & 1111 \\; 1111 \\; \\text{(1 の補数)} \\newline = 1 \\; & 0000 \\; 0000 \\; \\text{(2 の補数)} \\newline \\end{aligned} \\]

負のゼロの 1 の補数に \\(1\\) を加えると桁上がりが発生しますが、byte 型の長さは 8 ビットしかないため、第 9 ビットへあふれた \\(1\\) は捨てられます。つまり、負のゼロの 2 の補数は \\(0000 \\; 0000\\) であり、正のゼロの 2 の補数と同じです。そのため、2 の補数表現ではゼロは 1 つしか存在せず、正負のゼロの曖昧さは解消されます。

最後にもう 1 つ疑問が残ります。byte 型の値域は \\([-128, 127]\\) ですが、余分にある負数 \\(-128\\) はどのように得られるのでしょうか。区間 \\([-127, +127]\\) にあるすべての整数には、それぞれ対応する符号付き絶対値表現、1 の補数、2 の補数があり、符号付き絶対値表現と 2 の補数の間は相互に変換できます。

しかし、2 の補数 \\(1000 \\; 0000\\) だけは例外で、対応する符号付き絶対値表現を持ちません。変換規則に従うと、この 2 の補数に対応する符号付き絶対値表現は \\(0000 \\; 0000\\) になります。これは明らかに矛盾しています。なぜなら、この符号付き絶対値表現は数値 \\(0\\) を表し、その 2 の補数は自分自身であるはずだからです。コンピュータでは、この特別な 2 の補数 \\(1000 \\; 0000\\) を \\(-128\\) と定めています。実際、2 の補数での \\((-1) + (-127)\\) の計算結果はちょうど \\(-128\\) になります。

\\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\; 1111 \\; \\text{(符号付き絶対値表現)} + 1000 \\; 0001 \\; \\text{(符号付き絶対値表現)} \\newline & = 1000 \\; 0000 \\; \\text{(1 の補数)} + 1111 \\; 1110 \\; \\text{(1 の補数)} \\newline & = 1000 \\; 0001 \\; \\text{(2 の補数)} + 1111 \\; 1111 \\; \\text{(2 の補数)} \\newline & = 1000 \\; 0000 \\; \\text{(2 の補数)} \\newline & \\rightarrow -128 \\end{aligned} \\]

すでにお気づきかもしれませんが、上の計算はすべて加算です。これは重要な事実を示しています。**コンピュータ内部のハードウェア回路は、主として加算を基準に設計されている**のです。なぜなら、加算はほかの演算(乗算、除算、減算など)に比べてハードウェアで実装しやすく、並列化もしやすく、演算速度も速いからです。

ただし、これはコンピュータが加算しかできないという意味ではありません。加算といくつかの基本的な論理演算を組み合わせることで、コンピュータはさまざまな数学演算を実現できます。たとえば減算 \\(a - b\\) は加算 \\(a + (-b)\\) に変換できますし、乗算や除算も繰り返しの加算または減算に変換できます。

これで、コンピュータが 2 の補数を使う理由をまとめられます。2 の補数表現に基づけば、コンピュータは同じ回路と操作で正数と負数の加算を扱うことができ、減算専用の特別なハードウェア回路を設計する必要がなく、正負のゼロの曖昧さも特別に処理しなくて済みます。これにより、ハードウェア設計は大幅に簡略化され、演算効率も向上します。

2 の補数の設計は非常に巧妙ですが、紙幅の都合上ここまでにします。興味のある読者は、さらに深く調べてみてください。

","path":["第 3 章   データ構造","3.3   数値エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332","level":2,"title":"3.3.2   浮動小数点数のエンコーディング","text":"

注意深い人なら気づくかもしれません。intfloat はどちらも長さが 4 バイトで同じなのに、なぜ float の値域は int よりはるかに広いのでしょうか。これはかなり直感に反します。というのも、float は小数を表す必要があるので、本来なら値域は狭くなるはずだからです。

実際には、これは浮動小数点数 float が異なる表現方法を採用しているためです。32 ビット長の二進数を次のように表します。

\\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

IEEE 754 標準によれば、32-bit 長の float は次の 3 つの部分から構成されます。

  • 符号部 \\(\\mathrm{S}\\) :1 ビットを占め、\\(b_{31}\\) に対応します。
  • 指数部 \\(\\mathrm{E}\\) :8 ビットを占め、\\(b_{30} b_{29} \\ldots b_{23}\\) に対応します。
  • 仮数部 \\(\\mathrm{N}\\) :23 ビットを占め、\\(b_{22} b_{21} \\ldots b_0\\) に対応します。

二進数 float に対応する値は次式で計算されます。

\\[ \\text {val} = (-1)^{b_{31}} \\times 2^{\\left(b_{30} b_{29} \\ldots b_{23}\\right)_2-127} \\times\\left(1 . b_{22} b_{21} \\ldots b_0\\right)_2 \\]

十進数に直すと、計算式は次のようになります。

\\[ \\text {val}=(-1)^{\\mathrm{S}} \\times 2^{\\mathrm{E} -127} \\times (1 + \\mathrm{N}) \\]

各項の取り得る範囲は次のとおりです。

\\[ \\begin{aligned} \\mathrm{S} \\in & \\{ 0, 1\\}, \\quad \\mathrm{E} \\in \\{ 1, 2, \\dots, 254 \\} \\newline (1 + \\mathrm{N}) = & (1 + \\sum_{i=1}^{23} b_{23-i} 2^{-i}) \\subset [1, 2 - 2^{-23}] \\end{aligned} \\]

図 3-5   IEEE 754 標準における float の計算例

上図を見ると、例として \\(\\mathrm{S} = 0\\) 、 \\(\\mathrm{E} = 124\\) 、\\(\\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\\) が与えられた場合、次のようになります。

\\[ \\text { val } = (-1)^0 \\times 2^{124 - 127} \\times (1 + 0.375) = 0.171875 \\]

これで最初の疑問に答えられます。**float の表現方法には指数部が含まれているため、その値域は int よりはるかに広い**のです。上の計算より、float が表せる最大の正数は \\(2^{254 - 127} \\times (2 - 2^{-23}) \\approx 3.4 \\times 10^{38}\\) であり、符号ビットを切り替えれば最小の負数が得られます。

浮動小数点数 float は値域を広げる一方で、その代償として精度を犠牲にしています。整数型 int は 32 ビットすべてを数値の表現に使うため、数値は一様に分布します。しかし指数部があるため、浮動小数点数 float は値が大きくなるほど、隣り合う 2 つの数の差も大きくなる傾向があります。

次の表のとおり、指数部 \\(\\mathrm{E} = 0\\) と \\(\\mathrm{E} = 255\\) には特別な意味があり、ゼロ、無限大、\\(\\mathrm{NaN}\\) などを表すために使われます。

表 3-2   指数部の意味

指数部 E 仮数部 \\(\\mathrm{N} = 0\\) 仮数部 \\(\\mathrm{N} \\ne 0\\) 計算式 \\(0\\) \\(\\pm 0\\) 非正規化数 \\((-1)^{\\mathrm{S}} \\times 2^{-126} \\times (0.\\mathrm{N})\\) \\(1, 2, \\dots, 254\\) 正規化数 正規化数 \\((-1)^{\\mathrm{S}} \\times 2^{(\\mathrm{E} -127)} \\times (1.\\mathrm{N})\\) \\(255\\) \\(\\pm \\infty\\) \\(\\mathrm{NaN}\\)

なお、非正規化数によって浮動小数点数の精度は大きく向上します。最小の正の正規化数は \\(2^{-126}\\) であり、最小の正の非正規化数は \\(2^{-126} \\times 2^{-23}\\) です。

倍精度 doublefloat と同様の表現方法を採用しているため、ここでは詳述しません。

","path":["第 3 章   データ構造","3.3   数値エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/summary/","level":1,"title":"3.5   まとめ","text":"","path":["第 3 章   データ構造","3.5   まとめ"],"tags":[]},{"location":"chapter_data_structure/summary/#1","level":3,"title":"1.   重要ポイントの振り返り","text":"
  • データ構造は、論理構造と物理構造という 2 つの観点から分類できます。論理構造はデータ要素間の論理的関係を記述し、物理構造はデータのコンピュータメモリ上での格納方法を記述します。
  • 代表的な論理構造には、線形、木構造、網状構造などがあります。通常、論理構造に基づいてデータ構造を線形(配列、連結リスト、スタック、キュー)と非線形(木、グラフ、ヒープ)の 2 種類に分類します。ハッシュテーブルの実装には、線形データ構造と非線形データ構造が同時に含まれる場合があります。
  • プログラムの実行時、データはコンピュータメモリに格納されます。各メモリ空間には対応するメモリアドレスがあり、プログラムはそれらのメモリアドレスを通じてデータにアクセスします。
  • 物理構造は主に連続領域への格納(配列)と分散領域への格納(連結リスト)に分けられます。すべてのデータ構造は、配列、連結リスト、またはその両方の組み合わせによって実装されます。
  • コンピュータにおける基本データ型には、整数 byteshortintlong、浮動小数点数 floatdouble、文字 char、真偽値 bool があります。これらの値域は、使用する記憶領域の大きさと表現方式によって決まります。
  • 符号付き絶対値表現、1 の補数、2 の補数は、コンピュータで数値を符号化する 3 つの方法であり、相互に変換できます。整数の符号付き絶対値表現では最上位ビットが符号ビットで、残りのビットが数値の値です。
  • 整数はコンピュータ内では 2 の補数の形式で格納されます。2 の補数表現では、コンピュータは正数と負数の加算を同じように扱うことができ、減算のために特別なハードウェア回路を別途設計する必要がなく、さらに正負のゼロが重複する問題もありません。
  • 浮動小数点数の符号化は、1 ビットの符号部、8 ビットの指数部、23 ビットの仮数部で構成されます。指数部があるため、浮動小数点数の値域は整数よりはるかに広くなりますが、その代償として精度が犠牲になります。
  • ASCII コードは最も早く登場した英字文字集合で、長さは 1 バイト、収録文字数は 127 です。GBK 文字集合はよく使われる中国語文字集合で、2 万字以上の漢字を収録しています。Unicode は完全な文字集合標準を提供することを目指しており、世界中のさまざまな言語の文字を収録することで、文字コード方式の不一致によって生じる文字化けの問題を解決します。
  • UTF-8 は最も広く使われている Unicode の符号化方式で、汎用性が非常に高いです。可変長の符号化方式であり、拡張性に優れ、記憶領域の利用効率を効果的に高めます。UTF-16 と UTF-32 は固定長の符号化方式です。中国語を符号化する場合、UTF-16 は UTF-8 よりも使用領域が小さくなります。Java や C# などのプログラミング言語は、デフォルトで UTF-16 を使用します。
","path":["第 3 章   データ構造","3.5   まとめ"],"tags":[]},{"location":"chapter_data_structure/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:なぜハッシュテーブルには線形データ構造と非線形データ構造が同時に含まれるのですか?

ハッシュテーブルの基盤は配列であり、ハッシュ衝突を解決するために「チェイン法」(後続の「ハッシュ衝突」の章で説明します)を使うことがあります。配列内の各バケットは 1 つの連結リストを指し、その連結リストの長さがある閾値を超えると、木(通常は赤黒木)に変換されることもあります。

格納の観点から見ると、ハッシュテーブルの基盤は配列であり、各バケットスロットには値が入ることもあれば、連結リストや木が入ることもあります。したがって、ハッシュテーブルには線形データ構造(配列、連結リスト)と非線形データ構造(木)が同時に含まれる場合があります。

Q:char 型の長さは 1 バイトですか?

char 型の長さは、プログラミング言語が採用する符号化方式によって決まります。たとえば、Java、JavaScript、TypeScript、C# はいずれも UTF-16 符号化(Unicode コードポイントを保持)を採用しているため、char 型の長さは 2 バイトです。

Q:配列ベースで実装されたデータ構造を「静的データ構造」と呼ぶのは曖昧ではありませんか? スタックも push や pop などの操作ができ、これらの操作はどれも「動的」です。

スタックは確かに動的なデータ操作を実現できますが、データ構造自体は依然として「静的」(長さが不変)です。配列ベースのデータ構造でも要素を動的に追加または削除できますが、その容量は固定です。データ量が事前に確保した大きさを超えた場合は、より大きな新しい配列を作成し、古い配列の内容を新しい配列にコピーする必要があります。

Q:スタック(キュー)を構築するときにサイズを指定していないのに、なぜそれらは「静的データ構造」なのですか?

高水準プログラミング言語では、スタック(キュー)の初期容量を人手で指定する必要はなく、この作業はクラス内部で自動的に行われます。たとえば、Java の ArrayList の初期容量は通常 10 です。また、容量拡張も自動的に実装されています。詳しくは後続の「リスト」の章を参照してください。

Q:符号付き絶対値表現から 2 の補数への変換方法は「先にビット反転してから 1 を加える」ですが、2 の補数から符号付き絶対値表現への変換は逆演算である「先に 1 を引いてからビット反転する」べきなのに、同じく「先にビット反転してから 1 を加える」でも求められます。これはなぜですか?

これは、符号付き絶対値表現と 2 の補数の相互変換が、実際には「補数」を計算する過程だからです。まず補数の定義を示します。\\(a + b = c\\) とすると、\\(a\\) を \\(b\\) から \\(c\\) への補数と呼び、逆に \\(b\\) も \\(a\\) から \\(c\\) への補数と呼びます。

長さ \\(n = 4\\) ビットの 2 進数 \\(0010\\) が与えられたとします。この数を符号付き絶対値表現(符号ビットは考慮しない)とみなすと、その 2 の補数は「先にビット反転してから 1 を加える」ことで得られます。

\\[ 0010 \\rightarrow 1101 \\rightarrow 1110 \\]

ここで、符号付き絶対値表現と 2 の補数の和は \\(0010 + 1110 = 10000\\) となります。つまり、2 の補数 \\(1110\\) は符号付き絶対値表現 \\(0010\\) から \\(10000\\) への「補数」です。これは、上記の「先にビット反転してから 1 を加える」が、実際には \\(10000\\) への補数を計算する過程であることを意味します。

では、2 の補数 \\(1110\\) から \\(10000\\) への「補数」はいくつでしょうか。これもやはり「先にビット反転してから 1 を加える」ことで求められます。

\\[ 1110 \\rightarrow 0001 \\rightarrow 0010 \\]

言い換えると、符号付き絶対値表現と 2 の補数は互いに相手から \\(10000\\) への「補数」なので、「符号付き絶対値表現から 2 の補数への変換」と「2 の補数から符号付き絶対値表現への変換」は同じ操作(先にビット反転してから 1 を加える)で実現できます。

もちろん、逆演算を用いて 2 の補数 \\(1110\\) の符号付き絶対値表現を求めることもでき、その場合は「先に 1 を引いてからビット反転する」ことになります。

\\[ 1110 \\rightarrow 1101 \\rightarrow 0010 \\]

まとめると、「先にビット反転してから 1 を加える」と「先に 1 を引いてからビット反転する」の 2 つの演算は、どちらも \\(10000\\) への補数を計算しており、等価です。

本質的には、「ビット反転」という操作は実際には \\(1111\\) への補数を求めています(常に 符号付き絶対値表現 + 1 の補数 = 1111 が成り立つため)。そして、1 の補数にさらに 1 を加えて得られる 2 の補数が、\\(10000\\) への補数です。

上記では \\(n = 4\\) を例にしましたが、この考え方は任意のビット長の 2 進数に一般化できます。

","path":["第 3 章   データ構造","3.5   まとめ"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"第 12 章   分割統治","text":"

Abstract

難題は段階的に分解され、そのたびにより単純になっていく。

分割統治は一つの重要な事実を示している。単純なことから始めれば、すべてはもはや複雑ではない。

","path":["第 12 章   分割統治"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"章の内容","text":"
  • 12.1   分割統治法
  • 12.2   分割統治探索戦略
  • 12.3   二分木の構築問題
  • 12.4   ハノイの塔の問題
  • 12.5   まとめ
","path":["第 12 章   分割統治"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/","level":1,"title":"12.2   分割統治探索戦略","text":"

私たちはすでに学んだように、探索アルゴリズムは大きく二つに分けられる。

  • 力ずく探索:データ構造を走査することで実現され、時間計算量は \\(O(n)\\) である。
  • 適応的探索:固有のデータ構造や事前情報を利用し、時間計算量は \\(O(\\log n)\\) 、さらには \\(O(1)\\) に達しうる。

実際、時間計算量が \\(O(\\log n)\\) の探索アルゴリズムは通常、分割統治戦略に基づいて実装される。たとえば二分探索や木構造である。

  • 二分探索の各ステップでは、問題(配列内で目標要素を探索すること)を小さな問題(配列の半分で目標要素を探索すること)に分解し、この過程は配列が空になるか目標要素が見つかるまで続く。
  • 木構造は分割統治の考え方を代表するものであり、二分探索木、AVL 木、ヒープなどのデータ構造では、さまざまな操作の時間計算量はいずれも \\(O(\\log n)\\) である。

二分探索の分割統治戦略は以下のとおりである。

  • 問題は分解できる:二分探索は、元の問題(配列内で探索すること)を部分問題(配列の半分で探索すること)へ再帰的に分解する。これは中央要素と目標要素を比較することで実現される。
  • 部分問題は独立している:二分探索では、各ラウンドで一つの部分問題だけを処理し、ほかの部分問題の影響を受けない。
  • 部分問題の解を統合する必要はない:二分探索は特定の要素を探すことを目的としているため、部分問題の解を統合する必要がない。部分問題が解決されると、元の問題も同時に解決される。

分割統治が探索効率を高められる本質的な理由は、力ずく探索では各ラウンドで一つの候補しか除外できないのに対し、**分割統治による探索では各ラウンドで候補の半分を除外できる**からである。

","path":["第 12 章   分割統治","12.2   分割統治探索戦略"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/#1","level":3,"title":"1.   分割統治に基づく二分探索","text":"

前の章では、二分探索を漸化式(反復)に基づいて実装した。ここでは分割統治(再帰)に基づいてこれを実装する。

Question

長さ \\(n\\) の昇順配列 nums が与えられ、そのすべての要素は一意である。要素 target を探索せよ。

分割統治の観点から、探索区間 \\([i, j]\\) に対応する部分問題を \\(f(i, j)\\) と記す。

元の問題 \\(f(0, n-1)\\) を出発点として、次の手順で二分探索を行う。

  1. 探索区間 \\([i, j]\\) の中点 \\(m\\) を計算し、それに基づいて探索区間の半分を除外する。
  2. 規模が半分に縮小された部分問題を再帰的に解く。候補は \\(f(i, m-1)\\) または \\(f(m+1, j)\\) である。
  3. 1.2. の手順を繰り返し、target が見つかるか区間が空になったら返す。

次の図は、配列内で要素 \\(6\\) を二分探索する分割統治の過程を示している。

図 12-4   二分探索の分割統治の過程

実装コードでは、再帰関数 dfs() を宣言して問題 \\(f(i, j)\\) を解く。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_recur.py
def dfs(nums: list[int], target: int, i: int, j: int) -> int:\n    \"\"\"二分探索:問題 f(i, j)\"\"\"\n    # 区間が空なら対象要素は存在しないので -1 を返す\n    if i > j:\n        return -1\n    # 中点インデックス m を計算\n    m = (i + j) // 2\n    if nums[m] < target:\n        # 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j)\n    elif nums[m] > target:\n        # 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1)\n    else:\n        # 目標要素が見つかったらそのインデックスを返す\n        return m\n\ndef binary_search(nums: list[int], target: int) -> int:\n    \"\"\"二分探索\"\"\"\n    n = len(nums)\n    # 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1)\n
binary_search_recur.cpp
/* 二分探索:問題 f(i, j) */\nint dfs(vector<int> &nums, int target, int i, int j) {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nint binarySearch(vector<int> &nums, int target) {\n    int n = nums.size();\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.java
/* 二分探索:問題 f(i, j) */\nint dfs(int[] nums, int target, int i, int j) {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nint binarySearch(int[] nums, int target) {\n    int n = nums.length;\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.cs
/* 二分探索:問題 f(i, j) */\nint DFS(int[] nums, int target, int i, int j) {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return DFS(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return DFS(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nint BinarySearch(int[] nums, int target) {\n    int n = nums.Length;\n    // 問題 f(0, n-1) を解く\n    return DFS(nums, target, 0, n - 1);\n}\n
binary_search_recur.go
/* 二分探索:問題 f(i, j) */\nfunc dfs(nums []int, target, i, j int) int {\n    // 区間が空なら対象要素は存在しないため、-1 を返す\n    if i > j {\n        return -1\n    }\n    // 中点インデックスを計算する\n    m := i + ((j - i) >> 1)\n    // 中点の要素と目標要素の大小を判定する\n    if nums[m] < target {\n        // 小さければ右半分の配列を再帰\n        // 部分問題 f(m+1, j) を解く\n        return dfs(nums, target, m+1, j)\n    } else if nums[m] > target {\n        // 大きければ左半分の配列を再帰\n        // 部分問題 f(i, m-1) を解く\n        return dfs(nums, target, i, m-1)\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m\n    }\n}\n\n/* 二分探索 */\nfunc binarySearch(nums []int, target int) int {\n    n := len(nums)\n    return dfs(nums, target, 0, n-1)\n}\n
binary_search_recur.swift
/* 二分探索:問題 f(i, j) */\nfunc dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if i > j {\n        return -1\n    }\n    // 中点インデックス m を計算\n    let m = (i + j) / 2\n    if nums[m] < target {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums: nums, target: target, i: m + 1, j: j)\n    } else if nums[m] > target {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums: nums, target: target, i: i, j: m - 1)\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m\n    }\n}\n\n/* 二分探索 */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // 問題 f(0, n-1) を解く\n    dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)\n}\n
binary_search_recur.js
/* 二分探索:問題 f(i, j) */\nfunction dfs(nums, target, i, j) {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nfunction binarySearch(nums, target) {\n    const n = nums.length;\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.ts
/* 二分探索:問題 f(i, j) */\nfunction dfs(nums: number[], target: number, i: number, j: number): number {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nfunction binarySearch(nums: number[], target: number): number {\n    const n = nums.length;\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.dart
/* 二分探索:問題 f(i, j) */\nint dfs(List<int> nums, int target, int i, int j) {\n  // 区間が空なら対象要素は存在しないので -1 を返す\n  if (i > j) {\n    return -1;\n  }\n  // 中点インデックス m を計算\n  int m = (i + j) ~/ 2;\n  if (nums[m] < target) {\n    // 部分問題 f(m+1, j) を再帰的に解く\n    return dfs(nums, target, m + 1, j);\n  } else if (nums[m] > target) {\n    // 部分問題 f(i, m-1) を再帰的に解く\n    return dfs(nums, target, i, m - 1);\n  } else {\n    // 目標要素が見つかったらそのインデックスを返す\n    return m;\n  }\n}\n\n/* 二分探索 */\nint binarySearch(List<int> nums, int target) {\n  int n = nums.length;\n  // 問題 f(0, n-1) を解く\n  return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.rs
/* 二分探索:問題 f(i, j) */\nfn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if i > j {\n        return -1;\n    }\n    let m: i32 = i + (j - i) / 2;\n    if nums[m as usize] < target {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if nums[m as usize] > target {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    let n = nums.len() as i32;\n    // 問題 f(0, n-1) を解く\n    dfs(nums, target, 0, n - 1)\n}\n
binary_search_recur.c
/* 二分探索:問題 f(i, j) */\nint dfs(int nums[], int target, int i, int j) {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nint binarySearch(int nums[], int target, int numsSize) {\n    int n = numsSize;\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.kt
/* 二分探索:問題 f(i, j) */\nfun dfs(\n    nums: IntArray,\n    target: Int,\n    i: Int,\n    j: Int\n): Int {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1\n    }\n    // 中点インデックス m を計算\n    val m = (i + j) / 2\n    return if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        dfs(nums, target, m + 1, j)\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        dfs(nums, target, i, m - 1)\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        m\n    }\n}\n\n/* 二分探索 */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    val n = nums.size\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1)\n}\n
binary_search_recur.rb
### 二分探索: 問題 f(i, j) ###\ndef dfs(nums, target, i, j)\n  # 区間が空なら対象要素は存在しないので -1 を返す\n  return -1 if i > j\n\n  # 中点インデックス m を計算\n  m = (i + j) / 2\n\n  if nums[m] < target\n    # 部分問題 f(m+1, j) を再帰的に解く\n    return dfs(nums, target, m + 1, j)\n  elsif nums[m] > target\n    # 部分問題 f(i, m-1) を再帰的に解く\n    return dfs(nums, target, i, m - 1)\n  else\n    # 目標要素が見つかったらそのインデックスを返す\n    return m\n  end\nend\n\n### 二分探索 ###\ndef binary_search(nums, target)\n  n = nums.length\n  # 問題 f(0, n-1) を解く\n  dfs(nums, target, 0, n - 1)\nend\n
コードの可視化

全画面で見る >

","path":["第 12 章   分割統治","12.2   分割統治探索戦略"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/","level":1,"title":"12.3   二分木の構築問題","text":"

Question

二分木の前順走査 preorder と中順走査 inorder が与えられたとき、これらから二分木を構築し、その根ノードを返してください。二分木には値が重複するノードが存在しないものとします(下図のとおり)。

図 12-5   二分木を構築する例のデータ

","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1","level":3,"title":"1.   分割統治問題かどうかを判断する","text":"

元の問題は preorderinorder から二分木を構築することであり、典型的な分割統治問題です。

  • 問題は分解できる:分割統治の観点から見ると、元の問題は 2 つの部分問題、すなわち左部分木の構築と右部分木の構築に分けられ、さらに根ノードを初期化する 1 ステップが加わります。各部分木(部分問題)に対しても、同じ分割方法を再利用してより小さな部分木(部分問題)へと分けていき、最小の部分問題(空部分木)に達した時点で終了します。
  • 部分問題は独立している:左部分木と右部分木は互いに独立しており、両者の間に重なりはありません。左部分木を構築するときは、中順走査と前順走査のうち左部分木に対応する部分だけを見れば十分です。右部分木も同様です。
  • 部分問題の解は統合できる:左部分木と右部分木(部分問題の解)が得られたら、それらを根ノードに接続することで元の問題の解を得られます。
","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   部分木をどのように分割するか","text":"

以上の分析より、この問題は分割統治で解けます。では、前順走査 preorder と中順走査 inorder を使って左部分木と右部分木をどのように分割すればよいのでしょうか?

定義に従うと、preorderinorder はいずれも 3 つの部分に分けられます。

  • 前順走査:[ 根ノード | 左部分木 | 右部分木 ] ,例えば上図の木は [ 3 | 9 | 2 1 7 ] に対応します。
  • 中順走査:[ 左部分木 | 根ノード | 右部分木 ] ,例えば上図の木は [ 9 | 3 | 1 2 7 ] に対応します。

上図のデータを例にすると、下図の手順によって分割結果を得られます。

  1. 前順走査の先頭要素 3 が根ノードの値です。
  2. 根ノード 3 の inorder におけるインデックスを探すと、そのインデックスを用いて inorder[ 9 | 3 | 1 2 7 ] に分割できます。
  3. inorder の分割結果から、左部分木と右部分木のノード数はそれぞれ 1 と 3 であることがわかり、したがって preorder[ 3 | 9 | 2 1 7 ] に分割できます。

図 12-6   前順走査と中順走査で部分木を分割する

","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#3","level":3,"title":"3.   変数を用いて部分木区間を記述する","text":"

以上の分割方法により、**根ノード、左部分木、右部分木が preorderinorder の中で占めるインデックス区間**が得られました。これらのインデックス区間を表すために、いくつかのポインタ変数を導入します。

  • 現在の木の根ノードが preorder に現れるインデックスを \\(i\\) とします。
  • 現在の木の根ノードが inorder に現れるインデックスを \\(m\\) とします。
  • 現在の木が inorder において占めるインデックス区間を \\([l, r]\\) とします。

次の表のように、これらの変数を用いれば根ノードの preorder におけるインデックスと、部分木の inorder におけるインデックス区間を表せます。

表 12-1   根ノードと部分木の前順走査・中順走査におけるインデックス

根ノードの preorder におけるインデックス 部分木の inorder におけるインデックス区間 現在の木 \\(i\\) \\([l, r]\\) 左部分木 \\(i + 1\\) \\([l, m-1]\\) 右部分木 \\(i + 1 + (m - l)\\) \\([m+1, r]\\)

右部分木の根ノードのインデックスにある \\((m-l)\\) は「左部分木のノード数」を意味します。下図と合わせて理解することを勧めます。

図 12-7   根ノードと左右部分木のインデックス区間の表し方

","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#4","level":3,"title":"4.   コードの実装","text":"

\\(m\\) の検索効率を高めるために、ハッシュテーブル hmap を用いて配列 inorder の要素からインデックスへの対応を保存します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby build_tree.py
def dfs(\n    preorder: list[int],\n    inorder_map: dict[int, int],\n    i: int,\n    l: int,\n    r: int,\n) -> TreeNode | None:\n    \"\"\"二分木を構築:分割統治\"\"\"\n    # 部分木区間が空なら終了する\n    if r - l < 0:\n        return None\n    # ルートノードを初期化する\n    root = TreeNode(preorder[i])\n    # m を求めて左右部分木を分割する\n    m = inorder_map[preorder[i]]\n    # 部分問題:左部分木を構築する\n    root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n    # 部分問題:右部分木を構築する\n    root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n    # 根ノードを返す\n    return root\n\ndef build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:\n    \"\"\"二分木を構築\"\"\"\n    # inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    inorder_map = {val: i for i, val in enumerate(inorder)}\n    root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)\n    return root\n
build_tree.cpp
/* 二分木を構築:分割統治 */\nTreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {\n    // 部分木区間が空なら終了する\n    if (r - l < 0)\n        return NULL;\n    // ルートノードを初期化する\n    TreeNode *root = new TreeNode(preorder[i]);\n    // m を求めて左右部分木を分割する\n    int m = inorderMap[preorder[i]];\n    // 部分問題:左部分木を構築する\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nTreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    unordered_map<int, int> inorderMap;\n    for (int i = 0; i < inorder.size(); i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);\n    return root;\n}\n
build_tree.java
/* 二分木を構築:分割統治 */\nTreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {\n    // 部分木区間が空なら終了する\n    if (r - l < 0)\n        return null;\n    // ルートノードを初期化する\n    TreeNode root = new TreeNode(preorder[i]);\n    // m を求めて左右部分木を分割する\n    int m = inorderMap.get(preorder[i]);\n    // 部分問題:左部分木を構築する\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nTreeNode buildTree(int[] preorder, int[] inorder) {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    Map<Integer, Integer> inorderMap = new HashMap<>();\n    for (int i = 0; i < inorder.length; i++) {\n        inorderMap.put(inorder[i], i);\n    }\n    TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.cs
/* 二分木を構築:分割統治 */\nTreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {\n    // 部分木区間が空なら終了する\n    if (r - l < 0)\n        return null;\n    // ルートノードを初期化する\n    TreeNode root = new(preorder[i]);\n    // m を求めて左右部分木を分割する\n    int m = inorderMap[preorder[i]];\n    // 部分問題:左部分木を構築する\n    root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nTreeNode? BuildTree(int[] preorder, int[] inorder) {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    Dictionary<int, int> inorderMap = [];\n    for (int i = 0; i < inorder.Length; i++) {\n        inorderMap.TryAdd(inorder[i], i);\n    }\n    TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);\n    return root;\n}\n
build_tree.go
/* 二分木を構築:分割統治 */\nfunc dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {\n    // 部分木区間が空なら終了する\n    if r-l < 0 {\n        return nil\n    }\n    // ルートノードを初期化する\n    root := NewTreeNode(preorder[i])\n    // m を求めて左右部分木を分割する\n    m := inorderMap[preorder[i]]\n    // 部分問題:左部分木を構築する\n    root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)\n    // 部分問題:右部分木を構築する\n    root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)\n    // 根ノードを返す\n    return root\n}\n\n/* 二分木を構築 */\nfunc buildTree(preorder, inorder []int) *TreeNode {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    inorderMap := make(map[int]int, len(inorder))\n    for i := 0; i < len(inorder); i++ {\n        inorderMap[inorder[i]] = i\n    }\n\n    root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)\n    return root\n}\n
build_tree.swift
/* 二分木を構築:分割統治 */\nfunc dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {\n    // 部分木区間が空なら終了する\n    if r - l < 0 {\n        return nil\n    }\n    // ルートノードを初期化する\n    let root = TreeNode(x: preorder[i])\n    // m を求めて左右部分木を分割する\n    let m = inorderMap[preorder[i]]!\n    // 部分問題:左部分木を構築する\n    root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)\n    // 部分問題:右部分木を構築する\n    root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)\n    // 根ノードを返す\n    return root\n}\n\n/* 二分木を構築 */\nfunc buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }\n    return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)\n}\n
build_tree.js
/* 二分木を構築:分割統治 */\nfunction dfs(preorder, inorderMap, i, l, r) {\n    // 部分木区間が空なら終了する\n    if (r - l < 0) return null;\n    // ルートノードを初期化する\n    const root = new TreeNode(preorder[i]);\n    // m を求めて左右部分木を分割する\n    const m = inorderMap.get(preorder[i]);\n    // 部分問題:左部分木を構築する\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nfunction buildTree(preorder, inorder) {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    let inorderMap = new Map();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.ts
/* 二分木を構築:分割統治 */\nfunction dfs(\n    preorder: number[],\n    inorderMap: Map<number, number>,\n    i: number,\n    l: number,\n    r: number\n): TreeNode | null {\n    // 部分木区間が空なら終了する\n    if (r - l < 0) return null;\n    // ルートノードを初期化する\n    const root: TreeNode = new TreeNode(preorder[i]);\n    // m を求めて左右部分木を分割する\n    const m = inorderMap.get(preorder[i]);\n    // 部分問題:左部分木を構築する\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nfunction buildTree(preorder: number[], inorder: number[]): TreeNode | null {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    let inorderMap = new Map<number, number>();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.dart
/* 二分木を構築:分割統治 */\nTreeNode? dfs(\n  List<int> preorder,\n  Map<int, int> inorderMap,\n  int i,\n  int l,\n  int r,\n) {\n  // 部分木区間が空なら終了する\n  if (r - l < 0) {\n    return null;\n  }\n  // ルートノードを初期化する\n  TreeNode? root = TreeNode(preorder[i]);\n  // m を求めて左右部分木を分割する\n  int m = inorderMap[preorder[i]]!;\n  // 部分問題:左部分木を構築する\n  root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n  // 部分問題:右部分木を構築する\n  root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n  // 根ノードを返す\n  return root;\n}\n\n/* 二分木を構築 */\nTreeNode? buildTree(List<int> preorder, List<int> inorder) {\n  // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n  Map<int, int> inorderMap = {};\n  for (int i = 0; i < inorder.length; i++) {\n    inorderMap[inorder[i]] = i;\n  }\n  TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n  return root;\n}\n
build_tree.rs
/* 二分木を構築:分割統治 */\nfn dfs(\n    preorder: &[i32],\n    inorder_map: &HashMap<i32, i32>,\n    i: i32,\n    l: i32,\n    r: i32,\n) -> Option<Rc<RefCell<TreeNode>>> {\n    // 部分木区間が空なら終了する\n    if r - l < 0 {\n        return None;\n    }\n    // ルートノードを初期化する\n    let root = TreeNode::new(preorder[i as usize]);\n    // m を求めて左右部分木を分割する\n    let m = inorder_map.get(&preorder[i as usize]).unwrap();\n    // 部分問題:左部分木を構築する\n    root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    Some(root)\n}\n\n/* 二分木を構築 */\nfn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    let mut inorder_map: HashMap<i32, i32> = HashMap::new();\n    for i in 0..inorder.len() {\n        inorder_map.insert(inorder[i], i as i32);\n    }\n    let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);\n    root\n}\n
build_tree.c
/* 二分木を構築:分割統治 */\nTreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {\n    // 部分木区間が空なら終了する\n    if (r - l < 0)\n        return NULL;\n    // ルートノードを初期化する\n    TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));\n    root->val = preorder[i];\n    root->left = NULL;\n    root->right = NULL;\n    // m を求めて左右部分木を分割する\n    int m = inorderMap[preorder[i]];\n    // 部分問題:左部分木を構築する\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);\n    // 部分問題:右部分木を構築する\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nTreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);\n    for (int i = 0; i < inorderSize; i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);\n    free(inorderMap);\n    return root;\n}\n
build_tree.kt
/* 二分木を構築:分割統治 */\nfun dfs(\n    preorder: IntArray,\n    inorderMap: Map<Int?, Int?>,\n    i: Int,\n    l: Int,\n    r: Int\n): TreeNode? {\n    // 部分木区間が空なら終了する\n    if (r - l < 0) return null\n    // ルートノードを初期化する\n    val root = TreeNode(preorder[i])\n    // m を求めて左右部分木を分割する\n    val m = inorderMap[preorder[i]]!!\n    // 部分問題:左部分木を構築する\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)\n    // 部分問題:右部分木を構築する\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)\n    // 根ノードを返す\n    return root\n}\n\n/* 二分木を構築 */\nfun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    val inorderMap = HashMap<Int?, Int?>()\n    for (i in inorder.indices) {\n        inorderMap[inorder[i]] = i\n    }\n    val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)\n    return root\n}\n
build_tree.rb
### 二分木を構築:分割統治 ###\ndef dfs(preorder, inorder_map, i, l, r)\n  # 部分木区間が空なら終了する\n  return if r - l < 0\n\n  # ルートノードを初期化する\n  root = TreeNode.new(preorder[i])\n  # m を求めて左右部分木を分割する\n  m = inorder_map[preorder[i]]\n  # 部分問題:左部分木を構築する\n  root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n  # 部分問題:右部分木を構築する\n  root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n\n  # 根ノードを返す\n  root\nend\n\n### 二分木を構築 ###\ndef build_tree(preorder, inorder)\n  # inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n  inorder_map = {}\n  inorder.each_with_index { |val, i| inorder_map[val] = i }\n  dfs(preorder, inorder_map, 0, 0, inorder.length - 1)\nend\n
コードの可視化

全画面で見る >

下図は二分木を構築する再帰過程を示しています。各ノードは下向きに「再帰していく」過程で生成され、各辺(参照)は上向きに「戻る」過程で張られます。

<1><2><3><4><5><6><7><8><9>

図 12-8   二分木を構築する再帰過程

各再帰関数における前順走査 preorder と中順走査 inorder の分割結果を下図に示します。

図 12-9   各再帰関数での分割結果

木のノード数を \\(n\\) とすると、各ノードの初期化(再帰関数 dfs() の 1 回の実行)には \\(O(1)\\) 時間かかります。したがって、全体の時間計算量は \\(O(n)\\) です。

ハッシュテーブルには inorder の要素からインデックスへの対応を保存するため、空間計算量は \\(O(n)\\) です。最悪の場合、すなわち二分木が連結リストに退化すると、再帰の深さは \\(n\\) に達し、\\(O(n)\\) のスタックフレーム空間を使用します。したがって、全体の空間計算量は \\(O(n)\\) です。

","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/","level":1,"title":"12.1   分割統治法","text":"

分割統治法(divide and conquer)は、問題を分けて統べるという意味であり、非常に重要で一般的なアルゴリズム戦略です。分割統治法は通常、再帰に基づいて実装され、「分」と「治」の 2 つのステップから構成されます。

  1. 分(分割段階):元の問題を 2 つ以上の部分問題へ再帰的に分解し、最小の部分問題に到達した時点で停止します。
  2. 治(統合段階):解が既知である最小の部分問題から始めて、部分問題の解を下から上へ統合し、元の問題の解を構築します。

以下の図に示すように、「マージソート」は分割統治戦略の典型的な応用の一つです。

  1. 分:元の配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割し、部分配列に要素が 1 つだけ残るまで続けます。
  2. 治:整列済みの部分配列(部分問題の解)を下から上へ統合し、整列済みの元の配列(元の問題の解)を得ます。

図 12-1   マージソートの分割統治戦略

","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1211","level":2,"title":"12.1.1   分割統治法の問題を見極めるには","text":"

ある問題が分割統治法で解くのに適しているかどうかは、通常、次の判断基準を参考にできます。

  1. 問題は分解できる:元の問題は、より小さく類似した部分問題に分解でき、同じ方法で再帰的に分割できます。
  2. 部分問題は独立している:部分問題同士に重複がなく、相互依存もないため、独立して解決できます。
  3. 部分問題の解は統合できる:元の問題の解は、部分問題の解を統合することで得られます。

明らかに、マージソートは以上の 3 つの判断基準を満たしています。

  1. 問題は分解できる:配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割します。
  2. 部分問題は独立している:各部分配列は独立にソートできます(部分問題は独立に解けます)。
  3. 部分問題の解は統合できる:2 つの整列済み部分配列(部分問題の解)は、1 つの整列済み配列(元の問題の解)に統合できます。
","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212","level":2,"title":"12.1.2   分割統治法で効率を高める","text":"

分割統治法はアルゴリズムの問題を効果的に解けるだけでなく、多くの場合アルゴリズムの効率も高められます。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソートが選択ソート、バブルソート、挿入ソートより高速ですが、これは分割統治戦略を適用しているためです。

ここで次の疑問が生じます。なぜ分割統治法はアルゴリズム効率を高められるのでしょうか。その根本的な仕組みは何でしょうか?言い換えると、大きな問題を複数の部分問題に分解し、部分問題を解き、それらの解を統合して元の問題の解にするという手順は、なぜ元の問題を直接解くより効率的なのでしょうか。この問題は、操作回数と並列計算の 2 つの観点から議論できます。

","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1","level":3,"title":"1.   操作回数の最適化","text":"

「バブルソート」を例に取ると、長さ \\(n\\) の配列を処理するのに \\(O(n^2)\\) の時間がかかります。以下の図のように、配列を中央で 2 つの部分配列に分けると仮定すると、分割には \\(O(n)\\) の時間、各部分配列のソートには \\(O((n / 2)^2)\\) の時間、2 つの部分配列の統合には \\(O(n)\\) の時間が必要で、全体の時間計算量は次のようになります:

\\[ O(n + (\\frac{n}{2})^2 \\times 2 + n) = O(\\frac{n^2}{2} + 2n) \\]

図 12-2   配列分割前後のバブルソート

次に、以下の不等式を計算します。左辺と右辺はそれぞれ、分割前と分割後の操作総数です:

\\[ \\begin{aligned} n^2 & > \\frac{n^2}{2} + 2n \\newline n^2 - \\frac{n^2}{2} - 2n & > 0 \\newline n(n - 4) & > 0 \\end{aligned} \\]

これは、\\(n > 4\\) のときに分割後の操作回数の方が少なくなり、ソート効率が高くなることを意味します。ただし、分割後の時間計算量は依然として 2 次の \\(O(n^2)\\) であり、計算量の定数項が小さくなっただけです。

さらに考えると、部分配列を中央からさらに 2 つの部分配列へと分割し続け、部分配列に要素が 1 つだけ残るまで分割を止めないとしたらどうでしょうか。この考え方がまさに「マージソート」であり、時間計算量は \\(O(n \\log n)\\) です。

さらに、分割点をいくつか増やして、元の配列を平均的に \\(k\\) 個の部分配列に分けるとしたらどうでしょうか。この状況は「バケットソート」と非常によく似ており、大量データのソートに非常に適しています。理論上の時間計算量は \\(O(n + k)\\) に達します。

","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#2","level":3,"title":"2.   並列計算の最適化","text":"

分割統治法で生成される部分問題は互いに独立しているため、通常は並列に解くことができます。つまり、分割統治法はアルゴリズムの時間計算量を下げられるだけでなく、オペレーティングシステムの並列最適化にも有利です。

並列最適化は、マルチコアまたはマルチプロセッサ環境で特に有効です。システムが複数の部分問題を同時に処理でき、計算資源をより十分に活用できるため、全体の実行時間を大幅に短縮できます。

たとえば、以下の図に示す「バケットソート」では、大量のデータを各バケットに均等に割り当てることで、すべてのバケットのソート処理を各計算ユニットに分散し、完了後に結果を統合できます。

図 12-3   バケットソートの並列計算

","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1213","level":2,"title":"12.1.3   分割統治法の代表的な応用","text":"

一方では、分割統治法は多くの古典的なアルゴリズム問題を解くのに使えます。

  • 最近点対探索:このアルゴリズムは、まず点集合を 2 つに分け、それぞれの部分における最近点対を求め、最後に 2 つの部分をまたぐ最近点対を求めます。
  • 大整数乗算:たとえば Karatsuba 法では、大整数の乗算を、より小さな整数どうしのいくつかの乗算と加算に分解します。
  • 行列乗算:たとえば Strassen 法では、大きな行列の乗算を、複数の小さな行列の乗算と加算に分解します。
  • ハノイの塔問題:ハノイの塔問題は再帰によって解くことができ、これは典型的な分割統治戦略の応用です。
  • 反転対の計算:ある数列で前の数が後ろの数より大きい場合、その 2 つの数は反転対を構成します。反転対の問題は、分割統治の考え方を利用し、マージソートを用いて解けます。

他方で、分割統治法はアルゴリズムとデータ構造の設計にも非常に広く応用されています。

  • 二分探索:二分探索では、整列済み配列を中央のインデックスで 2 つに分け、目標値と中央要素の比較結果に基づいてどちらの半区間を除外するかを決め、残った区間で同じ二分操作を行います。
  • マージソート:本節の冒頭で紹介したため、ここでは繰り返しません。
  • クイックソート:クイックソートは基準値を 1 つ選び、配列を、基準値より小さい要素の部分配列と、基準値より大きい要素の部分配列に分け、その後それぞれに対して同じ分割操作を行い、部分配列に要素が 1 つだけ残るまで続けます。
  • バケットソート:バケットソートの基本的な考え方は、データを複数のバケットに分散し、各バケット内の要素をソートしたうえで、各バケットの要素を順に取り出して整列済み配列を得ることです。
  • 木構造:たとえば二分探索木、AVL 木、赤黒木、B 木、B+ 木などでは、探索・挿入・削除などの操作をいずれも分割統治戦略の応用とみなせます。
  • ヒープ:ヒープは特殊な完全二分木であり、挿入、削除、ヒープ化などの各種操作には、実際には分割統治の考え方が含まれています。
  • ハッシュテーブル:ハッシュテーブル自体は分割統治を直接適用しているわけではありませんが、いくつかのハッシュ衝突解決法では間接的に分割統治戦略が使われています。たとえば、連鎖アドレス法における長い連結リストは、検索効率を高めるために赤黒木へ変換されます。

このように、**分割統治法は「静かに物を潤す」ようなアルゴリズム思想**であり、さまざまなアルゴリズムやデータ構造の中に潜んでいます。

","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   ハノイの塔の問題","text":"

マージソートや二分木の構築では、いずれも元の問題を元問題の半分の規模をもつ 2 つの部分問題に分解していました。しかし、ハノイの塔の問題では、異なる分解戦略を採用します。

Question

3 本の柱があり、それぞれを ABC とします。初期状態では、柱 A に \\(n\\) 枚の円盤が通されており、上から下へ小さい順に並んでいます。私たちの課題は、この \\(n\\) 枚の円盤を柱 C に移し、元の順序を保つことです(以下の図のとおり)。円盤を移動する際には、次のルールに従う必要があります。

  1. 円盤は 1 本の柱の頂上から取り出し、別の柱の頂上に置くことしかできません。
  2. 1 回に移動できる円盤は 1 枚だけです。
  3. 小さい円盤は常に大きい円盤の上になければなりません。

図 12-10   ハノイの塔の問題の例

規模が \\(i\\) のハノイの塔の問題を \\(f(i)\\) と表します 。たとえば \\(f(3)\\) は、\\(3\\) 枚の円盤を A から C へ移動するハノイの塔の問題を表します。

","path":["第 12 章   分割統治","12.4   ハノイの塔の問題"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#1","level":3,"title":"1.   基本ケースを考える","text":"

以下の図に示すように、問題 \\(f(1)\\) 、すなわち円盤が 1 枚だけの場合は、それを A から C へ直接移動すれば済みます。

<1><2>

図 12-11   規模 1 の問題の解

以下の図に示すように、問題 \\(f(2)\\) 、すなわち円盤が 2 枚ある場合は、小さい円盤が常に大きい円盤の上にある条件を満たすため、B を借りて移動を行う必要があります。

  1. まず上の小さい円盤を A から B へ移します。
  2. 次に大きい円盤を A から C へ移します。
  3. 最後に小さい円盤を B から C へ移します。
<1><2><3><4>

図 12-12   規模 2 の問題の解

問題 \\(f(2)\\) を解く過程は、**2 枚の円盤を B を介して A から C へ移す**と要約できます。このとき、C を目標の柱、B を補助の柱と呼びます。

","path":["第 12 章   分割統治","12.4   ハノイの塔の問題"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2","level":3,"title":"2.   部分問題への分解","text":"

問題 \\(f(3)\\) 、すなわち円盤が 3 枚ある場合になると、状況はやや複雑になります。

\\(f(1)\\) と \\(f(2)\\) の解が既知なので、分割統治の観点から、A の上部にある 2 枚の円盤をひとまとまりとみなして、次の図の手順を実行できます。こうして 3 枚の円盤を A から C へ順調に移動できます。

  1. B を目標の柱、C を補助の柱として、2 枚の円盤を A から B へ移します。
  2. A に残った 1 枚の円盤を A から C へ直接移動します。
  3. C を目標の柱、A を補助の柱として、2 枚の円盤を B から C へ移します。
<1><2><3><4>

図 12-13   規模 3 の問題の解

本質的には、問題 \\(f(3)\\) を 2 つの部分問題 \\(f(2)\\) と 1 つの部分問題 \\(f(1)\\) に分けています 。この 3 つの部分問題を順に解けば、元の問題も解決されます。これは、部分問題が独立しており、解を組み合わせられることを示しています。

ここまでで、次の図に示すハノイの塔の問題を解く分割統治戦略をまとめられます。元の問題 \\(f(n)\\) を 2 つの部分問題 \\(f(n-1)\\) と 1 つの部分問題 \\(f(1)\\) に分け、次の順序でこの 3 つの部分問題を解きます。

  1. \\(n-1\\) 枚の円盤を C を介して A から B へ移します。
  2. 残り \\(1\\) 枚の円盤を A から C へ直接移します。
  3. \\(n-1\\) 枚の円盤を A を介して B から C へ移します。

この 2 つの部分問題 \\(f(n-1)\\) は、同じ方法で再帰的に分割できます。最小の部分問題 \\(f(1)\\) に到達するまでこれを続けます。一方、\\(f(1)\\) の解は既知であり、1 回の移動操作だけで済みます。

図 12-14   ハノイの塔の問題を解く分割統治戦略

","path":["第 12 章   分割統治","12.4   ハノイの塔の問題"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#3","level":3,"title":"3.   コードの実装","text":"

コードでは、再帰関数 dfs(i, src, buf, tar) を定義します。その役割は、柱 src の上部にある \\(i\\) 枚の円盤を、補助の柱 buf を使って目標の柱 tar へ移動することです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py
def move(src: list[int], tar: list[int]):\n    \"\"\"円盤を 1 枚移動\"\"\"\n    # src の上から円盤を1枚取り出す\n    pan = src.pop()\n    # 円盤を tar の上に置く\n    tar.append(pan)\n\ndef dfs(i: int, src: list[int], buf: list[int], tar: list[int]):\n    \"\"\"ハノイの塔の問題 f(i) を解く\"\"\"\n    # src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1:\n        move(src, tar)\n        return\n    # 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf)\n    # 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    # 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar)\n\ndef solve_hanota(A: list[int], B: list[int], C: list[int]):\n    \"\"\"ハノイの塔を解く\"\"\"\n    n = len(A)\n    # A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C)\n
hanota.cpp
/* 円盤を 1 枚移動 */\nvoid move(vector<int> &src, vector<int> &tar) {\n    // src の上から円盤を1枚取り出す\n    int pan = src.back();\n    src.pop_back();\n    // 円盤を tar の上に置く\n    tar.push_back(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nvoid dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nvoid solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {\n    int n = A.size();\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
hanota.java
/* 円盤を 1 枚移動 */\nvoid move(List<Integer> src, List<Integer> tar) {\n    // src の上から円盤を1枚取り出す\n    Integer pan = src.remove(src.size() - 1);\n    // 円盤を tar の上に置く\n    tar.add(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nvoid dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nvoid solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {\n    int n = A.size();\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
hanota.cs
/* 円盤を 1 枚移動 */\nvoid Move(List<int> src, List<int> tar) {\n    // src の上から円盤を1枚取り出す\n    int pan = src[^1];\n    src.RemoveAt(src.Count - 1);\n    // 円盤を tar の上に置く\n    tar.Add(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nvoid DFS(int i, List<int> src, List<int> buf, List<int> tar) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        Move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    DFS(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    Move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    DFS(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nvoid SolveHanota(List<int> A, List<int> B, List<int> C) {\n    int n = A.Count;\n    // A の上から n 枚の円盤を B を介して C へ移す\n    DFS(n, A, B, C);\n}\n
hanota.go
/* 円盤を 1 枚移動 */\nfunc move(src, tar *list.List) {\n    // src の上から円盤を1枚取り出す\n    pan := src.Back()\n    // 円盤を tar の上に置く\n    tar.PushBack(pan.Value)\n    // `src` の最上部の円盤を取り外す\n    src.Remove(pan)\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfunc dfsHanota(i int, src, buf, tar *list.List) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move(src, tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfsHanota(i-1, src, tar, buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* ハノイの塔を解く */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfsHanota(n, A, B, C)\n}\n
hanota.swift
/* 円盤を 1 枚移動 */\nfunc move(src: inout [Int], tar: inout [Int]) {\n    // src の上から円盤を1枚取り出す\n    let pan = src.popLast()!\n    // 円盤を tar の上に置く\n    tar.append(pan)\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfunc dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move(src: &src, tar: &tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src: &src, tar: &tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)\n}\n\n/* ハノイの塔を解く */\nfunc solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {\n    let n = A.count\n    // リストの末尾が柱の上端に対応する\n    // src の上から n 個の円盤を、B を介して C に移動する\n    dfs(i: n, src: &A, buf: &B, tar: &C)\n}\n
hanota.js
/* 円盤を 1 枚移動 */\nfunction move(src, tar) {\n    // src の上から円盤を1枚取り出す\n    const pan = src.pop();\n    // 円盤を tar の上に置く\n    tar.push(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfunction dfs(i, src, buf, tar) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
hanota.ts
/* 円盤を 1 枚移動 */\nfunction move(src: number[], tar: number[]): void {\n    // src の上から円盤を1枚取り出す\n    const pan = src.pop();\n    // 円盤を tar の上に置く\n    tar.push(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfunction dfs(i: number, src: number[], buf: number[], tar: number[]): void {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nfunction solveHanota(A: number[], B: number[], C: number[]): void {\n    const n = A.length;\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
hanota.dart
/* 円盤を 1 枚移動 */\nvoid move(List<int> src, List<int> tar) {\n  // src の上から円盤を1枚取り出す\n  int pan = src.removeLast();\n  // 円盤を tar の上に置く\n  tar.add(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nvoid dfs(int i, List<int> src, List<int> buf, List<int> tar) {\n  // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n  if (i == 1) {\n    move(src, tar);\n    return;\n  }\n  // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n  dfs(i - 1, src, tar, buf);\n  // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n  move(src, tar);\n  // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n  dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nvoid solveHanota(List<int> A, List<int> B, List<int> C) {\n  int n = A.length;\n  // A の上から n 枚の円盤を B を介して C へ移す\n  dfs(n, A, B, C);\n}\n
hanota.rs
/* 円盤を 1 枚移動 */\nfn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // src の上から円盤を1枚取り出す\n    let pan = src.pop().unwrap();\n    // 円盤を tar の上に置く\n    tar.push(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move_pan(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move_pan(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nfn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {\n    let n = A.len() as i32;\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
hanota.c
/* 円盤を 1 枚移動 */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n    // src の上から円盤を1枚取り出す\n    int pan = src[*srcSize - 1];\n    src[*srcSize - 1] = 0;\n    (*srcSize)--;\n    // 円盤を tar の上に置く\n    tar[*tarSize] = pan;\n    (*tarSize)++;\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nvoid dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, srcSize, tar, tarSize);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, srcSize, tar, tarSize);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);\n}\n\n/* ハノイの塔を解く */\nvoid solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n
hanota.kt
/* 円盤を 1 枚移動 */\nfun move(src: MutableList<Int>, tar: MutableList<Int>) {\n    // src の上から円盤を1枚取り出す\n    val pan = src.removeAt(src.size - 1)\n    // 円盤を tar の上に置く\n    tar.add(pan)\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar)\n}\n\n/* ハノイの塔を解く */\nfun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {\n    val n = A.size\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C)\n}\n
hanota.rb
### 円盤を1枚移動 ###\ndef move(src, tar)\n  # src の上から円盤を1枚取り出す\n  pan = src.pop\n  # 円盤を tar の上に置く\n  tar << pan\nend\n\n### ハノイの塔 f(i) を解く ###\ndef dfs(i, src, buf, tar)\n  # src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n  if i == 1\n    move(src, tar)\n    return\n  end\n\n  # 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n  dfs(i - 1, src, tar, buf)\n  # 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n  move(src, tar)\n  # 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n  dfs(i - 1, buf, src, tar)\nend\n\n### ハノイの塔を解く ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # A の上から n 枚の円盤を B を介して C へ移す\n  dfs(n, _A, _B, _C)\nend\n
コードの可視化

全画面で見る >

以下の図に示すように、ハノイの塔の問題は高さ \\(n\\) の再帰木を形成し、各ノードは 1 つの部分問題、すなわち 1 つ起動された dfs() 関数に対応します。したがって時間計算量は \\(O(2^n)\\) 、空間計算量は \\(O(n)\\) です。

図 12-15   ハノイの塔の問題の再帰木

Quote

ハノイの塔の問題は古い伝説に由来します。古代インドのある寺院で、僧侶たちは 3 本の高いダイヤモンドの柱と、\\(64\\) 枚の大きさの異なる金の円盤を持っていました。僧侶たちは絶えず円盤を動かし、最後の 1 枚が正しく置かれた瞬間に世界が終わると信じていました。

しかし、たとえ僧侶たちが 1 秒に 1 回移動するとしても、合計でおよそ \\(2^{64} \\approx 1.84×10^{19}\\) 秒、約 \\(5850\\) 億年が必要で、現在推定されている宇宙の年齢をはるかに上回ります。したがって、この伝説が本当だったとしても、世界の終わりを心配する必要はなさそうです。

","path":["第 12 章   分割統治","12.4   ハノイの塔の問題"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/","level":1,"title":"12.5   まとめ","text":"","path":["第 12 章   分割統治","12.5   まとめ"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • 分割統治法は一般的なアルゴリズム設計戦略であり、分(分割)と治(統合)の 2 つの段階からなり、通常は再帰に基づいて実装されます。
  • それが分割統治法の問題かどうかを判断する基準には、問題を分解できるか、部分問題が独立しているか、部分問題を統合できるかが含まれます。
  • マージソートは分割統治法の典型的な応用であり、配列を再帰的に同じ長さの 2 つの部分配列に分割し、要素が 1 つだけになるまで続け、その後で各層を順に統合してソートを完了します。
  • 分割統治法を導入すると、多くの場合アルゴリズムの効率を高められます。一方では操作回数が減り、他方では分割後にシステムの並列最適化を行いやすくなります。
  • 分割統治法は多くのアルゴリズム問題を解決できるだけでなく、データ構造やアルゴリズム設計にも広く応用されており、至る所でその姿を見ることができます。
  • 総当たり探索と比べて、適応的な探索のほうが効率的です。時間計算量が \\(O(\\log n)\\) の探索アルゴリズムは、通常は分割統治法に基づいて実装されます。
  • 二分探索は分割統治法のもう 1 つの典型的な応用であり、部分問題の解を統合する手順を含みません。再帰的な分割統治によって二分探索を実現できます。
  • 二分木を構築する問題では、木の構築(元の問題)を左部分木と右部分木の構築(部分問題)に分けられます。これは、先行順序走査と中間順序走査のインデックス区間を分割することで実現できます。
  • ハノイの塔の問題では、規模 \\(n\\) の問題を、規模 \\(n-1\\) の 2 つの部分問題と規模 \\(1\\) の 1 つの部分問題に分けられます。これら 3 つの部分問題を順に解くと、元の問題も解決されます。
","path":["第 12 章   分割統治","12.5   まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"第 14 章   動的計画法","text":"

Abstract

小川は川へと注ぎ、河川は大海へと注ぐ。

動的計画法は小さな問題の解を集めて大きな問題の答えとし、一歩ずつ私たちを問題解決の彼岸へと導く。

","path":["第 14 章   動的計画法"],"tags":[]},{"location":"chapter_dynamic_programming/#_1","level":2,"title":"章の内容","text":"
  • 14.1   動的計画法入門
  • 14.2   動的計画法の問題特性
  • 14.3   動的計画法の問題解決の考え方
  • 14.4   0-1 ナップサック問題
  • 14.5   完全ナップサック問題
  • 14.6   編集距離問題
  • 14.7   まとめ
","path":["第 14 章   動的計画法"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/","level":1,"title":"14.2   動的計画法の問題特性","text":"

前節では、動的計画法が部分問題への分解によってどのように元の問題を解くのかを学びました。実際、部分問題への分解は汎用的なアルゴリズムの考え方であり、分割統治法、動的計画法、バックトラッキングでは重視点が異なります。

  • 分割統治法は、元の問題を再帰的に複数の互いに独立した部分問題へ分割し、最小の部分問題に至るまで分解したうえで、バックトラック時に部分問題の解を統合し、最終的に元の問題の解を得ます。
  • 動的計画法も問題を再帰的に分解しますが、分割統治法との主な違いは、動的計画法における部分問題が相互依存しており、分解の過程で多数の重複部分問題が現れることです。
  • バックトラッキング法は、試行と巻き戻しの中ですべての可能な解を列挙し、枝刈りによって不要な探索分岐を避けます。元の問題の解は一連の意思決定ステップから構成されるため、各決定ステップ以前の部分系列を一つの部分問題と見なせます。

実際、動的計画法は最適化問題を解くためによく用いられます。これらは重複部分問題を含むだけでなく、さらに二つの大きな特性、すなわち最適部分構造と無後効性を備えています。

","path":["第 14 章   動的計画法","14.2   動的計画法の問題特性"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1421","level":2,"title":"14.2.1   最適部分構造","text":"

階段昇り問題を少し変更し、最適部分構造の概念をより示しやすくします。

階段昇りの最小コスト

階段が与えられ、各ステップで \\(1\\) 段または \\(2\\) 段上ることができます。各段には非負整数が貼られており、その段に到達するために支払う必要があるコストを表します。非負整数配列 \\(cost\\) が与えられ、\\(cost[i]\\) は第 \\(i\\) 段で支払うコストを表し、\\(cost[0]\\) は地面(開始地点)です。頂上に到達するために必要な最小コストを求めてください。

下図に示すように、第 \\(1\\)、\\(2\\)、\\(3\\) 段のコストがそれぞれ \\(1\\)、\\(10\\)、\\(1\\) である場合、地面から第 \\(3\\) 段まで上る最小コストは \\(2\\) です。

図 14-6   第 3 段まで上る最小コスト

\\(dp[i]\\) を第 \\(i\\) 段まで上るのに累積して支払ったコストとします。第 \\(i\\) 段には \\(i - 1\\) 段または \\(i - 2\\) 段からしか到達できないため、\\(dp[i]\\) は \\(dp[i - 1] + cost[i]\\) または \\(dp[i - 2] + cost[i]\\) のいずれかになります。コストをできるだけ小さくするには、この二つのうち小さいほうを選べばよいです。

\\[ dp[i] = \\min(dp[i-1], dp[i-2]) + cost[i] \\]

ここから最適部分構造の意味を導けます。**元の問題の最適解は、部分問題の最適解から構築される**ということです。

この問題が最適部分構造を持つことは明らかです。二つの部分問題の最適解 \\(dp[i-1]\\) と \\(dp[i-2]\\) からより良いほうを選び、それを用いて元の問題 \\(dp[i]\\) の最適解を構築しています。

では、前節の階段昇り問題には最適部分構造があるのでしょうか。その目的は方法数を求めることで、一見すると計数問題です。しかし問い方を変えて「最大の方法数を求める」とすると、意外にも、問題の変更前後は等価であるにもかかわらず、最適部分構造が現れます。すなわち、第 \\(n\\) 段の最大方法数は第 \\(n-1\\) 段と第 \\(n-2\\) 段の最大方法数の和に等しいのです。このように、最適部分構造の解釈は比較的柔軟であり、問題によって意味合いが異なります。

状態遷移方程式と初期状態 \\(dp[1] = cost[1]\\) および \\(dp[2] = cost[2]\\) に基づいて、次の動的計画法コードが得られます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
def min_cost_climbing_stairs_dp(cost: list[int]) -> int:\n    \"\"\"階段登りの最小コスト:動的計画法\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    # 部分問題の解を保存するために dp テーブルを初期化\n    dp = [0] * (n + 1)\n    # 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1], dp[2] = cost[1], cost[2]\n    # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in range(3, n + 1):\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    return dp[n]\n
min_cost_climbing_stairs_dp.cpp
/* 階段登りの最小コスト:動的計画法 */\nint minCostClimbingStairsDP(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 部分問題の解を保存するために dp テーブルを初期化\n    vector<int> dp(n + 1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.java
/* 階段登りの最小コスト:動的計画法 */\nint minCostClimbingStairsDP(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[] dp = new int[n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.cs
/* 階段登りの最小コスト:動的計画法 */\nint MinCostClimbingStairsDP(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[] dp = new int[n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.go
/* 階段登りの最小コスト:動的計画法 */\nfunc minCostClimbingStairsDP(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    dp := make([]int, n+1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i := 3; i <= n; i++ {\n        dp[i] = min(dp[i-1], dp[i-2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.swift
/* 階段登りの最小コスト:動的計画法 */\nfunc minCostClimbingStairsDP(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    var dp = Array(repeating: 0, count: n + 1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3 ... n {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.js
/* 階段登りの最小コスト:動的計画法 */\nfunction minCostClimbingStairsDP(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = new Array(n + 1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.ts
/* 階段登りの最小コスト:動的計画法 */\nfunction minCostClimbingStairsDP(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = new Array(n + 1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.dart
/* 階段登りの最小コスト:動的計画法 */\nint minCostClimbingStairsDP(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  // 部分問題の解を保存するために dp テーブルを初期化\n  List<int> dp = List.filled(n + 1, 0);\n  // 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1] = cost[1];\n  dp[2] = cost[2];\n  // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  for (int i = 3; i <= n; i++) {\n    dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n  }\n  return dp[n];\n}\n
min_cost_climbing_stairs_dp.rs
/* 階段登りの最小コスト:動的計画法 */\nfn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    let mut dp = vec![-1; n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3..=n {\n        dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    dp[n]\n}\n
min_cost_climbing_stairs_dp.c
/* 階段登りの最小コスト:動的計画法 */\nint minCostClimbingStairsDP(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int *dp = calloc(n + 1, sizeof(int));\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    int res = dp[n];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
min_cost_climbing_stairs_dp.kt
/* 階段登りの最小コスト:動的計画法 */\nfun minCostClimbingStairsDP(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    // 部分問題の解を保存するために dp テーブルを初期化\n    val dp = IntArray(n + 1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (i in 3..n) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.rb
### 階段登りの最小コスト:動的計画法 ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # 部分問題の解を保存するために dp テーブルを初期化\n  dp = Array.new(n + 1, 0)\n  # 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1], dp[2] = cost[1], cost[2]\n  # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n
コードの可視化

全画面で見る >

下図は上記コードの動的計画法の過程を示しています。

図 14-7   階段昇り最小コストの動的計画法の過程

この問題では空間最適化も可能であり、一次元をゼロ次元まで圧縮することで、空間計算量を \\(O(n)\\) から \\(O(1)\\) に削減できます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:\n    \"\"\"階段昇りの最小コスト:空間最適化後の動的計画法\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    a, b = cost[1], cost[2]\n    for i in range(3, n + 1):\n        a, b = b, min(a, b) + cost[i]\n    return b\n
min_cost_climbing_stairs_dp.cpp
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nint minCostClimbingStairsDPComp(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.java
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nint minCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.cs
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nint MinCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.Min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.go
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfunc minCostClimbingStairsDPComp(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    a, b := cost[1], cost[2]\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i := 3; i <= n; i++ {\n        tmp := b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.swift
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfunc minCostClimbingStairsDPComp(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    var (a, b) = (cost[1], cost[2])\n    for i in 3 ... n {\n        (a, b) = (b, min(a, b) + cost[i])\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.js
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfunction minCostClimbingStairsDPComp(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.ts
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfunction minCostClimbingStairsDPComp(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.dart
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nint minCostClimbingStairsDPComp(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  int a = cost[1], b = cost[2];\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = min(a, tmp) + cost[i];\n    a = tmp;\n  }\n  return b;\n}\n
min_cost_climbing_stairs_dp.rs
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    };\n    let (mut a, mut b) = (cost[1], cost[2]);\n    for i in 3..=n {\n        let tmp = b;\n        b = cmp::min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    b\n}\n
min_cost_climbing_stairs_dp.c
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nint minCostClimbingStairsDPComp(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = myMin(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.kt
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfun minCostClimbingStairsDPComp(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    var a = cost[1]\n    var b = cost[2]\n    for (i in 3..n) {\n        val tmp = b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.rb
### 階段登りの最小コスト:動的計画法 ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # 部分問題の解を保存するために dp テーブルを初期化\n  dp = Array.new(n + 1, 0)\n  # 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1], dp[2] = cost[1], cost[2]\n  # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n\n# 階段昇りの最小コスト:空間最適化後の動的計画法\ndef min_cost_climbing_stairs_dp_comp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  a, b = cost[1], cost[2]\n  (3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] }\n  b\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.2   動的計画法の問題特性"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1422","level":2,"title":"14.2.2   無後効性","text":"

無後効性は、動的計画法が問題を効率よく解ける重要な特性の一つであり、その定義は次のとおりです。ある確定した状態が与えられたとき、その後の発展は現在の状態のみに依存し、過去に経たすべての状態には依存しない。

階段昇り問題を例にすると、状態 \\(i\\) が与えられたとき、そこから状態 \\(i+1\\) と状態 \\(i+2\\) へ発展し、それぞれ \\(1\\) 段進む場合と \\(2\\) 段進む場合に対応します。この二つの選択を行う際、状態 \\(i\\) より前の状態を考慮する必要はなく、それらは状態 \\(i\\) の将来に影響を与えません。

しかし、階段昇り問題に制約を一つ追加すると、状況は変わります。

制約付き階段昇り

全部で \\(n\\) 段ある階段が与えられ、各ステップで \\(1\\) 段または \\(2\\) 段上ることができます。ただし、連続する 2 回で \\(1\\) 段ずつ上ることはできません。頂上まで上る方法は何通りあるでしょうか。

下図に示すように、第 \\(3\\) 段まで上る実行可能な方法は \\(2\\) 通りしか残りません。そのうち、\\(1\\) 段ずつ 3 回連続で上る方法は制約を満たさないため除外されます。

図 14-8   制約付きで第 3 段まで上る方法数

この問題では、前回が \\(1\\) 段上りだった場合、次回は必ず \\(2\\) 段上らなければなりません。これは、**次の一手が現在の状態(現在いる階段の段数)だけでは独立に決まらず、一つ前の状態(前回いた段数)にも関係する**ことを意味します。

容易に分かるように、この問題はもはや無後効性を満たしておらず、状態遷移方程式 \\(dp[i] = dp[i-1] + dp[i-2]\\) も成立しません。というのも、\\(dp[i-1]\\) は今回 \\(1\\) 段上る場合を表しますが、その中には「前回も \\(1\\) 段上ってきた」方法が多数含まれており、制約を満たすためには \\(dp[i-1]\\) をそのまま \\(dp[i]\\) に加えることができないからです。

このため、状態定義を拡張する必要があります。**状態 \\([i, j]\\) は、第 \\(i\\) 段にいて前回に \\(j\\) 段上ったことを表す**とし、ここで \\(j \\in \\{1, 2\\}\\) です。この状態定義により、前回が \\(1\\) 段上りか \\(2\\) 段上りかを有効に区別でき、現在の状態がどこから来たのかを判断できます。

  • 前回に \\(1\\) 段上った場合、その前の回は \\(2\\) 段上りしか選べないため、\\(dp[i, 1]\\) は \\(dp[i-1, 2]\\) からのみ遷移できます。
  • 前回に \\(2\\) 段上った場合、その前の回は \\(1\\) 段上りまたは \\(2\\) 段上りを選べるため、\\(dp[i, 2]\\) は \\(dp[i-2, 1]\\) または \\(dp[i-2, 2]\\) から遷移できます。

下図に示すように、この定義のもとでは \\(dp[i, j]\\) は状態 \\([i, j]\\) に対応する方法数を表します。このとき状態遷移方程式は次のようになります。

\\[ \\begin{cases} dp[i, 1] = dp[i-1, 2] \\\\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \\end{cases} \\]

図 14-9   制約を考慮した漸化関係

最終的に、\\(dp[n, 1] + dp[n, 2]\\) を返せば十分であり、その和が第 \\(n\\) 段まで上る方法の総数を表します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_constraint_dp.py
def climbing_stairs_constraint_dp(n: int) -> int:\n    \"\"\"制約付き階段登り:動的計画法\"\"\"\n    if n == 1 or n == 2:\n        return 1\n    # 部分問題の解を保存するために dp テーブルを初期化\n    dp = [[0] * 3 for _ in range(n + 1)]\n    # 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1], dp[1][2] = 1, 0\n    dp[2][1], dp[2][2] = 0, 1\n    # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in range(3, n + 1):\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    return dp[n][1] + dp[n][2]\n
climbing_stairs_constraint_dp.cpp
/* 制約付き階段登り:動的計画法 */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    vector<vector<int>> dp(n + 1, vector<int>(3, 0));\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.java
/* 制約付き階段登り:動的計画法 */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[][] dp = new int[n + 1][3];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.cs
/* 制約付き階段登り:動的計画法 */\nint ClimbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[,] dp = new int[n + 1, 3];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1, 1] = 1;\n    dp[1, 2] = 0;\n    dp[2, 1] = 0;\n    dp[2, 2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i, 1] = dp[i - 1, 2];\n        dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];\n    }\n    return dp[n, 1] + dp[n, 2];\n}\n
climbing_stairs_constraint_dp.go
/* 制約付き階段登り:動的計画法 */\nfunc climbingStairsConstraintDP(n int) int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    dp := make([][3]int, n+1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i := 3; i <= n; i++ {\n        dp[i][1] = dp[i-1][2]\n        dp[i][2] = dp[i-2][1] + dp[i-2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.swift
/* 制約付き階段登り:動的計画法 */\nfunc climbingStairsConstraintDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3 ... n {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.js
/* 制約付き階段登り:動的計画法 */\nfunction climbingStairsConstraintDP(n) {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = Array.from(new Array(n + 1), () => new Array(3));\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.ts
/* 制約付き階段登り:動的計画法 */\nfunction climbingStairsConstraintDP(n: number): number {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () => new Array(3));\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.dart
/* 制約付き階段登り:動的計画法 */\nint climbingStairsConstraintDP(int n) {\n  if (n == 1 || n == 2) {\n    return 1;\n  }\n  // 部分問題の解を保存するために dp テーブルを初期化\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));\n  // 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1][1] = 1;\n  dp[1][2] = 0;\n  dp[2][1] = 0;\n  dp[2][2] = 1;\n  // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  for (int i = 3; i <= n; i++) {\n    dp[i][1] = dp[i - 1][2];\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n  }\n  return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.rs
/* 制約付き階段登り:動的計画法 */\nfn climbing_stairs_constraint_dp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return 1;\n    };\n    // 部分問題の解を保存するために dp テーブルを初期化\n    let mut dp = vec![vec![-1; 3]; n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3..=n {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.c
/* 制約付き階段登り:動的計画法 */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(3, sizeof(int));\n    }\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    int res = dp[n][1] + dp[n][2];\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
climbing_stairs_constraint_dp.kt
/* 制約付き階段登り:動的計画法 */\nfun climbingStairsConstraintDP(n: Int): Int {\n    if (n == 1 || n == 2) {\n        return 1\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    val dp = Array(n + 1) { IntArray(3) }\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (i in 3..n) {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.rb
### 制約付き階段登り:動的計画法 ###\ndef climbing_stairs_constraint_dp(n)\n  return 1 if n == 1 || n == 2\n\n  # 部分問題の解を保存するために dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(3, 0) }\n  # 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1][1], dp[1][2] = 1, 0\n  dp[2][1], dp[2][2] = 0, 1\n  # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  for i in 3...(n + 1)\n    dp[i][1] = dp[i - 1][2]\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n  end\n\n  dp[n][1] + dp[n][2]\nend\n
コードの可視化

全画面で見る >

上の例では、追加で考慮すべきなのは一つ前の状態だけであるため、状態定義を拡張することで問題を再び無後効性に適合させることができます。しかし、問題によっては非常に強い「後効性」があります。

階段昇りと障害物生成

全部で \\(n\\) 段ある階段が与えられ、各ステップで \\(1\\) 段または \\(2\\) 段上ることができます。**第 \\(i\\) 段に到達すると、システムは自動的に第 \\(2i\\) 段に障害物を置き、それ以降はどの回でも第 \\(2i\\) 段へ跳ぶことができない**とします。例えば、最初の 2 回でそれぞれ第 \\(2\\) 段、第 \\(3\\) 段に到達した場合、その後は第 \\(4\\) 段と第 \\(6\\) 段に跳ぶことはできません。頂上まで上る方法は何通りあるでしょうか。

この問題では、次の跳躍が過去のすべての状態に依存します。なぜなら、各跳躍がより高い段に障害物を設置し、将来の跳躍に影響するからです。この種の問題は、動的計画法では解きにくいことが多いです。

実際、多くの複雑な組合せ最適化問題(例えば巡回セールスマン問題)は無後効性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を用いて、限られた時間内に実用的な局所最適解を得ます。

","path":["第 14 章   動的計画法","14.2   動的計画法の問題特性"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   動的計画法の問題解決の考え方","text":"

前の 2 節では動的計画法の問題の主要な特徴を紹介しました。ここからは、さらに実用的な 2 つの問題を一緒に考えていきます。

  1. ある問題が動的計画法の問題かどうかを、どのように判断すればよいでしょうか?
  2. 動的計画法の問題を解くには、どこから着手し、完全な手順はどのようなものでしょうか?
","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431","level":2,"title":"14.3.1   問題の判定","text":"

一般に、ある問題が重複部分問題と最適部分構造を含み、さらに無後效性を満たしているなら、通常は動的計画法で解くのに適しています。しかし、問題文からこれらの性質を直接読み取るのは簡単ではありません。そのため通常は条件を少し緩めて、**まずその問題がバックトラッキング(全探索)で解くのに適しているか**を観察します。

バックトラッキングで解くのに適した問題は、通常「決定木モデル」を満たします。この種の問題は木構造で表現でき、各ノードは 1 つの決定を表し、各経路は 1 つの決定列を表します。

言い換えると、問題に明確な決定の概念が含まれており、解が一連の決定によって生成されるなら、その問題は決定木モデルを満たし、通常はバックトラッキングで解くことができます。

これに加えて、動的計画法の問題には判定のための「加点要素」もあります。

  • 問題文に最大(最小)や最多(最少)などの最適化に関する記述がある。
  • 問題の状態が配列、多次元行列、または木で表現でき、ある状態とその周辺の状態の間に漸化的な関係がある。

反対に、「減点要素」もあります。

  • 問題の目的が最適解を求めることではなく、あり得るすべての解を列挙することである。
  • 問題文に明確な順列・組合せの特徴があり、具体的な複数の解を返す必要がある。

ある問題が決定木モデルを満たし、さらに比較的明確な「加点要素」を備えているなら、その問題は動的計画法の問題であると仮定し、解く過程でそれを検証できます。

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432","level":2,"title":"14.3.2   問題を解く手順","text":"

動的計画法の解法の流れは問題の性質や難易度によって異なりますが、通常は次の手順に従います。すなわち、決定を記述し、状態を定義し、\\(dp\\) テーブルを構築し、状態遷移方程式を導出し、境界条件を定めます。

解法の手順をより具体的に示すために、ここでは古典的な問題である「最小経路和」を例にします。

Question

\\(n \\times m\\) の 2 次元グリッド grid が与えられます。グリッドの各セルには非負整数が格納されており、そのセルのコストを表します。ロボットは左上のセルを始点とし、毎回下または右に 1 マスだけ移動して、右下のセルまで進みます。左上から右下までの最小経路和を返してください。

次の図は 1 つの例を示しており、このグリッドの最小経路和は \\(13\\) です。

図 14-10   最小経路和のサンプルデータ

ステップ 1:各ラウンドの決定を考え、状態を定義して、\\(dp\\) テーブルを得る

この問題における各ラウンドの決定は、現在のマスから下または右へ 1 マス進むことです。現在のマスの行・列インデックスを \\([i, j]\\) とすると、下または右へ 1 マス進んだ後のインデックスは \\([i+1, j]\\) または \\([i, j+1]\\) になります。したがって、状態には行インデックスと列インデックスの 2 つの変数を含め、\\([i, j]\\) と表します。

状態 \\([i, j]\\) に対応する部分問題は、始点 \\([0, 0]\\) から \\([i, j]\\) まで進む最小経路和であり、その解を \\(dp[i, j]\\) と記します。

これで、次の図に示す 2 次元の \\(dp\\) 行列が得られます。そのサイズは入力グリッド \\(grid\\) と同じです。

図 14-11   状態の定義と dp テーブル

Note

動的計画法とバックトラッキングの過程は、いずれも 1 つの決定列として記述できます。そして状態は、すべての決定変数から構成されます。状態には解法の進行状況を表すすべての変数が含まれているべきであり、次の状態を導くのに十分な情報を持っている必要があります。

各状態は 1 つの部分問題に対応しており、すべての部分問題の解を保存するために \\(dp\\) テーブルを定義します。状態の各独立変数は、\\(dp\\) テーブルの 1 つの次元に対応します。本質的に、\\(dp\\) テーブルは状態と部分問題の解との対応関係です。

ステップ 2:最適部分構造を見つけ、状態遷移方程式を導出する

状態 \\([i, j]\\) は、上のマス \\([i-1, j]\\) または左のマス \\([i, j-1]\\) からしか遷移してきません。したがって最適部分構造は、\\([i, j]\\) に到達する最小経路和が、\\([i, j-1]\\) の最小経路和と \\([i-1, j]\\) の最小経路和のうち小さい方によって決まる、ということです。

以上の分析から、次の図に示す状態遷移方程式を導くことができます。

\\[ dp[i, j] = \\min(dp[i-1, j], dp[i, j-1]) + grid[i, j] \\]

図 14-12   最適部分構造と状態遷移方程式

Note

定義済みの \\(dp\\) テーブルに基づいて、元の問題と部分問題の関係を考え、部分問題の最適解から元の問題の最適解を構成する方法、すなわち最適部分構造を見つけます。

ひとたび最適部分構造が見つかれば、それを使って状態遷移方程式を構築できます。

ステップ 3:境界条件と状態遷移の順序を決める

この問題では、先頭行にある状態は左の状態からしか得られず、先頭列にある状態は上の状態からしか得られません。したがって、先頭行 \\(i = 0\\) と先頭列 \\(j = 0\\) が境界条件になります。

次の図に示すように、各マスは左のマスと上のマスから遷移してくるため、ループを用いて行列を走査します。外側のループで各行を、内側のループで各列を走査します。

図 14-13   境界条件と状態遷移の順序

Note

境界条件は、動的計画法では \\(dp\\) テーブルの初期化に使われ、探索では枝刈りに使われます。

状態遷移の順序で重要なのは、現在の問題の解を計算するときに、それが依存するより小さな部分問題の解がすべてすでに正しく計算済みであることを保証する点です。

以上の分析により、すでに動的計画法のコードを直接書くことができます。しかし、部分問題への分解はトップダウンの考え方であるため、「力任せ探索 \\(\\rightarrow\\) メモ化探索 \\(\\rightarrow\\) 動的計画法」の順に実装するほうが、思考の流れにはより自然です。

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1-1","level":3,"title":"1.   方法 1:力任せ探索","text":"

状態 \\([i, j]\\) から探索を開始し、より小さな状態 \\([i-1, j]\\) と \\([i, j-1]\\) へと分解していきます。再帰関数には次の要素が含まれます。

  • 再帰引数:状態 \\([i, j]\\) 。
  • 戻り値:\\([0, 0]\\) から \\([i, j]\\) までの最小経路和 \\(dp[i, j]\\) 。
  • 終了条件:\\(i = 0\\) かつ \\(j = 0\\) のとき、コスト \\(grid[0, 0]\\) を返す。
  • 枝刈り:\\(i < 0\\) または \\(j < 0\\) でインデックスが範囲外になった場合、コスト \\(+\\infty\\) を返し、実行不可能であることを表す。

実装コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:\n    \"\"\"最小経路和:全探索\"\"\"\n    # 左上のセルなら探索を終了する\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 or j < 0:\n        return inf\n    # 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    up = min_path_sum_dfs(grid, i - 1, j)\n    left = min_path_sum_dfs(grid, i, j - 1)\n    # 左上隅から (i, j) までの最小経路コストを返す\n    return min(left, up) + grid[i][j]\n
min_path_sum.cpp
/* 最小経路和:全探索 */\nint minPathSumDFS(vector<vector<int>> &grid, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n}\n
min_path_sum.java
/* 最小経路和:全探索 */\nint minPathSumDFS(int[][] grid, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.cs
/* 最小経路和:全探索 */\nint MinPathSumDFS(int[][] grid, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    int up = MinPathSumDFS(grid, i - 1, j);\n    int left = MinPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return Math.Min(left, up) + grid[i][j];\n}\n
min_path_sum.go
/* 最小経路和:全探索 */\nfunc minPathSumDFS(grid [][]int, i, j int) int {\n    // 左上のセルなら探索を終了する\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    up := minPathSumDFS(grid, i-1, j)\n    left := minPathSumDFS(grid, i, j-1)\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return int(math.Min(float64(left), float64(up))) + grid[i][j]\n}\n
min_path_sum.swift
/* 最小経路和:全探索 */\nfunc minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int {\n    // 左上のセルなら探索を終了する\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    let up = minPathSumDFS(grid: grid, i: i - 1, j: j)\n    let left = minPathSumDFS(grid: grid, i: i, j: j - 1)\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return min(left, up) + grid[i][j]\n}\n
min_path_sum.js
/* 最小経路和:全探索 */\nfunction minPathSumDFS(grid, i, j) {\n    // 左上のセルなら探索を終了する\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.ts
/* 最小経路和:全探索 */\nfunction minPathSumDFS(\n    grid: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // 左上のセルなら探索を終了する\n    if (i === 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.dart
/* 最小経路和:全探索 */\nint minPathSumDFS(List<List<int>> grid, int i, int j) {\n  // 左上のセルなら探索を終了する\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n  if (i < 0 || j < 0) {\n    // Dart では、int 型は固定範囲の整数であり、「無限大」を表す値は存在しない\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n  int up = minPathSumDFS(grid, i - 1, j);\n  int left = minPathSumDFS(grid, i, j - 1);\n  // 左上隅から (i, j) までの最小経路コストを返す\n  return min(left, up) + grid[i][j];\n}\n
min_path_sum.rs
/* 最小経路和:全探索 */\nfn min_path_sum_dfs(grid: &Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // 左上のセルなら探索を終了する\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    let up = min_path_sum_dfs(grid, i - 1, j);\n    let left = min_path_sum_dfs(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    std::cmp::min(left, up) + grid[i as usize][j as usize]\n}\n
min_path_sum.c
/* 最小経路和:全探索 */\nint minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n}\n
min_path_sum.kt
/* 最小経路和:全探索 */\nfun minPathSumDFS(grid: Array<IntArray>, i: Int, j: Int): Int {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    val up = minPathSumDFS(grid, i - 1, j)\n    val left = minPathSumDFS(grid, i, j - 1)\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return min(left, up) + grid[i][j]\n}\n
min_path_sum.rb
### 最小経路和:全探索 ###\ndef min_path_sum_dfs(grid, i, j)\n  # 左上のセルなら探索を終了する\n  return grid[i][j] if i == 0 && j == 0\n  # 行または列のインデックスが範囲外なら、コスト +∞ を返す\n  return Float::INFINITY if i < 0 || j < 0\n  # 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n  up = min_path_sum_dfs(grid, i - 1, j)\n  left = min_path_sum_dfs(grid, i, j - 1)\n  # 左上隅から (i, j) までの最小経路コストを返す\n  [left, up].min + grid[i][j]\nend\n
コードの可視化

全画面で見る >

次の図は、\\(dp[2, 1]\\) を根ノードとする再帰木を示しています。この中にはいくつかの重複部分問題が含まれており、その数はグリッド grid のサイズが大きくなるにつれて急激に増加します。

本質的に、重複部分問題が生じる理由は、**左上からあるセルへ到達する経路が複数存在すること**にあります。

図 14-14   力任せ探索の再帰木

各状態には下と右の 2 通りの選択肢があり、左上から右下まで進むには合計で \\(m + n - 2\\) 歩必要です。したがって最悪時間計算量は \\(O(2^{m + n})\\) です。ここで、\\(n\\) と \\(m\\) はそれぞれグリッドの行数と列数を表します。なお、この見積もりではグリッド境界付近の状況を考慮していません。境界に達すると選択肢は 1 つだけになるため、実際の経路数はこれより少なくなります。

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-2","level":3,"title":"2.   方法 2:メモ化探索","text":"

グリッド grid と同じサイズのメモ配列 mem を導入し、各部分問題の解を記録して、重複部分問題を枝刈りします。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dfs_mem(\n    grid: list[list[int]], mem: list[list[int]], i: int, j: int\n) -> int:\n    \"\"\"最小経路和:メモ化探索\"\"\"\n    # 左上のセルなら探索を終了する\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 or j < 0:\n        return inf\n    # 既に記録があればそのまま返す\n    if mem[i][j] != -1:\n        return mem[i][j]\n    # 左と上のセルからの最小経路コスト\n    up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n    left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n    # 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n
min_path_sum.cpp
/* 最小経路和:メモ化探索 */\nint minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
min_path_sum.java
/* 最小経路和:メモ化探索 */\nint minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.cs
/* 最小経路和:メモ化探索 */\nint MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    int up = MinPathSumDFSMem(grid, mem, i - 1, j);\n    int left = MinPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = Math.Min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.go
/* 最小経路和:メモ化探索 */\nfunc minPathSumDFSMem(grid, mem [][]int, i, j int) int {\n    // 左上のセルなら探索を終了する\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // 既に記録があればそのまま返す\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // 左と上のセルからの最小経路コスト\n    up := minPathSumDFSMem(grid, mem, i-1, j)\n    left := minPathSumDFSMem(grid, mem, i, j-1)\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = int(math.Min(float64(left), float64(up))) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.swift
/* 最小経路和:メモ化探索 */\nfunc minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int {\n    // 左上のセルなら探索を終了する\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // 既に記録があればそのまま返す\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // 左と上のセルからの最小経路コスト\n    let up = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j)\n    let left = minPathSumDFSMem(grid: grid, mem: &mem, i: i, j: j - 1)\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.js
/* 最小経路和:メモ化探索 */\nfunction minPathSumDFSMem(grid, mem, i, j) {\n    // 左上のセルなら探索を終了する\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] !== -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.ts
/* 最小経路和:メモ化探索 */\nfunction minPathSumDFSMem(\n    grid: Array<Array<number>>,\n    mem: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // 左上のセルなら探索を終了する\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.dart
/* 最小経路和:メモ化探索 */\nint minPathSumDFSMem(List<List<int>> grid, List<List<int>> mem, int i, int j) {\n  // 左上のセルなら探索を終了する\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n  if (i < 0 || j < 0) {\n    // Dart では、int 型は固定範囲の整数であり、「無限大」を表す値は存在しない\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // 既に記録があればそのまま返す\n  if (mem[i][j] != -1) {\n    return mem[i][j];\n  }\n  // 左と上のセルからの最小経路コスト\n  int up = minPathSumDFSMem(grid, mem, i - 1, j);\n  int left = minPathSumDFSMem(grid, mem, i, j - 1);\n  // 左上から (i, j) までの最小経路コストを記録して返す\n  mem[i][j] = min(left, up) + grid[i][j];\n  return mem[i][j];\n}\n
min_path_sum.rs
/* 最小経路和:メモ化探索 */\nfn min_path_sum_dfs_mem(grid: &Vec<Vec<i32>>, mem: &mut Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // 左上のセルなら探索を終了する\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // 既に記録があればそのまま返す\n    if mem[i as usize][j as usize] != -1 {\n        return mem[i as usize][j as usize];\n    }\n    // 左と上のセルからの最小経路コスト\n    let up = min_path_sum_dfs_mem(grid, mem, i - 1, j);\n    let left = min_path_sum_dfs_mem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize];\n    mem[i as usize][j as usize]\n}\n
min_path_sum.c
/* 最小経路和:メモ化探索 */\nint minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
min_path_sum.kt
/* 最小経路和:メモ化探索 */\nfun minPathSumDFSMem(\n    grid: Array<IntArray>,\n    mem: Array<IntArray>,\n    i: Int,\n    j: Int\n): Int {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j]\n    }\n    // 左と上のセルからの最小経路コスト\n    val up = minPathSumDFSMem(grid, mem, i - 1, j)\n    val left = minPathSumDFSMem(grid, mem, i, j - 1)\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.rb
### 最小経路和:メモ化探索 ###\ndef min_path_sum_dfs_mem(grid, mem, i, j)\n  # 左上のセルなら探索を終了する\n  return grid[0][0] if i == 0 && j == 0\n  # 行または列のインデックスが範囲外なら、コスト +∞ を返す\n  return Float::INFINITY if i < 0 || j < 0\n  # 既に記録があればそのまま返す\n  return mem[i][j] if mem[i][j] != -1\n  # 左と上のセルからの最小経路コスト\n  up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n  left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n  # 左上から (i, j) までの最小経路コストを記録して返す\n  mem[i][j] = [left, up].min + grid[i][j]\nend\n
コードの可視化

全画面で見る >

次の図に示すように、メモ化を導入すると、すべての部分問題の解は 1 回だけ計算すればよくなります。したがって時間計算量は状態総数、すなわちグリッドサイズの \\(O(nm)\\) に依存します。

図 14-15   メモ化探索の再帰木

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#3-3","level":3,"title":"3.   方法 3:動的計画法","text":"

反復に基づいて動的計画法の解法を実装すると、コードは次のようになります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dp(grid: list[list[int]]) -> int:\n    \"\"\"最小経路和:動的計画法\"\"\"\n    n, m = len(grid), len(grid[0])\n    # dp テーブルを初期化\n    dp = [[0] * m for _ in range(n)]\n    dp[0][0] = grid[0][0]\n    # 状態遷移:先頭行\n    for j in range(1, m):\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    # 状態遷移:先頭列\n    for i in range(1, n):\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    # 状態遷移: 残りの行と列\n    for i in range(1, n):\n        for j in range(1, m):\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n    return dp[n - 1][m - 1]\n
min_path_sum.cpp
/* 最小経路和:動的計画法 */\nint minPathSumDP(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // dp テーブルを初期化\n    vector<vector<int>> dp(n, vector<int>(m));\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.java
/* 最小経路和:動的計画法 */\nint minPathSumDP(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // dp テーブルを初期化\n    int[][] dp = new int[n][m];\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.cs
/* 最小経路和:動的計画法 */\nint MinPathSumDP(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // dp テーブルを初期化\n    int[,] dp = new int[n, m];\n    dp[0, 0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (int j = 1; j < m; j++) {\n        dp[0, j] = dp[0, j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (int i = 1; i < n; i++) {\n        dp[i, 0] = dp[i - 1, 0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i, j] = Math.Min(dp[i, j - 1], dp[i - 1, j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1, m - 1];\n}\n
min_path_sum.go
/* 最小経路和:動的計画法 */\nfunc minPathSumDP(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // dp テーブルを初期化\n    dp := make([][]int, n)\n    for i := 0; i < n; i++ {\n        dp[i] = make([]int, m)\n    }\n    dp[0][0] = grid[0][0]\n    // 状態遷移:先頭行\n    for j := 1; j < m; j++ {\n        dp[0][j] = dp[0][j-1] + grid[0][j]\n    }\n    // 状態遷移:先頭列\n    for i := 1; i < n; i++ {\n        dp[i][0] = dp[i-1][0] + grid[i][0]\n    }\n    // 状態遷移: 残りの行と列\n    for i := 1; i < n; i++ {\n        for j := 1; j < m; j++ {\n            dp[i][j] = int(math.Min(float64(dp[i][j-1]), float64(dp[i-1][j]))) + grid[i][j]\n        }\n    }\n    return dp[n-1][m-1]\n}\n
min_path_sum.swift
/* 最小経路和:動的計画法 */\nfunc minPathSumDP(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: m), count: n)\n    dp[0][0] = grid[0][0]\n    // 状態遷移:先頭行\n    for j in 1 ..< m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // 状態遷移:先頭列\n    for i in 1 ..< n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1 ..< n {\n        for j in 1 ..< m {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
min_path_sum.js
/* 最小経路和:動的計画法 */\nfunction minPathSumDP(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i < n; i++) {\n        for (let j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.ts
/* 最小経路和:動的計画法 */\nfunction minPathSumDP(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i < n; i++) {\n        for (let j: number = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.dart
/* 最小経路和:動的計画法 */\nint minPathSumDP(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // dp テーブルを初期化\n  List<List<int>> dp = List.generate(n, (i) => List.filled(m, 0));\n  dp[0][0] = grid[0][0];\n  // 状態遷移:先頭行\n  for (int j = 1; j < m; j++) {\n    dp[0][j] = dp[0][j - 1] + grid[0][j];\n  }\n  // 状態遷移:先頭列\n  for (int i = 1; i < n; i++) {\n    dp[i][0] = dp[i - 1][0] + grid[i][0];\n  }\n  // 状態遷移: 残りの行と列\n  for (int i = 1; i < n; i++) {\n    for (int j = 1; j < m; j++) {\n      dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n    }\n  }\n  return dp[n - 1][m - 1];\n}\n
min_path_sum.rs
/* 最小経路和:動的計画法 */\nfn min_path_sum_dp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // dp テーブルを初期化\n    let mut dp = vec![vec![0; m]; n];\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for j in 1..m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for i in 1..n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1..n {\n        for j in 1..m {\n            dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    dp[n - 1][m - 1]\n}\n
min_path_sum.c
/* 最小経路和:動的計画法 */\nint minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // dp テーブルを初期化\n    int **dp = malloc(n * sizeof(int *));\n    for (int i = 0; i < n; i++) {\n        dp[i] = calloc(m, sizeof(int));\n    }\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    int res = dp[n - 1][m - 1];\n    // メモリを解放する\n    for (int i = 0; i < n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
min_path_sum.kt
/* 最小経路和:動的計画法 */\nfun minPathSumDP(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // dp テーブルを初期化\n    val dp = Array(n) { IntArray(m) }\n    dp[0][0] = grid[0][0]\n    // 状態遷移:先頭行\n    for (j in 1..<m) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // 状態遷移:先頭列\n    for (i in 1..<n) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // 状態遷移: 残りの行と列\n    for (i in 1..<n) {\n        for (j in 1..<m) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
min_path_sum.rb
### 最小経路和:動的計画法 ###\ndef min_path_sum_dp(grid)\n  n, m = grid.length, grid.first.length\n  # dp テーブルを初期化\n  dp = Array.new(n) { Array.new(m, 0) }\n  dp[0][0] = grid[0][0]\n  # 状態遷移:先頭行\n  (1...m).each { |j| dp[0][j] = dp[0][j - 1] + grid[0][j] }\n  # 状態遷移:先頭列\n  (1...n).each { |i| dp[i][0] = dp[i - 1][0] + grid[i][0] }\n  # 状態遷移: 残りの行と列\n  for i in 1...n\n    for j in 1...m\n      dp[i][j] = [dp[i][j - 1], dp[i - 1][j]].min + grid[i][j]\n    end\n  end\n  dp[n -1][m -1]\nend\n
コードの可視化

全画面で見る >

次の図は最小経路和の状態遷移の過程を示しています。グリッド全体を走査するため、時間計算量は \\(O(nm)\\) です。

配列 dp のサイズは \\(n \\times m\\) であるため、空間計算量は \\(O(nm)\\) です。

<1><2><3><4><5><6><7><8><9><10><11><12>

図 14-16   最小経路和の動的計画法の過程

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#4","level":3,"title":"4.   空間最適化","text":"

各マスは左のマスと上のマスにのみ関係するため、1 行の配列だけを使って \\(dp\\) テーブルを実装できます。

ただし、配列 dp は 1 行分の状態しか表せないため、先頭列の状態を事前に初期化することはできず、各行を走査するときに更新する必要があります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dp_comp(grid: list[list[int]]) -> int:\n    \"\"\"最小経路和:空間最適化後の動的計画法\"\"\"\n    n, m = len(grid), len(grid[0])\n    # dp テーブルを初期化\n    dp = [0] * m\n    # 状態遷移:先頭行\n    dp[0] = grid[0][0]\n    for j in range(1, m):\n        dp[j] = dp[j - 1] + grid[0][j]\n    # 状態遷移:残りの行\n    for i in range(1, n):\n        # 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0]\n        # 状態遷移:残りの列\n        for j in range(1, m):\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n    return dp[m - 1]\n
min_path_sum.cpp
/* 最小経路和:空間最適化後の動的計画法 */\nint minPathSumDPComp(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // dp テーブルを初期化\n    vector<int> dp(m);\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (int j = 1; j < m; j++) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.java
/* 最小経路和:空間最適化後の動的計画法 */\nint minPathSumDPComp(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // dp テーブルを初期化\n    int[] dp = new int[m];\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.cs
/* 最小経路和:空間最適化後の動的計画法 */\nint MinPathSumDPComp(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // dp テーブルを初期化\n    int[] dp = new int[m];\n    dp[0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.Min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.go
/* 最小経路和:空間最適化後の動的計画法 */\nfunc minPathSumDPComp(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // dp テーブルを初期化\n    dp := make([]int, m)\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0]\n    for j := 1; j < m; j++ {\n        dp[j] = dp[j-1] + grid[0][j]\n    }\n    // 状態遷移: 残りの行と列\n    for i := 1; i < n; i++ {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0]\n        // 状態遷移:残りの列\n        for j := 1; j < m; j++ {\n            dp[j] = int(math.Min(float64(dp[j-1]), float64(dp[j]))) + grid[i][j]\n        }\n    }\n    return dp[m-1]\n}\n
min_path_sum.swift
/* 最小経路和:空間最適化後の動的計画法 */\nfunc minPathSumDPComp(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // dp テーブルを初期化\n    var dp = Array(repeating: 0, count: m)\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0]\n    for j in 1 ..< m {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // 状態遷移:残りの行\n    for i in 1 ..< n {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0]\n        // 状態遷移:残りの列\n        for j in 1 ..< m {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
min_path_sum.js
/* 最小経路和:空間最適化後の動的計画法 */\nfunction minPathSumDPComp(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // dp テーブルを初期化\n    const dp = new Array(m);\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (let i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.ts
/* 最小経路和:空間最適化後の動的計画法 */\nfunction minPathSumDPComp(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // dp テーブルを初期化\n    const dp = new Array(m);\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (let i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.dart
/* 最小経路和:空間最適化後の動的計画法 */\nint minPathSumDPComp(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // dp テーブルを初期化\n  List<int> dp = List.filled(m, 0);\n  dp[0] = grid[0][0];\n  for (int j = 1; j < m; j++) {\n    dp[j] = dp[j - 1] + grid[0][j];\n  }\n  // 状態遷移:残りの行\n  for (int i = 1; i < n; i++) {\n    // 状態遷移:先頭列\n    dp[0] = dp[0] + grid[i][0];\n    // 状態遷移:残りの列\n    for (int j = 1; j < m; j++) {\n      dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n    }\n  }\n  return dp[m - 1];\n}\n
min_path_sum.rs
/* 最小経路和:空間最適化後の動的計画法 */\nfn min_path_sum_dp_comp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // dp テーブルを初期化\n    let mut dp = vec![0; m];\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for j in 1..m {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for i in 1..n {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for j in 1..m {\n            dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    dp[m - 1]\n}\n
min_path_sum.c
/* 最小経路和:空間最適化後の動的計画法 */\nint minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // dp テーブルを初期化\n    int *dp = calloc(m, sizeof(int));\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (int j = 1; j < m; j++) {\n            dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    int res = dp[m - 1];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
min_path_sum.kt
/* 最小経路和:空間最適化後の動的計画法 */\nfun minPathSumDPComp(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // dp テーブルを初期化\n    val dp = IntArray(m)\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0]\n    for (j in 1..<m) {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // 状態遷移:残りの行\n    for (i in 1..<n) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0]\n        // 状態遷移:残りの列\n        for (j in 1..<m) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
min_path_sum.rb
### 最小経路和:空間最適化後の動的計画法 ###\ndef min_path_sum_dp_comp(grid)\n  n, m = grid.length, grid.first.length\n  # dp テーブルを初期化\n  dp = Array.new(m, 0)\n  # 状態遷移:先頭行\n  dp[0] = grid[0][0]\n  (1...m).each { |j| dp[j] = dp[j - 1] + grid[0][j] }\n  # 状態遷移:残りの行\n  for i in 1...n\n    # 状態遷移:先頭列\n    dp[0] = dp[0] + grid[i][0]\n    # 状態遷移:残りの列\n    (1...m).each { |j| dp[j] = [dp[j - 1], dp[j]].min + grid[i][j] }\n  end\n  dp[m - 1]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/","level":1,"title":"14.6   編集距離問題","text":"

編集距離は、Levenshtein 距離とも呼ばれ、2つの文字列の相互変換に必要な最小の編集回数を指し、通常は情報検索や自然言語処理において2つの系列の類似度を測るために用いられます。

Question

2つの文字列 \\(s\\) と \\(t\\) を入力し、\\(s\\) を \\(t\\) に変換するのに必要な最小編集回数を返してください。

1つの文字列に対して3種類の編集操作を行えます。1文字の挿入、1文字の削除、任意の文字への置換です。

下図に示すように、kittensitting に変換するには 3 回の編集が必要で、内訳は 2 回の置換と 1 回の挿入です。helloalgo に変換する場合も 3 回必要で、内訳は 2 回の置換と 1 回の削除です。

図 14-27   編集距離のサンプルデータ

編集距離問題は決定木モデルで自然に説明できます。文字列が木のノードに対応し、1回の決定(1回の編集操作)が木の1本の辺に対応します。

下図に示すように、操作に制限がない場合、各ノードからは多くの辺を派生でき、それぞれの辺が1種類の操作に対応します。これは hello から algo への変換に多くの経路があり得ることを意味します。

決定木の観点から見ると、本問の目標はノード hello とノード algo の間の最短経路を求めることです。

図 14-28   決定木モデルに基づく編集距離問題の表現

","path":["第 14 章   動的計画法","14.6   編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#1","level":3,"title":"1.   動的計画法の考え方","text":"

第1ステップ:各ラウンドの決定を考え、状態を定義して、\\(dp\\) テーブルを得る

各ラウンドの決定は、文字列 \\(s\\) に対して1回の編集操作を行うことです。

編集操作の過程で問題の規模が徐々に小さくなることを期待します。そうして初めて部分問題を構築できます。文字列 \\(s\\) と \\(t\\) の長さをそれぞれ \\(n\\) と \\(m\\) とし、まず両文字列の末尾の文字 \\(s[n-1]\\) と \\(t[m-1]\\) を考えます。

  • \\(s[n-1]\\) と \\(t[m-1]\\) が同じなら、それらをスキップして、直接 \\(s[n-2]\\) と \\(t[m-2]\\) を考えます。
  • \\(s[n-1]\\) と \\(t[m-1]\\) が異なるなら、\\(s\\) に対して1回の編集(挿入、削除、置換)を行い、両文字列の末尾の文字を同じにします。そうすることでそれらをスキップし、より小さい問題を考えられます。

つまり、文字列 \\(s\\) に対する各ラウンドの決定(編集操作)は、\\(s\\) と \\(t\\) における残りの未一致文字を変化させます。したがって、状態は現在 \\(s\\) と \\(t\\) で考えている第 \\(i\\) と第 \\(j\\) 文字とし、\\([i, j]\\) と記します。

状態 \\([i, j]\\) に対応する部分問題は、**\\(s\\) の先頭 \\(i\\) 文字を \\(t\\) の先頭 \\(j\\) 文字に変換するのに必要な最小編集回数**です。

これにより、サイズが \\((i+1) \\times (j+1)\\) の2次元 \\(dp\\) テーブルが得られます。

第2ステップ:最適部分構造を見つけ、状態遷移方程式を導く

部分問題 \\(dp[i, j]\\) を考えます。これに対応する2つの文字列の末尾文字は \\(s[i-1]\\) と \\(t[j-1]\\) であり、編集操作の違いに応じて下図の3つの場合に分けられます。

  1. \\(s[i-1]\\) の後ろに \\(t[j-1]\\) を追加する。このとき残る部分問題は \\(dp[i, j-1]\\) です。
  2. \\(s[i-1]\\) を削除する。このとき残る部分問題は \\(dp[i-1, j]\\) です。
  3. \\(s[i-1]\\) を \\(t[j-1]\\) に置き換える。このとき残る部分問題は \\(dp[i-1, j-1]\\) です。

図 14-29   編集距離の状態遷移

以上の分析から、最適部分構造は次のように得られます。\\(dp[i, j]\\) の最小編集回数は、\\(dp[i, j-1]\\)、\\(dp[i-1, j]\\)、\\(dp[i-1, j-1]\\) の3つのうち最小の編集回数に、今回の編集回数 \\(1\\) を加えたものです。対応する状態遷移方程式は次のとおりです:

\\[ dp[i, j] = \\min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \\]

注意すべき点として、\\(s[i-1]\\) と \\(t[j-1]\\) が同じ場合、現在の文字を編集する必要はありません。この場合の状態遷移方程式は次のとおりです:

\\[ dp[i, j] = dp[i-1, j-1] \\]

第3ステップ:境界条件と状態遷移の順序を決める

2つの文字列がともに空のとき、編集回数は \\(0\\)、すなわち \\(dp[0, 0] = 0\\) です。\\(s\\) が空で \\(t\\) が空でないとき、最小編集回数は \\(t\\) の長さに等しいため、先頭行は \\(dp[0, j] = j\\) です。\\(s\\) が空でなく \\(t\\) が空のとき、最小編集回数は \\(s\\) の長さに等しいため、先頭列は \\(dp[i, 0] = i\\) です。

状態遷移方程式を観察すると、\\(dp[i, j]\\) の解は左、上、左上の解に依存します。そのため、2重ループで \\(dp\\) テーブル全体を順方向に走査すれば十分です。

","path":["第 14 章   動的計画法","14.6   編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#2","level":3,"title":"2.   コードの実装","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
def edit_distance_dp(s: str, t: str) -> int:\n    \"\"\"編集距離:動的計画法\"\"\"\n    n, m = len(s), len(t)\n    dp = [[0] * (m + 1) for _ in range(n + 1)]\n    # 状態遷移:先頭行と先頭列\n    for i in range(1, n + 1):\n        dp[i][0] = i\n    for j in range(1, m + 1):\n        dp[0][j] = j\n    # 状態遷移: 残りの行と列\n    for i in range(1, n + 1):\n        for j in range(1, m + 1):\n            if s[i - 1] == t[j - 1]:\n                # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            else:\n                # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1\n    return dp[n][m]\n
edit_distance.cpp
/* 編集距離:動的計画法 */\nint editDistanceDP(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));\n    // 状態遷移:先頭行と先頭列\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.java
/* 編集距離:動的計画法 */\nint editDistanceDP(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[][] dp = new int[n + 1][m + 1];\n    // 状態遷移:先頭行と先頭列\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.cs
/* 編集距離:動的計画法 */\nint EditDistanceDP(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[,] dp = new int[n + 1, m + 1];\n    // 状態遷移:先頭行と先頭列\n    for (int i = 1; i <= n; i++) {\n        dp[i, 0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0, j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i, j] = dp[i - 1, j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n, m];\n}\n
edit_distance.go
/* 編集距離:動的計画法 */\nfunc editDistanceDP(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, m+1)\n    }\n    // 状態遷移:先頭行と先頭列\n    for i := 1; i <= n; i++ {\n        dp[i][0] = i\n    }\n    for j := 1; j <= m; j++ {\n        dp[0][j] = j\n    }\n    // 状態遷移: 残りの行と列\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= m; j++ {\n            if s[i-1] == t[j-1] {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i-1][j-1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.swift
/* 編集距離:動的計画法 */\nfunc editDistanceDP(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)\n    // 状態遷移:先頭行と先頭列\n    for i in 1 ... n {\n        dp[i][0] = i\n    }\n    for j in 1 ... m {\n        dp[0][j] = j\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1 ... n {\n        for j in 1 ... m {\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.js
/* 編集距離:動的計画法 */\nfunction editDistanceDP(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));\n    // 状態遷移:先頭行と先頭列\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.ts
/* 編集距離:動的計画法 */\nfunction editDistanceDP(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: m + 1 }, () => 0)\n    );\n    // 状態遷移:先頭行と先頭列\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.dart
/* 編集距離:動的計画法 */\nint editDistanceDP(String s, String t) {\n  int n = s.length, m = t.length;\n  List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));\n  // 状態遷移:先頭行と先頭列\n  for (int i = 1; i <= n; i++) {\n    dp[i][0] = i;\n  }\n  for (int j = 1; j <= m; j++) {\n    dp[0][j] = j;\n  }\n  // 状態遷移: 残りの行と列\n  for (int i = 1; i <= n; i++) {\n    for (int j = 1; j <= m; j++) {\n      if (s[i - 1] == t[j - 1]) {\n        // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[i][j] = dp[i - 1][j - 1];\n      } else {\n        // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n        dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n      }\n    }\n  }\n  return dp[n][m];\n}\n
edit_distance.rs
/* 編集距離:動的計画法 */\nfn edit_distance_dp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![vec![0; m + 1]; n + 1];\n    // 状態遷移:先頭行と先頭列\n    for i in 1..=n {\n        dp[i][0] = i as i32;\n    }\n    for j in 1..m {\n        dp[0][j] = j as i32;\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1..=n {\n        for j in 1..=m {\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] =\n                    std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    dp[n][m]\n}\n
edit_distance.c
/* 編集距離:動的計画法 */\nint editDistanceDP(char *s, char *t, int n, int m) {\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(m + 1, sizeof(int));\n    }\n    // 状態遷移:先頭行と先頭列\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    int res = dp[n][m];\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
edit_distance.kt
/* 編集距離:動的計画法 */\nfun editDistanceDP(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = Array(n + 1) { IntArray(m + 1) }\n    // 状態遷移:先頭行と先頭列\n    for (i in 1..n) {\n        dp[i][0] = i\n    }\n    for (j in 1..m) {\n        dp[0][j] = j\n    }\n    // 状態遷移: 残りの行と列\n    for (i in 1..n) {\n        for (j in 1..m) {\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.rb
### 編集距離:動的計画法 ###\ndef edit_distance_dp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(n + 1) { Array.new(m + 1, 0) }\n  # 状態遷移:先頭行と先頭列\n  (1...(n + 1)).each { |i| dp[i][0] = i }\n  (1...(m + 1)).each { |j| dp[0][j] = j }\n  # 状態遷移: 残りの行と列\n  for i in 1...(n + 1)\n    for j in 1...(m +1)\n      if s[i - 1] == t[j - 1]\n        # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[i][j] = dp[i - 1][j - 1]\n      else\n        # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n        dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1\n      end\n    end\n  end\n  dp[n][m]\nend\n
コードの可視化

全画面で見る >

下図に示すように、編集距離問題の状態遷移の過程はナップサック問題と非常によく似ており、どちらも2次元グリッドを埋めていく過程とみなせます。

<1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

図 14-30   編集距離の動的計画法の過程

","path":["第 14 章   動的計画法","14.6   編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#3","level":3,"title":"3.   空間最適化","text":"

\\(dp[i,j]\\) は上の \\(dp[i-1, j]\\)、左の \\(dp[i, j-1]\\)、左上の \\(dp[i-1, j-1]\\) から遷移されますが、順方向走査では左上の \\(dp[i-1, j-1]\\) を失い、逆方向走査では \\(dp[i, j-1]\\) を事前に構築できません。そのため、どちらの走査順序も適切ではありません。

そのため、変数 leftup を用いて左上の解 \\(dp[i-1, j-1]\\) を一時保存し、左と上の解だけを考えればよくなります。このときの状況は完全ナップサック問題と同じであり、順方向走査を用いることができます。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
def edit_distance_dp_comp(s: str, t: str) -> int:\n    \"\"\"編集距離:空間最適化した動的計画法\"\"\"\n    n, m = len(s), len(t)\n    dp = [0] * (m + 1)\n    # 状態遷移:先頭行\n    for j in range(1, m + 1):\n        dp[j] = j\n    # 状態遷移:残りの行\n    for i in range(1, n + 1):\n        # 状態遷移:先頭列\n        leftup = dp[0]  # dp[i-1, j-1] を一時保存する\n        dp[0] += 1\n        # 状態遷移:残りの列\n        for j in range(1, m + 1):\n            temp = dp[j]\n            if s[i - 1] == t[j - 1]:\n                # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            else:\n                # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = min(dp[j - 1], dp[j], leftup) + 1\n            leftup = temp  # 次の反復の dp[i-1, j-1] に更新する\n    return dp[m]\n
edit_distance.cpp
/* 編集距離:空間最適化した動的計画法 */\nint editDistanceDPComp(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<int> dp(m + 1, 0);\n    // 状態遷移:先頭行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m];\n}\n
edit_distance.java
/* 編集距離:空間最適化した動的計画法 */\nint editDistanceDPComp(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[] dp = new int[m + 1];\n    // 状態遷移:先頭行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m];\n}\n
edit_distance.cs
/* 編集距離:空間最適化した動的計画法 */\nint EditDistanceDPComp(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[] dp = new int[m + 1];\n    // 状態遷移:先頭行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m];\n}\n
edit_distance.go
/* 編集距離:空間最適化した動的計画法 */\nfunc editDistanceDPComp(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([]int, m+1)\n    // 状態遷移:先頭行\n    for j := 1; j <= m; j++ {\n        dp[j] = j\n    }\n    // 状態遷移:残りの行\n    for i := 1; i <= n; i++ {\n        // 状態遷移:先頭列\n        leftUp := dp[0] // dp[i-1, j-1] を一時保存する\n        dp[0] = i\n        // 状態遷移:残りの列\n        for j := 1; j <= m; j++ {\n            temp := dp[j]\n            if s[i-1] == t[j-1] {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftUp\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1\n            }\n            leftUp = temp // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m]\n}\n
edit_distance.swift
/* 編集距離:空間最適化した動的計画法 */\nfunc editDistanceDPComp(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: 0, count: m + 1)\n    // 状態遷移:先頭行\n    for j in 1 ... m {\n        dp[j] = j\n    }\n    // 状態遷移:残りの行\n    for i in 1 ... n {\n        // 状態遷移:先頭列\n        var leftup = dp[0] // dp[i-1, j-1] を一時保存する\n        dp[0] = i\n        // 状態遷移:残りの列\n        for j in 1 ... m {\n            let temp = dp[j]\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m]\n}\n
edit_distance.js
/* 編集距離:空間最適化した動的計画法 */\nfunction editDistanceDPComp(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // 状態遷移:先頭行\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (let i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        let leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m];\n}\n
edit_distance.ts
/* 編集距離:空間最適化した動的計画法 */\nfunction editDistanceDPComp(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // 状態遷移:先頭行\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (let i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        let leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m];\n}\n
edit_distance.dart
/* 編集距離:空間最適化した動的計画法 */\nint editDistanceDPComp(String s, String t) {\n  int n = s.length, m = t.length;\n  List<int> dp = List.filled(m + 1, 0);\n  // 状態遷移:先頭行\n  for (int j = 1; j <= m; j++) {\n    dp[j] = j;\n  }\n  // 状態遷移:残りの行\n  for (int i = 1; i <= n; i++) {\n    // 状態遷移:先頭列\n    int leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n    dp[0] = i;\n    // 状態遷移:残りの列\n    for (int j = 1; j <= m; j++) {\n      int temp = dp[j];\n      if (s[i - 1] == t[j - 1]) {\n        // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[j] = leftup;\n      } else {\n        // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n        dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n      }\n      leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n    }\n  }\n  return dp[m];\n}\n
edit_distance.rs
/* 編集距離:空間最適化した動的計画法 */\nfn edit_distance_dp_comp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![0; m + 1];\n    // 状態遷移:先頭行\n    for j in 1..m {\n        dp[j] = j as i32;\n    }\n    // 状態遷移:残りの行\n    for i in 1..=n {\n        // 状態遷移:先頭列\n        let mut leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i as i32;\n        // 状態遷移:残りの列\n        for j in 1..=m {\n            let temp = dp[j];\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    dp[m]\n}\n
edit_distance.c
/* 編集距離:空間最適化した動的計画法 */\nint editDistanceDPComp(char *s, char *t, int n, int m) {\n    int *dp = calloc(m + 1, sizeof(int));\n    // 状態遷移:先頭行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    int res = dp[m];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
edit_distance.kt
/* 編集距離:空間最適化した動的計画法 */\nfun editDistanceDPComp(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = IntArray(m + 1)\n    // 状態遷移:先頭行\n    for (j in 1..m) {\n        dp[j] = j\n    }\n    // 状態遷移:残りの行\n    for (i in 1..n) {\n        // 状態遷移:先頭列\n        var leftup = dp[0] // dp[i-1, j-1] を一時保存する\n        dp[0] = i\n        // 状態遷移:残りの列\n        for (j in 1..m) {\n            val temp = dp[j]\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m]\n}\n
edit_distance.rb
### 編集距離:空間最適化した動的計画法 ###\ndef edit_distance_dp_comp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(m + 1, 0)\n  # 状態遷移:先頭行\n  (1...(m + 1)).each { |j| dp[j] = j }\n  # 状態遷移:残りの行\n  for i in 1...(n + 1)\n    # 状態遷移:先頭列\n    leftup = dp.first # dp[i-1, j-1] を一時保存する\n    dp[0] += 1\n    # 状態遷移:残りの列\n    for j in 1...(m + 1)\n      temp = dp[j]\n      if s[i - 1] == t[j - 1]\n        # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[j] = leftup\n      else\n        # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n        dp[j] = [dp[j - 1], dp[j], leftup].min + 1\n      end\n      leftup = temp # 次の反復の dp[i-1, j-1] に更新する\n    end\n  end\n  dp[m]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.6   編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/","level":1,"title":"14.1   動的計画法入門","text":"

動的計画法(dynamic programming)は重要なアルゴリズムパラダイムであり、問題をより小さな部分問題の列に分解し、それらの解を保存して重複計算を避けることで、時間効率を大幅に向上させます。

本節では、古典的な例題から始めて、まずその力任せのバックトラッキング解法を示し、そこに含まれる重複部分問題を観察したうえで、より効率的な動的計画法の解法を段階的に導きます。

階段を上る

全体で \\(n\\) 段ある階段が与えられ、各ステップで \\(1\\) 段または \\(2\\) 段上ることができます。頂上まで到達する方法は何通りあるでしょうか?

次の図に示すように、\\(3\\) 段の階段では、頂上まで到達する方法は全部で \\(3\\) 通りあります。

図 14-1   3 段の階段を上る方法の数

この問題の目的は方法の総数を求めることです。考えられるすべての可能性をバックトラッキングで総当たりすることができます。具体的には、階段を上ることを複数ラウンドの選択過程とみなし、地面から出発して各ラウンドで \\(1\\) 段または \\(2\\) 段上ります。階段の頂上に到達するたびに方法数を \\(1\\) 増やし、頂上を越えた場合は枝刈りします。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_backtrack.py
def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:\n    \"\"\"バックトラッキング\"\"\"\n    # 第 n 段に到達したら、方法数を 1 増やす\n    if state == n:\n        res[0] += 1\n    # すべての選択肢を走査\n    for choice in choices:\n        # 枝刈り: 第 n 段を超えないようにする\n        if state + choice > n:\n            continue\n        # 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res)\n        # バックトラック\n\ndef climbing_stairs_backtrack(n: int) -> int:\n    \"\"\"階段登り:バックトラッキング\"\"\"\n    choices = [1, 2]  # 1 段または 2 段上ることを選べる\n    state = 0  # 第 0 段から上り始める\n    res = [0]  # res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res)\n    return res[0]\n
climbing_stairs_backtrack.cpp
/* バックトラッキング */\nvoid backtrack(vector<int> &choices, int state, int n, vector<int> &res) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state == n)\n        res[0]++;\n    // すべての選択肢を走査\n    for (auto &choice : choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n)\n            continue;\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nint climbingStairsBacktrack(int n) {\n    vector<int> choices = {1, 2}; // 1 段または 2 段上ることを選べる\n    int state = 0;                // 第 0 段から上り始める\n    vector<int> res = {0};        // res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res);\n    return res[0];\n}\n
climbing_stairs_backtrack.java
/* バックトラッキング */\nvoid backtrack(List<Integer> choices, int state, int n, List<Integer> res) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state == n)\n        res.set(0, res.get(0) + 1);\n    // すべての選択肢を走査\n    for (Integer choice : choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n)\n            continue;\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nint climbingStairsBacktrack(int n) {\n    List<Integer> choices = Arrays.asList(1, 2); // 1 段または 2 段上ることを選べる\n    int state = 0; // 第 0 段から上り始める\n    List<Integer> res = new ArrayList<>();\n    res.add(0); // res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.cs
/* バックトラッキング */\nvoid Backtrack(List<int> choices, int state, int n, List<int> res) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state == n)\n        res[0]++;\n    // すべての選択肢を走査\n    foreach (int choice in choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n)\n            continue;\n        // 試行: 選択を行い、状態を更新\n        Backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nint ClimbingStairsBacktrack(int n) {\n    List<int> choices = [1, 2]; // 1 段または 2 段上ることを選べる\n    int state = 0; // 第 0 段から上り始める\n    List<int> res = [0]; // res[0] を使って方法数を記録する\n    Backtrack(choices, state, n, res);\n    return res[0];\n}\n
climbing_stairs_backtrack.go
/* バックトラッキング */\nfunc backtrack(choices []int, state, n int, res []int) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if state == n {\n        res[0] = res[0] + 1\n    }\n    // すべての選択肢を走査\n    for _, choice := range choices {\n        // 枝刈り: 第 n 段を超えないようにする\n        if state+choice > n {\n            continue\n        }\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state+choice, n, res)\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfunc climbingStairsBacktrack(n int) int {\n    // 1 段または 2 段上ることを選べる\n    choices := []int{1, 2}\n    // 第 0 段から上り始める\n    state := 0\n    res := make([]int, 1)\n    // res[0] を使って方法数を記録する\n    res[0] = 0\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
climbing_stairs_backtrack.swift
/* バックトラッキング */\nfunc backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if state == n {\n        res[0] += 1\n    }\n    // すべての選択肢を走査\n    for choice in choices {\n        // 枝刈り: 第 n 段を超えないようにする\n        if state + choice > n {\n            continue\n        }\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices: choices, state: state + choice, n: n, res: &res)\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfunc climbingStairsBacktrack(n: Int) -> Int {\n    let choices = [1, 2] // 1 段または 2 段上ることを選べる\n    let state = 0 // 第 0 段から上り始める\n    var res: [Int] = []\n    res.append(0) // res[0] を使って方法数を記録する\n    backtrack(choices: choices, state: state, n: n, res: &res)\n    return res[0]\n}\n
climbing_stairs_backtrack.js
/* バックトラッキング */\nfunction backtrack(choices, state, n, res) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state === n) res.set(0, res.get(0) + 1);\n    // すべての選択肢を走査\n    for (const choice of choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n) continue;\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfunction climbingStairsBacktrack(n) {\n    const choices = [1, 2]; // 1 段または 2 段上ることを選べる\n    const state = 0; // 第 0 段から上り始める\n    const res = new Map();\n    res.set(0, 0); // res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.ts
/* バックトラッキング */\nfunction backtrack(\n    choices: number[],\n    state: number,\n    n: number,\n    res: Map<0, any>\n): void {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state === n) res.set(0, res.get(0) + 1);\n    // すべての選択肢を走査\n    for (const choice of choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n) continue;\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfunction climbingStairsBacktrack(n: number): number {\n    const choices = [1, 2]; // 1 段または 2 段上ることを選べる\n    const state = 0; // 第 0 段から上り始める\n    const res = new Map();\n    res.set(0, 0); // res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.dart
/* バックトラッキング */\nvoid backtrack(List<int> choices, int state, int n, List<int> res) {\n  // 第 n 段に到達したら、方法数を 1 増やす\n  if (state == n) {\n    res[0]++;\n  }\n  // すべての選択肢を走査\n  for (int choice in choices) {\n    // 枝刈り: 第 n 段を超えないようにする\n    if (state + choice > n) continue;\n    // 試行: 選択を行い、状態を更新\n    backtrack(choices, state + choice, n, res);\n    // バックトラック\n  }\n}\n\n/* 階段登り:バックトラッキング */\nint climbingStairsBacktrack(int n) {\n  List<int> choices = [1, 2]; // 1 段または 2 段上ることを選べる\n  int state = 0; // 第 0 段から上り始める\n  List<int> res = [];\n  res.add(0); // res[0] を使って方法数を記録する\n  backtrack(choices, state, n, res);\n  return res[0];\n}\n
climbing_stairs_backtrack.rs
/* バックトラッキング */\nfn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if state == n {\n        res[0] = res[0] + 1;\n    }\n    // すべての選択肢を走査\n    for &choice in choices {\n        // 枝刈り: 第 n 段を超えないようにする\n        if state + choice > n {\n            continue;\n        }\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfn climbing_stairs_backtrack(n: usize) -> i32 {\n    let choices = vec![1, 2]; // 1 段または 2 段上ることを選べる\n    let state = 0; // 第 0 段から上り始める\n    let mut res = Vec::new();\n    res.push(0); // res[0] を使って方法数を記録する\n    backtrack(&choices, state, n as i32, &mut res);\n    res[0]\n}\n
climbing_stairs_backtrack.c
/* バックトラッキング */\nvoid backtrack(int *choices, int state, int n, int *res, int len) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state == n)\n        res[0]++;\n    // すべての選択肢を走査\n    for (int i = 0; i < len; i++) {\n        int choice = choices[i];\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n)\n            continue;\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res, len);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nint climbingStairsBacktrack(int n) {\n    int choices[2] = {1, 2}; // 1 段または 2 段上ることを選べる\n    int state = 0;           // 第 0 段から上り始める\n    int *res = (int *)malloc(sizeof(int));\n    *res = 0; // res[0] を使って方法数を記録する\n    int len = sizeof(choices) / sizeof(int);\n    backtrack(choices, state, n, res, len);\n    int result = *res;\n    free(res);\n    return result;\n}\n
climbing_stairs_backtrack.kt
/* バックトラッキング */\nfun backtrack(\n    choices: MutableList<Int>,\n    state: Int,\n    n: Int,\n    res: MutableList<Int>\n) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state == n)\n        res[0] = res[0] + 1\n    // すべての選択肢を走査\n    for (choice in choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n) continue\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res)\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfun climbingStairsBacktrack(n: Int): Int {\n    val choices = mutableListOf(1, 2) // 1 段または 2 段上ることを選べる\n    val state = 0 // 第 0 段から上り始める\n    val res = mutableListOf<Int>()\n    res.add(0) // res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
climbing_stairs_backtrack.rb
### バックトラッキング ###\ndef backtrack(choices, state, n, res)\n  # 第 n 段に到達したら、方法数を 1 増やす\n  res[0] += 1 if state == n\n  # すべての選択肢を走査\n  for choice in choices\n    # 枝刈り: 第 n 段を超えないようにする\n    next if state + choice > n\n\n    # 試行: 選択を行い、状態を更新\n    backtrack(choices, state + choice, n, res)\n  end\n  # バックトラック\nend\n\n### 階段登り:バックトラッキング ###\ndef climbing_stairs_backtrack(n)\n  choices = [1, 2] # 1 段または 2 段上ることを選べる\n  state = 0 # 第 0 段から上り始める\n  res = [0] # res[0] を使って方法数を記録する\n  backtrack(choices, state, n, res)\n  res.first\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.1   動的計画法入門"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1411-1","level":2,"title":"14.1.1   方法 1:総当たり探索","text":"

バックトラッキング法は通常、問題を明示的に分解するのではなく、問題解決を一連の意思決定ステップとみなし、試行と枝刈りによってあらゆる可能な解を探索します。

この問題を問題分解の観点から分析してみましょう。\\(i\\) 段目まで上る方法が全部で \\(dp[i]\\) 通りあるとすると、\\(dp[i]\\) が元の問題であり、その部分問題には次が含まれます:

\\[ dp[i-1], dp[i-2], \\dots, dp[2], dp[1] \\]

各ラウンドでは \\(1\\) 段または \\(2\\) 段しか上れないため、\\(i\\) 段目の階段に立っているとき、直前のラウンドでは \\(i - 1\\) 段目または \\(i - 2\\) 段目にしか立てません。言い換えると、\\(i -1\\) 段目または \\(i - 2\\) 段目からしか \\(i\\) 段目へ進めません。

ここから重要な帰結が得られます。**\\(i - 1\\) 段目まで上る方法数と \\(i - 2\\) 段目まで上る方法数の和が、\\(i\\) 段目まで上る方法数に等しい**のです。式は次のとおりです:

\\[ dp[i] = dp[i-1] + dp[i-2] \\]

これは、階段を上る問題では各部分問題の間に漸化関係があり、**元の問題の解は部分問題の解から構築できる**ことを意味します。次の図はこの漸化関係を示しています。

図 14-2   方法数の漸化関係

漸化式に基づいて総当たり探索の解法を得ることができます。\\(dp[n]\\) を出発点とし、**より大きな問題を再帰的に 2 つのより小さな問題の和へ分解**していき、最小部分問題 \\(dp[1]\\) と \\(dp[2]\\) に到達したら返します。ここで最小部分問題の解は既知であり、\\(dp[1] = 1\\)、\\(dp[2] = 2\\) です。これは、第 \\(1\\) 段目と第 \\(2\\) 段目まで上る方法がそれぞれ \\(1\\) 通り、\\(2\\) 通りであることを表します。

次のコードを見ると、標準的なバックトラッキングコードと同じく深さ優先探索に属しますが、より簡潔です:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs.py
def dfs(i: int) -> int:\n    \"\"\"検索\"\"\"\n    # dp[1] と dp[2] は既知なので返す\n    if i == 1 or i == 2:\n        return i\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1) + dfs(i - 2)\n    return count\n\ndef climbing_stairs_dfs(n: int) -> int:\n    \"\"\"階段登り:探索\"\"\"\n    return dfs(n)\n
climbing_stairs_dfs.cpp
/* 検索 */\nint dfs(int i) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.java
/* 検索 */\nint dfs(int i) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.cs
/* 検索 */\nint DFS(int i) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1) + DFS(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nint ClimbingStairsDFS(int n) {\n    return DFS(n);\n}\n
climbing_stairs_dfs.go
/* 検索 */\nfunc dfs(i int) int {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfs(i-1) + dfs(i-2)\n    return count\n}\n\n/* 階段登り:探索 */\nfunc climbingStairsDFS(n int) int {\n    return dfs(n)\n}\n
climbing_stairs_dfs.swift
/* 検索 */\nfunc dfs(i: Int) -> Int {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1) + dfs(i: i - 2)\n    return count\n}\n\n/* 階段登り:探索 */\nfunc climbingStairsDFS(n: Int) -> Int {\n    dfs(i: n)\n}\n
climbing_stairs_dfs.js
/* 検索 */\nfunction dfs(i) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nfunction climbingStairsDFS(n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.ts
/* 検索 */\nfunction dfs(i: number): number {\n    // dp[1] と dp[2] は既知なので返す\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nfunction climbingStairsDFS(n: number): number {\n    return dfs(n);\n}\n
climbing_stairs_dfs.dart
/* 検索 */\nint dfs(int i) {\n  // dp[1] と dp[2] は既知なので返す\n  if (i == 1 || i == 2) return i;\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1) + dfs(i - 2);\n  return count;\n}\n\n/* 階段登り:探索 */\nint climbingStairsDFS(int n) {\n  return dfs(n);\n}\n
climbing_stairs_dfs.rs
/* 検索 */\nfn dfs(i: usize) -> i32 {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1) + dfs(i - 2);\n    count\n}\n\n/* 階段登り:探索 */\nfn climbing_stairs_dfs(n: usize) -> i32 {\n    dfs(n)\n}\n
climbing_stairs_dfs.c
/* 検索 */\nint dfs(int i) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.kt
/* 検索 */\nfun dfs(i: Int): Int {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2) return i\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1) + dfs(i - 2)\n    return count\n}\n\n/* 階段登り:探索 */\nfun climbingStairsDFS(n: Int): Int {\n    return dfs(n)\n}\n
climbing_stairs_dfs.rb
### 探索 ###\ndef dfs(i)\n  # dp[1] と dp[2] は既知なので返す\n  return i if i == 1 || i == 2\n  # dp[i] = dp[i-1] + dp[i-2]\n  dfs(i - 1) + dfs(i - 2)\nend\n\n### 階段登り:探索 ###\ndef climbing_stairs_dfs(n)\n  dfs(n)\nend\n
コードの可視化

全画面で見る >

次の図は総当たり探索によって形成される再帰木を示しています。問題 \\(dp[n]\\) に対して、その再帰木の深さは \\(n\\)、時間計算量は \\(O(2^n)\\) です。指数オーダーは爆発的に増加するため、比較的大きな \\(n\\) を入力すると長時間待たされることになります。

図 14-3   階段上りに対応する再帰木

上の図を見ると、指数オーダーの時間計算量は「重複部分問題」によって生じています。たとえば \\(dp[9]\\) は \\(dp[8]\\) と \\(dp[7]\\) に分解され、\\(dp[8]\\) は \\(dp[7]\\) と \\(dp[6]\\) に分解されるため、どちらにも部分問題 \\(dp[7]\\) が含まれています。

このように、部分問題の中にはさらに小さな重複部分問題が含まれ、それが際限なく続いていきます。計算資源の大部分は、こうした重複部分問題に浪費されています。

","path":["第 14 章   動的計画法","14.1   動的計画法入門"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1412-2","level":2,"title":"14.1.2   方法 2:メモ化探索","text":"

アルゴリズム効率を高めるため、**すべての重複部分問題を 1 回だけ計算したい**と考えます。そのために、各部分問題の解を記録する配列 mem を宣言し、探索の過程で重複部分問題を枝刈りします。

  1. \\(dp[i]\\) を初めて計算したとき、その結果を mem[i] に記録して後で使えるようにします。
  2. 再び \\(dp[i]\\) を計算する必要が生じたときは、mem[i] から直接結果を取得し、その部分問題の重複計算を避けます。

コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs_mem.py
def dfs(i: int, mem: list[int]) -> int:\n    \"\"\"メモ化探索\"\"\"\n    # dp[1] と dp[2] は既知なので返す\n    if i == 1 or i == 2:\n        return i\n    # dp[i] の記録があれば、それをそのまま返す\n    if mem[i] != -1:\n        return mem[i]\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    # dp[i] を記録する\n    mem[i] = count\n    return count\n\ndef climbing_stairs_dfs_mem(n: int) -> int:\n    \"\"\"階段登り:メモ化探索\"\"\"\n    # mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    mem = [-1] * (n + 1)\n    return dfs(n, mem)\n
climbing_stairs_dfs_mem.cpp
/* メモ化探索 */\nint dfs(int i, vector<int> &mem) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nint climbingStairsDFSMem(int n) {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    vector<int> mem(n + 1, -1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.java
/* メモ化探索 */\nint dfs(int i, int[] mem) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nint climbingStairsDFSMem(int n) {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    int[] mem = new int[n + 1];\n    Arrays.fill(mem, -1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.cs
/* メモ化探索 */\nint DFS(int i, int[] mem) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1, mem) + DFS(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nint ClimbingStairsDFSMem(int n) {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    int[] mem = new int[n + 1];\n    Array.Fill(mem, -1);\n    return DFS(n, mem);\n}\n
climbing_stairs_dfs_mem.go
/* メモ化探索 */\nfunc dfsMem(i int, mem []int) int {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] の記録があれば、それをそのまま返す\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfsMem(i-1, mem) + dfsMem(i-2, mem)\n    // dp[i] を記録する\n    mem[i] = count\n    return count\n}\n\n/* 階段登り:メモ化探索 */\nfunc climbingStairsDFSMem(n int) int {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    mem := make([]int, n+1)\n    for i := range mem {\n        mem[i] = -1\n    }\n    return dfsMem(n, mem)\n}\n
climbing_stairs_dfs_mem.swift
/* メモ化探索 */\nfunc dfs(i: Int, mem: inout [Int]) -> Int {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] の記録があれば、それをそのまま返す\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1, mem: &mem) + dfs(i: i - 2, mem: &mem)\n    // dp[i] を記録する\n    mem[i] = count\n    return count\n}\n\n/* 階段登り:メモ化探索 */\nfunc climbingStairsDFSMem(n: Int) -> Int {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    var mem = Array(repeating: -1, count: n + 1)\n    return dfs(i: n, mem: &mem)\n}\n
climbing_stairs_dfs_mem.js
/* メモ化探索 */\nfunction dfs(i, mem) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i === 1 || i === 2) return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nfunction climbingStairsDFSMem(n) {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.ts
/* メモ化探索 */\nfunction dfs(i: number, mem: number[]): number {\n    // dp[1] と dp[2] は既知なので返す\n    if (i === 1 || i === 2) return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nfunction climbingStairsDFSMem(n: number): number {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.dart
/* メモ化探索 */\nint dfs(int i, List<int> mem) {\n  // dp[1] と dp[2] は既知なので返す\n  if (i == 1 || i == 2) return i;\n  // dp[i] の記録があれば、それをそのまま返す\n  if (mem[i] != -1) return mem[i];\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n  // dp[i] を記録する\n  mem[i] = count;\n  return count;\n}\n\n/* 階段登り:メモ化探索 */\nint climbingStairsDFSMem(int n) {\n  // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n  List<int> mem = List.filled(n + 1, -1);\n  return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.rs
/* メモ化探索 */\nfn dfs(i: usize, mem: &mut [i32]) -> i32 {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // dp[i] の記録があれば、それをそのまま返す\n    if mem[i] != -1 {\n        return mem[i];\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    count\n}\n\n/* 階段登り:メモ化探索 */\nfn climbing_stairs_dfs_mem(n: usize) -> i32 {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    let mut mem = vec![-1; n + 1];\n    dfs(n, &mut mem)\n}\n
climbing_stairs_dfs_mem.c
/* メモ化探索 */\nint dfs(int i, int *mem) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nint climbingStairsDFSMem(int n) {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    int *mem = (int *)malloc((n + 1) * sizeof(int));\n    for (int i = 0; i <= n; i++) {\n        mem[i] = -1;\n    }\n    int result = dfs(n, mem);\n    free(mem);\n    return result;\n}\n
climbing_stairs_dfs_mem.kt
/* メモ化探索 */\nfun dfs(i: Int, mem: IntArray): Int {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2) return i\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1) return mem[i]\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    // dp[i] を記録する\n    mem[i] = count\n    return count\n}\n\n/* 階段登り:メモ化探索 */\nfun climbingStairsDFSMem(n: Int): Int {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    val mem = IntArray(n + 1)\n    mem.fill(-1)\n    return dfs(n, mem)\n}\n
climbing_stairs_dfs_mem.rb
### メモ化探索 ###\ndef dfs(i, mem)\n  # dp[1] と dp[2] は既知なので返す\n  return i if i == 1 || i == 2\n  # dp[i] の記録があれば、それをそのまま返す\n  return mem[i] if mem[i] != -1\n\n  # dp[i] = dp[i-1] + dp[i-2]\n  count = dfs(i - 1, mem) + dfs(i - 2, mem)\n  # dp[i] を記録する\n  mem[i] = count\nend\n\n### 階段登り:メモ化探索 ###\ndef climbing_stairs_dfs_mem(n)\n  # mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n  mem = Array.new(n + 1, -1)\n  dfs(n, mem)\nend\n
コードの可視化

全画面で見る >

次の図を見ると、メモ化を行うことで、すべての重複部分問題は 1 回だけ計算すればよくなり、時間計算量は \\(O(n)\\) まで改善されます。これは大きな飛躍です。

図 14-4   メモ化探索に対応する再帰木

","path":["第 14 章   動的計画法","14.1   動的計画法入門"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1413-3","level":2,"title":"14.1.3   方法 3:動的計画法","text":"

**メモ化探索は「トップダウン」の方法**です。元の問題(根ノード)から始めて、より大きな部分問題を再帰的により小さな部分問題へ分解し、解が既知である最小部分問題(葉ノード)に至ります。その後、バックトラックしながら各層で部分問題の解を集め、元の問題の解を構築します。

これとは対照的に、**動的計画法は「ボトムアップ」の方法**です。最小部分問題の解から始めて、より大きな部分問題の解を反復的に構築し、最終的に元の問題の解を得ます。

動的計画法にはバックトラックの過程が含まれないため、再帰を使う必要はなく、ループによる反復だけで実装できます。次のコードでは、部分問題の解を保存する配列 dp を初期化しており、これはメモ化探索における配列 mem と同じ記録の役割を果たします:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
def climbing_stairs_dp(n: int) -> int:\n    \"\"\"階段登り:動的計画法\"\"\"\n    if n == 1 or n == 2:\n        return n\n    # 部分問題の解を保存するために dp テーブルを初期化\n    dp = [0] * (n + 1)\n    # 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1], dp[2] = 1, 2\n    # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in range(3, n + 1):\n        dp[i] = dp[i - 1] + dp[i - 2]\n    return dp[n]\n
climbing_stairs_dp.cpp
/* 階段登り:動的計画法 */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    vector<int> dp(n + 1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.java
/* 階段登り:動的計画法 */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[] dp = new int[n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.cs
/* 階段登り:動的計画法 */\nint ClimbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[] dp = new int[n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.go
/* 階段登り:動的計画法 */\nfunc climbingStairsDP(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    dp := make([]int, n+1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1\n    dp[2] = 2\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i := 3; i <= n; i++ {\n        dp[i] = dp[i-1] + dp[i-2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.swift
/* 階段登り:動的計画法 */\nfunc climbingStairsDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    var dp = Array(repeating: 0, count: n + 1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1\n    dp[2] = 2\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3 ... n {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.js
/* 階段登り:動的計画法 */\nfunction climbingStairsDP(n) {\n    if (n === 1 || n === 2) return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = new Array(n + 1).fill(-1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.ts
/* 階段登り:動的計画法 */\nfunction climbingStairsDP(n: number): number {\n    if (n === 1 || n === 2) return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = new Array(n + 1).fill(-1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.dart
/* 階段登り:動的計画法 */\nint climbingStairsDP(int n) {\n  if (n == 1 || n == 2) return n;\n  // 部分問題の解を保存するために dp テーブルを初期化\n  List<int> dp = List.filled(n + 1, 0);\n  // 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1] = 1;\n  dp[2] = 2;\n  // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  for (int i = 3; i <= n; i++) {\n    dp[i] = dp[i - 1] + dp[i - 2];\n  }\n  return dp[n];\n}\n
climbing_stairs_dp.rs
/* 階段登り:動的計画法 */\nfn climbing_stairs_dp(n: usize) -> i32 {\n    // dp[1] と dp[2] は既知なので返す\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    let mut dp = vec![-1; n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3..=n {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    dp[n]\n}\n
climbing_stairs_dp.c
/* 階段登り:動的計画法 */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int *dp = (int *)malloc((n + 1) * sizeof(int));\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    int result = dp[n];\n    free(dp);\n    return result;\n}\n
climbing_stairs_dp.kt
/* 階段登り:動的計画法 */\nfun climbingStairsDP(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    // 部分問題の解を保存するために dp テーブルを初期化\n    val dp = IntArray(n + 1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1\n    dp[2] = 2\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (i in 3..n) {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.rb
### 階段登り:動的計画法 ###\ndef climbing_stairs_dp(n)\n  return n  if n == 1 || n == 2\n\n  # 部分問題の解を保存するために dp テーブルを初期化\n  dp = Array.new(n + 1, 0)\n  # 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1], dp[2] = 1, 2\n  # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] }\n\n  dp[n]\nend\n
コードの可視化

全画面で見る >

次の図は、以上のコードの実行過程をシミュレートしたものです。

図 14-5   階段上りの動的計画法の過程

バックトラッキング法と同様に、動的計画法でも問題解決の特定段階を表すために「状態」という概念を用います。各状態は 1 つの部分問題と、それに対応する局所最適解に対応します。たとえば、階段を上る問題では、状態は現在いる階段の段数 \\(i\\) と定義されます。

以上を踏まえると、動的計画法のよく使われる用語を次のようにまとめられます。

  • 配列 dp を dp テーブル と呼び、\\(dp[i]\\) は状態 \\(i\\) に対応する部分問題の解を表します。
  • 最小部分問題に対応する状態(第 \\(1\\) 段目と第 \\(2\\) 段目の階段)を初期状態と呼びます。
  • 漸化式 \\(dp[i] = dp[i-1] + dp[i-2]\\) を状態遷移方程式と呼びます。
","path":["第 14 章   動的計画法","14.1   動的計画法入門"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1414","level":2,"title":"14.1.4   空間最適化","text":"

注意深い読者は気づいたかもしれません。\\(dp[i]\\) は \\(dp[i-1]\\) と \\(dp[i-2]\\) にしか依存しないため、すべての部分問題の解を保存するために配列 dp を使う必要はありません。2 つの変数を順に更新していくだけで十分です。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
def climbing_stairs_dp_comp(n: int) -> int:\n    \"\"\"階段登り:空間最適化した動的計画法\"\"\"\n    if n == 1 or n == 2:\n        return n\n    a, b = 1, 2\n    for _ in range(3, n + 1):\n        a, b = b, a + b\n    return b\n
climbing_stairs_dp.cpp
/* 階段登り:空間最適化した動的計画法 */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.java
/* 階段登り:空間最適化した動的計画法 */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.cs
/* 階段登り:空間最適化した動的計画法 */\nint ClimbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.go
/* 階段登り:空間最適化した動的計画法 */\nfunc climbingStairsDPComp(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    a, b := 1, 2\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i := 3; i <= n; i++ {\n        a, b = b, a+b\n    }\n    return b\n}\n
climbing_stairs_dp.swift
/* 階段登り:空間最適化した動的計画法 */\nfunc climbingStairsDPComp(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    var a = 1\n    var b = 2\n    for _ in 3 ... n {\n        (a, b) = (b, a + b)\n    }\n    return b\n}\n
climbing_stairs_dp.js
/* 階段登り:空間最適化した動的計画法 */\nfunction climbingStairsDPComp(n) {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.ts
/* 階段登り:空間最適化した動的計画法 */\nfunction climbingStairsDPComp(n: number): number {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.dart
/* 階段登り:空間最適化した動的計画法 */\nint climbingStairsDPComp(int n) {\n  if (n == 1 || n == 2) return n;\n  int a = 1, b = 2;\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = a + b;\n    a = tmp;\n  }\n  return b;\n}\n
climbing_stairs_dp.rs
/* 階段登り:空間最適化した動的計画法 */\nfn climbing_stairs_dp_comp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    let (mut a, mut b) = (1, 2);\n    for _ in 3..=n {\n        let tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    b\n}\n
climbing_stairs_dp.c
/* 階段登り:空間最適化した動的計画法 */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.kt
/* 階段登り:空間最適化した動的計画法 */\nfun climbingStairsDPComp(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    var a = 1\n    var b = 2\n    for (i in 3..n) {\n        val temp = b\n        b += a\n        a = temp\n    }\n    return b\n}\n
climbing_stairs_dp.rb
### 階段登り:空間最適化後の動的計画法 ###\ndef climbing_stairs_dp_comp(n)\n  return n if n == 1 || n == 2\n\n  a, b = 1, 2\n  (3...(n + 1)).each { a, b = b, a + b }\n\n  b\nend\n
コードの可視化

全画面で見る >

上のコードを見ると、配列 dp が占めていた領域を省けるため、空間計算量は \\(O(n)\\) から \\(O(1)\\) へと下がります。

動的計画法の問題では、現在の状態はしばしば直前の限られた個数の状態にしか関係しません。このような場合は、必要な状態だけを保持し、「次元削減」によってメモリ空間を節約できます。この空間最適化の技巧は「ローリング変数」または「ローリング配列」と呼ばれます。

","path":["第 14 章   動的計画法","14.1   動的計画法入門"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/","level":1,"title":"14.4   0-1 ナップサック問題","text":"

ナップサック問題は、動的計画法の入門として非常に適した問題であり、動的計画法で最もよく見られる問題形式の1つです。これには 0-1 ナップサック問題、完全ナップサック問題、多重ナップサック問題など、多くの派生があります。

本節では、まず最も一般的な 0-1 ナップサック問題を解いていきます。

Question

\\(n\\) 個の品物が与えられ、\\(i\\) 番目の品物の重さは \\(wgt[i-1]\\)、価値は \\(val[i-1]\\) であり、容量 \\(cap\\) のナップサックがあります。各品物は1回しか選べないとき、ナップサック容量の制約下で入れられる品物の最大価値を求めてください。

以下の図を見てみましょう。品物番号 \\(i\\) は \\(1\\) から始まり、配列のインデックスは \\(0\\) から始まるため、品物 \\(i\\) は重さ \\(wgt[i-1]\\)、価値 \\(val[i-1]\\) に対応します。

図 14-17   0-1 ナップサックのサンプルデータ

0-1 ナップサック問題は、\\(n\\) 回の意思決定からなる過程とみなせます。各品物について「入れない」「入れる」という2つの選択肢があるため、この問題は決定木モデルを満たします。

この問題の目的は「ナップサック容量の制約下で入れられる品物の最大価値」を求めることなので、動的計画法の問題である可能性が高いです。

ステップ1:各ラウンドの選択を考え、状態を定義して、\\(dp\\) テーブルを得る

各品物について、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。ここから状態を、現在の品物番号 \\(i\\) とナップサック容量 \\(c\\) として定義し、\\([i, c]\\) と表せます。

状態 \\([i, c]\\) に対応する部分問題は、先頭 \\(i\\) 個の品物を容量 \\(c\\) のナップサックに入れるときの最大価値 であり、これを \\(dp[i, c]\\) と記します。

求めるべきものは \\(dp[n, cap]\\) なので、サイズ \\((n+1) \\times (cap+1)\\) の2次元 \\(dp\\) テーブルが必要です。

ステップ2:最適部分構造を見つけ、状態遷移方程式を導く

品物 \\(i\\) に対する選択を行った後に残るのは、先頭 \\(i-1\\) 個の品物に対する部分問題であり、次の2つのケースに分けられます。

  • 品物 \\(i\\) を入れない :ナップサック容量は変わらず、状態は \\([i-1, c]\\) に変化します。
  • 品物 \\(i\\) を入れる :ナップサック容量は \\(wgt[i-1]\\) だけ減少し、価値は \\(val[i-1]\\) だけ増加して、状態は \\([i-1, c-wgt[i-1]]\\) に変化します。

以上の分析から、この問題の最適部分構造が分かります。すなわち、最大価値 \\(dp[i, c]\\) は、品物 \\(i\\) を入れない場合と入れる場合のうち、より価値の大きい方に等しい ということです。これにより、次の状態遷移方程式を導けます。

\\[ dp[i, c] = \\max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \\]

注意すべき点として、現在の品物の重さ \\(wgt[i - 1]\\) が残りのナップサック容量 \\(c\\) を超える場合は、入れない選択しかできません。

ステップ3:境界条件と状態遷移の順序を決める

品物がない場合、またはナップサック容量が \\(0\\) の場合、最大価値は \\(0\\) です。すなわち、先頭列 \\(dp[i, 0]\\) と先頭行 \\(dp[0, c]\\) はいずれも \\(0\\) になります。

現在の状態 \\([i, c]\\) は、上側の状態 \\([i-1, c]\\) と左上の状態 \\([i-1, c-wgt[i-1]]\\) から遷移してくるため、2重ループで \\(dp\\) テーブル全体を順方向に走査すれば十分です。

以上の分析に基づき、次に全探索、メモ化探索、動的計画法の順で実装していきます。

","path":["第 14 章   動的計画法","14.4   0-1 ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#1-1","level":3,"title":"1.   方法1:全探索","text":"

探索コードには次の要素が含まれます。

  • 再帰引数:状態 \\([i, c]\\) です。
  • 戻り値:部分問題の解 \\(dp[i, c]\\) です。
  • 終了条件:品物番号が範囲外である \\(i = 0\\)、またはナップサックの残り容量が \\(0\\) のとき、再帰を終了して価値 \\(0\\) を返します。
  • 枝刈り:現在の品物の重さがナップサックの残り容量を超える場合、入れない選択しかできません。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:\n    \"\"\"0-1 ナップサック:総当たり探索\"\"\"\n    # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 or c == 0:\n        return 0\n    # ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c:\n        return knapsack_dfs(wgt, val, i - 1, c)\n    # 品物 i を入れない場合と入れる場合の最大価値を計算する\n    no = knapsack_dfs(wgt, val, i - 1, c)\n    yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # 2つの案のうち価値が大きいほうを返す\n    return max(no, yes)\n
knapsack.cpp
/* 0-1 ナップサック:総当たり探索 */\nint knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return max(no, yes);\n}\n
knapsack.java
/* 0-1 ナップサック:総当たり探索 */\nint knapsackDFS(int[] wgt, int[] val, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return Math.max(no, yes);\n}\n
knapsack.cs
/* 0-1 ナップサック:総当たり探索 */\nint KnapsackDFS(int[] weight, int[] val, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (weight[i - 1] > c) {\n        return KnapsackDFS(weight, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = KnapsackDFS(weight, val, i - 1, c);\n    int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return Math.Max(no, yes);\n}\n
knapsack.go
/* 0-1 ナップサック:総当たり探索 */\nfunc knapsackDFS(wgt, val []int, i, c int) int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i-1] > c {\n        return knapsackDFS(wgt, val, i-1, c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    no := knapsackDFS(wgt, val, i-1, c)\n    yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1]\n    // 2つの案のうち価値が大きいほうを返す\n    return int(math.Max(float64(no), float64(yes)))\n}\n
knapsack.swift
/* 0-1 ナップサック:総当たり探索 */\nfunc knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c {\n        return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    let no = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    let yes = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // 2つの案のうち価値が大きいほうを返す\n    return max(no, yes)\n}\n
knapsack.js
/* 0-1 ナップサック:総当たり探索 */\nfunction knapsackDFS(wgt, val, i, c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return Math.max(no, yes);\n}\n
knapsack.ts
/* 0-1 ナップサック:総当たり探索 */\nfunction knapsackDFS(\n    wgt: Array<number>,\n    val: Array<number>,\n    i: number,\n    c: number\n): number {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return Math.max(no, yes);\n}\n
knapsack.dart
/* 0-1 ナップサック:総当たり探索 */\nint knapsackDFS(List<int> wgt, List<int> val, int i, int c) {\n  // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // ナップサック容量を超える場合は、入れない選択しかできない\n  if (wgt[i - 1] > c) {\n    return knapsackDFS(wgt, val, i - 1, c);\n  }\n  // 品物 i を入れない場合と入れる場合の最大価値を計算する\n  int no = knapsackDFS(wgt, val, i - 1, c);\n  int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // 2つの案のうち価値が大きいほうを返す\n  return max(no, yes);\n}\n
knapsack.rs
/* 0-1 ナップサック:総当たり探索 */\nfn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    let no = knapsack_dfs(wgt, val, i - 1, c);\n    let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    std::cmp::max(no, yes)\n}\n
knapsack.c
/* 0-1 ナップサック:総当たり探索 */\nint knapsackDFS(int wgt[], int val[], int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return myMax(no, yes);\n}\n
knapsack.kt
/* 0-1 ナップサック:総当たり探索 */\nfun knapsackDFS(\n    wgt: IntArray,\n    _val: IntArray,\n    i: Int,\n    c: Int\n): Int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, _val, i - 1, c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    val no = knapsackDFS(wgt, _val, i - 1, c)\n    val yes = knapsackDFS(wgt, _val, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // 2つの案のうち価値が大きいほうを返す\n    return max(no, yes)\n}\n
knapsack.rb
### 0-1 ナップサック: 全探索 ###\ndef knapsack_dfs(wgt, val, i, c)\n  # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n  return 0 if i == 0 || c == 0\n  # ナップサック容量を超える場合は、入れない選択しかできない\n  return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c\n  # 品物 i を入れない場合と入れる場合の最大価値を計算する\n  no = knapsack_dfs(wgt, val, i - 1, c)\n  yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # 2つの案のうち価値が大きいほうを返す\n  [no, yes].max\nend\n
コードの可視化

全画面で見る >

以下の図のように、各品物ごとに「選ばない」「選ぶ」の2つの探索分岐が生じるため、時間計算量は \\(O(2^n)\\) です。

再帰木を観察すると、\\(dp[1, 10]\\) などの重複部分問題が存在することが分かります。品物数が多く、ナップサック容量が大きく、特に同じ重さの品物が多い場合には、重複部分問題の数は大幅に増加します。

図 14-18   0-1 ナップサック問題の全探索の再帰木

","path":["第 14 章   動的計画法","14.4   0-1 ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#2-2","level":3,"title":"2.   方法2:メモ化探索","text":"

重複部分問題が一度だけ計算されるようにするため、メモ配列 mem を用いて部分問題の解を記録します。ここで mem[i][c] は \\(dp[i, c]\\) に対応します。

メモ化を導入すると、時間計算量は部分問題の数に依存し、すなわち \\(O(n \\times cap)\\) になります。実装コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dfs_mem(\n    wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int\n) -> int:\n    \"\"\"0-1 ナップサック:メモ化探索\"\"\"\n    # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 or c == 0:\n        return 0\n    # 既に記録があればそのまま返す\n    if mem[i][c] != -1:\n        return mem[i][c]\n    # ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c:\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    # 品物 i を入れない場合と入れる場合の最大価値を計算する\n    no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n
knapsack.cpp
/* 0-1 ナップサック:メモ化探索 */\nint knapsackDFSMem(vector<int> &wgt, vector<int> &val, vector<vector<int>> &mem, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = max(no, yes);\n    return mem[i][c];\n}\n
knapsack.java
/* 0-1 ナップサック:メモ化探索 */\nint knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.cs
/* 0-1 ナップサック:メモ化探索 */\nint KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (weight[i - 1] > c) {\n        return KnapsackDFSMem(weight, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = KnapsackDFSMem(weight, val, mem, i - 1, c);\n    int yes = KnapsackDFSMem(weight, val, mem, i - 1, c - weight[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = Math.Max(no, yes);\n    return mem[i][c];\n}\n
knapsack.go
/* 0-1 ナップサック:メモ化探索 */\nfunc knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // 既に記録があればそのまま返す\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i-1] > c {\n        return knapsackDFSMem(wgt, val, mem, i-1, c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    no := knapsackDFSMem(wgt, val, mem, i-1, c)\n    yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1]\n    // 2つの案のうち価値が大きいほうを返す\n    mem[i][c] = int(math.Max(float64(no), float64(yes)))\n    return mem[i][c]\n}\n
knapsack.swift
/* 0-1 ナップサック:メモ化探索 */\nfunc knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // 既に記録があればそのまま返す\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c {\n        return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    let no = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    let yes = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
knapsack.js
/* 0-1 ナップサック:メモ化探索 */\nfunction knapsackDFSMem(wgt, val, mem, i, c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.ts
/* 0-1 ナップサック:メモ化探索 */\nfunction knapsackDFSMem(\n    wgt: Array<number>,\n    val: Array<number>,\n    mem: Array<Array<number>>,\n    i: number,\n    c: number\n): number {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.dart
/* 0-1 ナップサック:メモ化探索 */\nint knapsackDFSMem(\n  List<int> wgt,\n  List<int> val,\n  List<List<int>> mem,\n  int i,\n  int c,\n) {\n  // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // 既に記録があればそのまま返す\n  if (mem[i][c] != -1) {\n    return mem[i][c];\n  }\n  // ナップサック容量を超える場合は、入れない選択しかできない\n  if (wgt[i - 1] > c) {\n    return knapsackDFSMem(wgt, val, mem, i - 1, c);\n  }\n  // 品物 i を入れない場合と入れる場合の最大価値を計算する\n  int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n  int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // 2 つの案のうち価値が大きい方を記録して返す\n  mem[i][c] = max(no, yes);\n  return mem[i][c];\n}\n
knapsack.rs
/* 0-1 ナップサック:メモ化探索 */\nfn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec<Vec<i32>>, i: usize, c: usize) -> i32 {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if mem[i][c] != -1 {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = std::cmp::max(no, yes);\n    mem[i][c]\n}\n
knapsack.c
/* 0-1 ナップサック:メモ化探索 */\nint knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = myMax(no, yes);\n    return mem[i][c];\n}\n
knapsack.kt
/* 0-1 ナップサック:メモ化探索 */\nfun knapsackDFSMem(\n    wgt: IntArray,\n    _val: IntArray,\n    mem: Array<IntArray>,\n    i: Int,\n    c: Int\n): Int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] != -1) {\n        return mem[i][c]\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    val no = knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    val yes = knapsackDFSMem(wgt, _val, mem, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
knapsack.rb
### 0-1 ナップサック: メモ化探索 ###\ndef knapsack_dfs_mem(wgt, val, mem, i, c)\n  # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n  return 0 if i == 0 || c == 0\n  # 既に記録があればそのまま返す\n  return mem[i][c] if mem[i][c] != -1\n  # ナップサック容量を超える場合は、入れない選択しかできない\n  return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c\n  # 品物 i を入れない場合と入れる場合の最大価値を計算する\n  no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n  yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # 2 つの案のうち価値が大きい方を記録して返す\n  mem[i][c] = [no, yes].max\nend\n
コードの可視化

全画面で見る >

次の図は、メモ化探索で剪定された探索分岐を示しています。

図 14-19   0-1 ナップサック問題のメモ化探索の再帰木

","path":["第 14 章   動的計画法","14.4   0-1 ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#3-3","level":3,"title":"3.   方法3:動的計画法","text":"

動的計画法の本質は、状態遷移に従って \\(dp\\) テーブルを埋めていく過程です。コードは次のようになります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 ナップサック:動的計画法\"\"\"\n    n = len(wgt)\n    # dp テーブルを初期化\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # 状態遷移\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
knapsack.cpp
/* 0-1 ナップサック:動的計画法 */\nint knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // dp テーブルを初期化\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.java
/* 0-1 ナップサック:動的計画法 */\nint knapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // dp テーブルを初期化\n    int[][] dp = new int[n + 1][cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.cs
/* 0-1 ナップサック:動的計画法 */\nint KnapsackDP(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // dp テーブルを初期化\n    int[,] dp = new int[n + 1, cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (weight[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i, c] = Math.Max(dp[i - 1, c - weight[i - 1]] + val[i - 1], dp[i - 1, c]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
knapsack.go
/* 0-1 ナップサック:動的計画法 */\nfunc knapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // dp テーブルを初期化\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i-1][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.swift
/* 0-1 ナップサック:動的計画法 */\nfunc knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // 状態遷移\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.js
/* 0-1 ナップサック:動的計画法 */\nfunction knapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array(n + 1)\n        .fill(0)\n        .map(() => Array(cap + 1).fill(0));\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.ts
/* 0-1 ナップサック:動的計画法 */\nfunction knapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.dart
/* 0-1 ナップサック:動的計画法 */\nint knapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // dp テーブルを初期化\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // ナップサック容量を超えるなら品物 i は選ばない\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
knapsack.rs
/* 0-1 ナップサック:動的計画法 */\nfn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // dp テーブルを初期化\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // 状態遷移\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = std::cmp::max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1],\n                );\n            }\n        }\n    }\n    dp[n][cap]\n}\n
knapsack.c
/* 0-1 ナップサック:動的計画法 */\nint knapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // dp テーブルを初期化\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
knapsack.kt
/* 0-1 ナップサック:動的計画法 */\nfun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // dp テーブルを初期化\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // 状態遷移\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.rb
### 0-1 ナップサック: 動的計画法 ###\ndef knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # 状態遷移\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # ナップサック容量を超えるなら品物 i は選ばない\n        dp[i][c] = dp[i - 1][c]\n      else\n        # 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[i][c] = [dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
コードの可視化

全画面で見る >

以下の図のように、時間計算量と空間計算量はいずれも配列 dp のサイズによって決まり、\\(O(n \\times cap)\\) です。

<1><2><3><4><5><6><7><8><9><10><11><12><13><14>

図 14-20   0-1 ナップサック問題の動的計画法の過程

","path":["第 14 章   動的計画法","14.4   0-1 ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#4","level":3,"title":"4.   空間最適化","text":"

各状態は直前の行の状態にしか依存しないため、2つの配列をローテーションして用いることで、空間計算量を \\(O(n^2)\\) から \\(O(n)\\) に削減できます。

さらに考えると、1つの配列だけで空間最適化を実現できるでしょうか。観察すると、各状態は真上または左上のマスから遷移してきます。配列が1つしかないと仮定すると、\\(i\\) 行目の走査を開始した時点では、その配列にはまだ \\(i-1\\) 行目の状態が格納されています。

  • 順方向に走査すると、\\(dp[i, j]\\) に到達した時点で、左上にある \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\) の値がすでに上書きされている可能性があり、正しい状態遷移結果を得られません。
  • 逆方向に走査すれば、上書きの問題は発生せず、状態遷移を正しく行えます。

次の図は、単一配列のもとで \\(i = 1\\) 行目から \\(i = 2\\) 行目へ変換する過程を示しています。順方向走査と逆方向走査の違いを考えてみてください。

<1><2><3><4><5><6>

図 14-21   0-1 ナップサックの空間最適化後の動的計画法の過程

コード実装では、配列 dp の第1次元 \\(i\\) をそのまま削除し、内側のループを逆方向走査に変更するだけで済みます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 ナップサック:空間最適化後の動的計画法\"\"\"\n    n = len(wgt)\n    # dp テーブルを初期化\n    dp = [0] * (cap + 1)\n    # 状態遷移\n    for i in range(1, n + 1):\n        # 逆順に走査する\n        for c in range(cap, 0, -1):\n            if wgt[i - 1] > c:\n                # ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c]\n            else:\n                # 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
knapsack.cpp
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nint knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // dp テーブルを初期化\n    vector<int> dp(cap + 1, 0);\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.java
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nint knapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // dp テーブルを初期化\n    int[] dp = new int[cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.cs
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nint KnapsackDPComp(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // dp テーブルを初期化\n    int[] dp = new int[cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (int c = cap; c > 0; c--) {\n            if (weight[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.go
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfunc knapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // dp テーブルを初期化\n    dp := make([]int, cap+1)\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        // 逆順に走査する\n        for c := cap; c >= 1; c-- {\n            if wgt[i-1] <= c {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.swift
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfunc knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: 0, count: cap + 1)\n    // 状態遷移\n    for i in 1 ... n {\n        // 逆順に走査する\n        for c in (1 ... cap).reversed() {\n            if wgt[i - 1] <= c {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.js
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfunction knapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array(cap + 1).fill(0);\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.ts
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfunction knapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array(cap + 1).fill(0);\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.dart
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nint knapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // dp テーブルを初期化\n  List<int> dp = List.filled(cap + 1, 0);\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    // 逆順に走査する\n    for (int c = cap; c >= 1; c--) {\n      if (wgt[i - 1] <= c) {\n        // 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
knapsack.rs
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // dp テーブルを初期化\n    let mut dp = vec![0; cap + 1];\n    // 状態遷移\n    for i in 1..=n {\n        // 逆順に走査する\n        for c in (1..=cap).rev() {\n            if wgt[i - 1] <= c as i32 {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
knapsack.c
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nint knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // dp テーブルを初期化\n    int *dp = calloc(cap + 1, sizeof(int));\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
knapsack.kt
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // dp テーブルを初期化\n    val dp = IntArray(cap + 1)\n    // 状態遷移\n    for (i in 1..n) {\n        // 逆順に走査する\n        for (c in cap downTo 1) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.rb
### 0-1 ナップサック: 空間最適化後の動的計画法 ###\ndef knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # dp テーブルを初期化\n  dp = Array.new(cap + 1, 0)\n  # 状態遷移\n  for i in 1...(n + 1)\n    # 逆順に走査する\n    for c in cap.downto(1)\n      if wgt[i - 1] > c\n        # ナップサック容量を超えるなら品物 i は選ばない\n        dp[c] = dp[c]\n      else\n        # 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.4   0-1 ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/summary/","level":1,"title":"14.7   まとめ","text":"","path":["第 14 章   動的計画法","14.7   まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • 動的計画法は問題を分解し、部分問題の解を保存することで重複計算を避け、計算効率を高めます。
  • 時間を考慮しなければ、すべての動的計画法の問題はバックトラッキング(総当たり探索)で解けますが、再帰木には大量の重複部分問題が存在するため、効率はきわめて低くなります。メモ化配列を導入すると、計算済みのすべての部分問題の解を保存でき、重複部分問題が 1 回だけ計算されることを保証できます。
  • メモ化探索はトップダウンの再帰的解法であり、それに対応する動的計画法はボトムアップの漸化式による解法で、ちょうど「表を埋める」ようなものです。現在の状態は一部の局所状態にのみ依存するため、\\(dp\\) 表の 1 次元を削減して空間計算量を下げることができます。
  • 部分問題への分解は汎用的なアルゴリズムの考え方であり、分割統治、動的計画法、バックトラッキングではそれぞれ異なる性質を持ちます。
  • 動的計画法の問題には 3 つの大きな特徴があります。重複部分問題、最適部分構造、無後効性です。
  • 元の問題の最適解が部分問題の最適解から構築できるなら、その問題は最適部分構造を持ちます。
  • 無後効性とは、ある状態の将来の発展がその状態のみに関係し、過去に経たすべての状態とは無関係であることを指します。多くの組合せ最適化問題は無後効性を持たず、動的計画法で高速に解くことはできません。

ナップサック問題

  • ナップサック問題は最も典型的な動的計画法の問題の 1 つであり、0-1 ナップサック、完全ナップサック、多重ナップサックなどの派生があります。
  • 0-1 ナップサックの状態は、容量 \\(c\\) のナップサックに対して、前 \\(i\\) 個の品物で得られる最大価値として定義されます。ナップサックに入れない場合と入れる場合の 2 つの判断から最適部分構造を得て、状態遷移方程式を構築できます。空間最適化では、各状態が真上と左上の状態に依存するため、左上の状態が上書きされるのを避けるために配列を逆順に走査する必要があります。
  • 完全ナップサック問題では各品物の選択数に制限がないため、品物を入れる場合の状態遷移は 0-1 ナップサック問題とは異なります。状態は真上と真左の状態に依存するので、空間最適化では順方向に走査するべきです。
  • コイン両替問題は完全ナップサック問題の変種です。「最大」価値を求める問題から「最小」の硬貨枚数を求める問題へ変わるため、状態遷移方程式の \\(\\max()\\) は \\(\\min()\\) に置き換える必要があります。また、ナップサック容量を「超えない」ことを目指すのではなく、目標金額を「ちょうど」作ることを目指すため、\\(amt + 1\\) を「目標金額を作れない」無効解の表現として用います。
  • コイン両替問題 II では、「最少硬貨枚数」を求める問題から「硬貨の組合せ数」を求める問題へ変わるため、状態遷移方程式も \\(\\min()\\) から総和演算子へ対応して変わります。

編集距離問題

  • 編集距離(Levenshtein 距離)は 2 つの文字列間の類似度を測るために用いられ、ある文字列を別の文字列へ変換するための最小編集回数として定義されます。編集操作には追加、削除、置換が含まれます。
  • 編集距離問題の状態は、\\(s\\) の前 \\(i\\) 文字を \\(t\\) の前 \\(j\\) 文字へ変更するのに必要な最小編集回数として定義されます。\\(s[i] \\ne t[j]\\) のときは、追加、削除、置換の 3 つの判断があり、それぞれに対応する残りの部分問題があります。これにより最適部分構造を見いだし、状態遷移方程式を構築できます。一方、\\(s[i] = t[j]\\) のときは現在の文字を編集する必要はありません。
  • 編集距離では、状態は真上、真左、左上の状態に依存します。そのため、空間最適化後は順方向でも逆方向でも正しく状態遷移できません。そこで、変数を 1 つ用いて左上の状態を一時保存し、完全ナップサック問題と等価な形へ変換することで、空間最適化後に順方向走査を行えるようにします。
","path":["第 14 章   動的計画法","14.7   まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   完全ナップサック問題","text":"

本節では、まずもう 1 つの代表的なナップサック問題である完全ナップサック問題を解き、その特殊例である硬貨交換問題について見ていきます。

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1451","level":2,"title":"14.5.1   完全ナップサック問題","text":"

Question

\\(n\\) 個の品物が与えられ、\\(i\\) 番目の品物の重さは \\(wgt[i-1]\\)、価値は \\(val[i-1]\\) であり、容量 \\(cap\\) のナップサックがあります。各品物は繰り返し選択できます。ナップサック容量の制約下で入れられる品物の最大価値を求めてください。例を以下の図に示します。

図 14-22   完全ナップサック問題のサンプルデータ

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1","level":3,"title":"1.   動的計画法の考え方","text":"

完全ナップサック問題は 0-1 ナップサック問題と非常によく似ています。違いは、品物の選択回数に制限がない点だけです。

  • 0-1 ナップサック問題では、各品物は 1 つしかないため、品物 \\(i\\) をナップサックに入れた後は先頭 \\(i-1\\) 個の品物からしか選べません。
  • 完全ナップサック問題では、各品物の数は無限であるため、品物 \\(i\\) をナップサックに入れた後も、引き続き先頭 \\(i\\) 個の品物から選べます。

完全ナップサック問題では、状態 \\([i, c]\\) の変化は 2 つの場合に分けられます。

  • 品物 \\(i\\) を入れない :0-1 ナップサック問題と同様に、\\([i-1, c]\\) へ遷移します。
  • 品物 \\(i\\) を入れる :0-1 ナップサック問題とは異なり、\\([i, c-wgt[i-1]]\\) へ遷移します。

したがって、状態遷移方程式は次のようになります。

\\[ dp[i, c] = \\max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \\]","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2","level":3,"title":"2.   コード実装","text":"

2 つの問題のコードを比較すると、状態遷移の中で 1 か所だけ \\(i-1\\) が \\(i\\) に変わり、それ以外は完全に同じです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"完全ナップサック問題:動的計画法\"\"\"\n    n = len(wgt)\n    # dp テーブルを初期化\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # 状態遷移\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
unbounded_knapsack.cpp
/* 完全ナップサック問題:動的計画法 */\nint unboundedKnapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // dp テーブルを初期化\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.java
/* 完全ナップサック問題:動的計画法 */\nint unboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // dp テーブルを初期化\n    int[][] dp = new int[n + 1][cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.cs
/* 完全ナップサック問題:動的計画法 */\nint UnboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // dp テーブルを初期化\n    int[,] dp = new int[n + 1, cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i, c] = Math.Max(dp[i - 1, c], dp[i, c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
unbounded_knapsack.go
/* 完全ナップサック問題:動的計画法 */\nfunc unboundedKnapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // dp テーブルを初期化\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.swift
/* 完全ナップサック問題:動的計画法 */\nfunc unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // 状態遷移\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.js
/* 完全ナップサック問題:動的計画法 */\nfunction unboundedKnapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.ts
/* 完全ナップサック問題:動的計画法 */\nfunction unboundedKnapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.dart
/* 完全ナップサック問題:動的計画法 */\nint unboundedKnapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // dp テーブルを初期化\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // ナップサック容量を超えるなら品物 i は選ばない\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
unbounded_knapsack.rs
/* 完全ナップサック問題:動的計画法 */\nfn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // dp テーブルを初期化\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // 状態遷移\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.c
/* 完全ナップサック問題:動的計画法 */\nint unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // dp テーブルを初期化\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = myMax(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
unbounded_knapsack.kt
/* 完全ナップサック問題:動的計画法 */\nfun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // dp テーブルを初期化\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // 状態遷移\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.rb
### 完全ナップサック:動的計画法 ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # 状態遷移\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # ナップサック容量を超えるなら品物 i は選ばない\n        dp[i][c] = dp[i - 1][c]\n      else\n        # 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3","level":3,"title":"3.   空間最適化","text":"

現在の状態は左側と上側の状態から遷移してくるため、空間最適化後は \\(dp\\) テーブルの各行を順方向に走査する必要があります。

この走査順序は 0-1 ナップサックとはちょうど逆です。両者の違いは次の図を用いて理解してください。

<1><2><3><4><5><6>

図 14-23   完全ナップサック問題における空間最適化後の動的計画法の過程

コード実装は比較的簡単で、配列 dp の第 1 次元を削除するだけです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"完全ナップサック問題:空間最適化後の動的計画法\"\"\"\n    n = len(wgt)\n    # dp テーブルを初期化\n    dp = [0] * (cap + 1)\n    # 状態遷移\n    for i in range(1, n + 1):\n        # 順方向に走査する\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c]\n            else:\n                # 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
unbounded_knapsack.cpp
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nint unboundedKnapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // dp テーブルを初期化\n    vector<int> dp(cap + 1, 0);\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.java
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nint unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // dp テーブルを初期化\n    int[] dp = new int[cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.cs
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nint UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // dp テーブルを初期化\n    int[] dp = new int[cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.go
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfunc unboundedKnapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // dp テーブルを初期化\n    dp := make([]int, cap+1)\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.swift
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfunc unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: 0, count: cap + 1)\n    // 状態遷移\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.js
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfunction unboundedKnapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.ts
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfunction unboundedKnapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.dart
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nint unboundedKnapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // dp テーブルを初期化\n  List<int> dp = List.filled(cap + 1, 0);\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // ナップサック容量を超えるなら品物 i は選ばない\n        dp[c] = dp[c];\n      } else {\n        // 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
unbounded_knapsack.rs
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // dp テーブルを初期化\n    let mut dp = vec![0; cap + 1];\n    // 状態遷移\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
unbounded_knapsack.c
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nint unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // dp テーブルを初期化\n    int *dp = calloc(cap + 1, sizeof(int));\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
unbounded_knapsack.kt
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfun unboundedKnapsackDPComp(\n    wgt: IntArray,\n    _val: IntArray,\n    cap: Int\n): Int {\n    val n = wgt.size\n    // dp テーブルを初期化\n    val dp = IntArray(cap + 1)\n    // 状態遷移\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.rb
### 完全ナップサック:動的計画法 ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # 状態遷移\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # ナップサック容量を超えるなら品物 i は選ばない\n        dp[i][c] = dp[i - 1][c]\n      else\n        # 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n\n# ## 完全ナップサック: 空間最適化後の動的計画法 ##3\ndef unbounded_knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # dp テーブルを初期化\n  dp = Array.new(cap + 1, 0)\n  # 状態遷移\n  for i in 1...(n + 1)\n    # 順方向に走査する\n    for c in 1...(cap + 1)\n      if wgt[i -1] > c\n        # ナップサック容量を超えるなら品物 i は選ばない\n        dp[c] = dp[c]\n      else\n        # 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1452","level":2,"title":"14.5.2   硬貨交換問題","text":"

ナップサック問題は動的計画法の代表的な問題群であり、多くの派生問題があります。硬貨交換問題もその 1 つです。

Question

\\(n\\) 種類の硬貨が与えられ、\\(i\\) 番目の硬貨の額面は \\(coins[i - 1]\\) 、目標金額は \\(amt\\) です。各硬貨は繰り返し選択できます。目標金額を作るために必要な最小の硬貨枚数を求めてください。目標金額を作れない場合は \\(-1\\) を返します。例を以下の図に示します。

図 14-24   硬貨交換問題のサンプルデータ

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_1","level":3,"title":"1.   動的計画法の考え方","text":"

硬貨交換は完全ナップサック問題の特殊なケースとみなせます。両者には次の対応関係と相違点があります。

  • 2 つの問題は相互に変換でき、「品物」は「硬貨」、「品物の重さ」は「硬貨の額面」、「ナップサック容量」は「目標金額」に対応します。
  • 最適化の目標は逆であり、完全ナップサック問題は品物価値の最大化、硬貨交換問題は硬貨枚数の最小化を目指します。
  • 完全ナップサック問題はナップサック容量を「超えない」解を求めますが、硬貨交換は目標金額に「ちょうど」一致する解を求めます。

ステップ 1:各ラウンドの選択を考え、状態を定義して、\\(dp\\) テーブルを得る

状態 \\([i, a]\\) に対応する部分問題は、**先頭 \\(i\\) 種類の硬貨で金額 \\(a\\) を作るための最小硬貨枚数**であり、これを \\(dp[i, a]\\) と表します。

2 次元 \\(dp\\) テーブルのサイズは \\((n+1) \\times (amt+1)\\) です。

ステップ 2:最適部分構造を見つけ、状態遷移方程式を導く

本問の状態遷移方程式は、完全ナップサック問題と比べて次の 2 点が異なります。

  • 本問では最小値を求めるため、演算子 \\(\\max()\\) を \\(\\min()\\) に変更する必要があります。
  • 最適化の対象は品物価値ではなく硬貨枚数であるため、硬貨を選んだときは \\(+1\\) すれば十分です。
\\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

ステップ 3:境界条件と状態遷移順序を決める

目標金額が \\(0\\) のとき、それを作るための最小硬貨枚数は \\(0\\) です。つまり、先頭列のすべての \\(dp[i, 0]\\) は \\(0\\) になります。

硬貨が 1 枚もない場合、任意の \\(> 0\\) の目標金額を作ることはできません。これは無効解です。状態遷移方程式内の \\(\\min()\\) 関数が無効解を識別して除外できるように、それらを \\(+ \\infty\\) で表すことを考えます。すなわち、先頭行のすべての \\(dp[0, a]\\) を \\(+ \\infty\\) とします。

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_1","level":3,"title":"2.   コード実装","text":"

多くのプログラミング言語には \\(+ \\infty\\) を表す変数が用意されていないため、通常は整数型 int の最大値で代用します。しかし、その場合は大きな数のオーバーフローが起こり得ます。状態遷移方程式中の \\(+ 1\\) 操作で桁あふれが発生する可能性があるためです。

そのため、ここでは数値 \\(amt + 1\\) を無効解の表現として用います。金額 \\(amt\\) を作るための硬貨枚数は最大でも \\(amt\\) 枚だからです。最後に返す前に、\\(dp[n, amt]\\) が \\(amt + 1\\) に等しいかを判定し、等しければ \\(-1\\) を返して目標金額を作れないことを表します。コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
def coin_change_dp(coins: list[int], amt: int) -> int:\n    \"\"\"コイン両替:動的計画法\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # dp テーブルを初期化\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # 状態遷移:先頭行と先頭列\n    for a in range(1, amt + 1):\n        dp[0][a] = MAX\n    # 状態遷移: 残りの行と列\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n    return dp[n][amt] if dp[n][amt] != MAX else -1\n
coin_change.cpp
/* コイン両替:動的計画法 */\nint coinChangeDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // 状態遷移:先頭行と先頭列\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.java
/* コイン両替:動的計画法 */\nint coinChangeDP(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int[][] dp = new int[n + 1][amt + 1];\n    // 状態遷移:先頭行と先頭列\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.cs
/* コイン両替:動的計画法 */\nint CoinChangeDP(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int[,] dp = new int[n + 1, amt + 1];\n    // 状態遷移:先頭行と先頭列\n    for (int a = 1; a <= amt; a++) {\n        dp[0, a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i, a] = Math.Min(dp[i - 1, a], dp[i, a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n, amt] != MAX ? dp[n, amt] : -1;\n}\n
coin_change.go
/* コイン両替:動的計画法 */\nfunc coinChangeDP(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // dp テーブルを初期化\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // 状態遷移:先頭行と先頭列\n    for a := 1; a <= amt; a++ {\n        dp[0][a] = max\n    }\n    // 状態遷移: 残りの行と列\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = int(math.Min(float64(dp[i-1][a]), float64(dp[i][a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt]\n    }\n    return -1\n}\n
coin_change.swift
/* コイン両替:動的計画法 */\nfunc coinChangeDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // 状態遷移:先頭行と先頭列\n    for a in 1 ... amt {\n        dp[0][a] = MAX\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1\n}\n
coin_change.js
/* コイン両替:動的計画法 */\nfunction coinChangeDP(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 状態遷移:先頭行と先頭列\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
coin_change.ts
/* コイン両替:動的計画法 */\nfunction coinChangeDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 状態遷移:先頭行と先頭列\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
coin_change.dart
/* コイン両替:動的計画法 */\nint coinChangeDP(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // dp テーブルを初期化\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // 状態遷移:先頭行と先頭列\n  for (int a = 1; a <= amt; a++) {\n    dp[0][a] = MAX;\n  }\n  // 状態遷移: 残りの行と列\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 目標金額を超えるなら硬貨 i は選ばない\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n        dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.rs
/* コイン両替:動的計画法 */\nfn coin_change_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // dp テーブルを初期化\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // 状態遷移:先頭行と先頭列\n    for a in 1..=amt {\n        dp[0][a] = max;\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt] as i32;\n    } else {\n        -1\n    }\n}\n
coin_change.c
/* コイン両替:動的計画法 */\nint coinChangeDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // 状態遷移:先頭行と先頭列\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[n][amt] != MAX ? dp[n][amt] : -1;\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
coin_change.kt
/* コイン両替:動的計画法 */\nfun coinChangeDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // dp テーブルを初期化\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // 状態遷移:先頭行と先頭列\n    for (a in 1..amt) {\n        dp[0][a] = MAX\n    }\n    // 状態遷移: 残りの行と列\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[n][amt] != MAX) dp[n][amt] else -1\n}\n
coin_change.rb
### コイン両替:動的計画法 ###\ndef coin_change_dp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # 状態遷移:先頭行と先頭列\n  (1...(amt + 1)).each { |a| dp[0][a] = _MAX }\n  # 状態遷移: 残りの行と列\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 目標金額を超えるなら硬貨 i は選ばない\n        dp[i][a] = dp[i - 1][a]\n      else\n        # 硬貨 i を選ばない場合と選ぶ場合の小さい方\n        dp[i][a] = [dp[i - 1][a], dp[i][a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[n][amt] != _MAX ? dp[n][amt] : -1\nend\n
コードの可視化

全画面で見る >

次の図は硬貨交換の動的計画法の過程を示しており、完全ナップサック問題と非常によく似ています。

<1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

図 14-25   硬貨交換問題の動的計画法の過程

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_1","level":3,"title":"3.   空間最適化","text":"

硬貨交換の空間最適化の方法は、完全ナップサック問題と同じです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
def coin_change_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"コイン交換:空間最適化後の動的計画法\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # dp テーブルを初期化\n    dp = [MAX] * (amt + 1)\n    dp[0] = 0\n    # 状態遷移\n    for i in range(1, n + 1):\n        # 順方向に走査する\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            else:\n                # 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n    return dp[amt] if dp[amt] != MAX else -1\n
coin_change.cpp
/* コイン交換:空間最適化後の動的計画法 */\nint coinChangeDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    vector<int> dp(amt + 1, MAX);\n    dp[0] = 0;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.java
/* コイン交換:空間最適化後の動的計画法 */\nint coinChangeDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int[] dp = new int[amt + 1];\n    Arrays.fill(dp, MAX);\n    dp[0] = 0;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.cs
/* コイン交換:空間最適化後の動的計画法 */\nint CoinChangeDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int[] dp = new int[amt + 1];\n    Array.Fill(dp, MAX);\n    dp[0] = 0;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = Math.Min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.go
/* コイン両替:動的計画法 */\nfunc coinChangeDPComp(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // dp テーブルを初期化\n    dp := make([]int, amt+1)\n    for i := 1; i <= amt; i++ {\n        dp[i] = max\n    }\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        // 順方向に走査する\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = int(math.Min(float64(dp[a]), float64(dp[a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt]\n    }\n    return -1\n}\n
coin_change.swift
/* コイン交換:空間最適化後の動的計画法 */\nfunc coinChangeDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // dp テーブルを初期化\n    var dp = Array(repeating: MAX, count: amt + 1)\n    dp[0] = 0\n    // 状態遷移\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1\n}\n
coin_change.js
/* コイン交換:空間最適化後の動的計画法 */\nfunction coinChangeDPComp(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
coin_change.ts
/* コイン交換:空間最適化後の動的計画法 */\nfunction coinChangeDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
coin_change.dart
/* コイン交換:空間最適化後の動的計画法 */\nint coinChangeDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // dp テーブルを初期化\n  List<int> dp = List.filled(amt + 1, MAX);\n  dp[0] = 0;\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 目標金額を超えるなら硬貨 i は選ばない\n        dp[a] = dp[a];\n      } else {\n        // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n        dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.rs
/* コイン交換:空間最適化後の動的計画法 */\nfn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // dp テーブルを初期化\n    let mut dp = vec![0; amt + 1];\n    dp.fill(max);\n    dp[0] = 0;\n    // 状態遷移\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt] as i32;\n    } else {\n        -1\n    }\n}\n
coin_change.c
/* コイン交換:空間最適化後の動的計画法 */\nint coinChangeDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int *dp = malloc((amt + 1) * sizeof(int));\n    for (int j = 1; j <= amt; j++) {\n        dp[j] = MAX;\n    } \n    dp[0] = 0;\n\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[amt] != MAX ? dp[amt] : -1;\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
coin_change.kt
/* コイン交換:空間最適化後の動的計画法 */\nfun coinChangeDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // dp テーブルを初期化\n    val dp = IntArray(amt + 1)\n    dp.fill(MAX)\n    dp[0] = 0\n    // 状態遷移\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[amt] != MAX) dp[amt] else -1\n}\n
coin_change.rb
### コイン両替:空間最適化した動的計画法 ###\ndef coin_change_dp_comp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # dp テーブルを初期化\n  dp = Array.new(amt + 1, _MAX)\n  dp[0] = 0\n  # 状態遷移\n  for i in 1...(n + 1)\n    # 順方向に走査する\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 目標金額を超えるなら硬貨 i は選ばない\n        dp[a] = dp[a]\n      else\n        # 硬貨 i を選ばない場合と選ぶ場合の小さい方\n        dp[a] = [dp[a], dp[a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[amt] != _MAX ? dp[amt] : -1\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1453-ii","level":2,"title":"14.5.3   硬貨交換問題 II","text":"

Question

\\(n\\) 種類の硬貨が与えられ、\\(i\\) 番目の硬貨の額面は \\(coins[i - 1]\\) 、目標金額は \\(amt\\) です。各硬貨は繰り返し選択できるとして、**目標金額を作る硬貨の組合せ数**を求めてください。例を以下の図に示します。

図 14-26   硬貨交換問題 II のサンプルデータ

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_2","level":3,"title":"1.   動的計画法の考え方","text":"

前問と比べて、本問の目的は組合せ数を求めることです。そのため、部分問題は 先頭 \\(i\\) 種類の硬貨で金額 \\(a\\) を作れる組合せ数 になります。一方、\\(dp\\) テーブルは引き続きサイズ \\((n+1) \\times (amt + 1)\\) の 2 次元行列です。

現在の状態における組合せ数は、現在の硬貨を選ばない場合と選ぶ場合の 2 つの選択肢の組合せ数の和に等しくなります。状態遷移方程式は次のとおりです。

\\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\]

目標金額が \\(0\\) のときは、どの硬貨も選ばなくても目標金額を作れるため、先頭列のすべての \\(dp[i, 0]\\) を \\(1\\) に初期化します。硬貨がないときは、任意の \\(>0\\) の目標金額を作れないため、先頭行のすべての \\(dp[0, a]\\) は \\(0\\) になります。

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_2","level":3,"title":"2.   コード実装","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
def coin_change_ii_dp(coins: list[int], amt: int) -> int:\n    \"\"\"コイン両替 II:動的計画法\"\"\"\n    n = len(coins)\n    # dp テーブルを初期化\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # 先頭列を初期化する\n    for i in range(n + 1):\n        dp[i][0] = 1\n    # 状態遷移\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n    return dp[n][amt]\n
coin_change_ii.cpp
/* コイン両替 II:動的計画法 */\nint coinChangeIIDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // dp テーブルを初期化\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // 先頭列を初期化する\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.java
/* コイン両替 II:動的計画法 */\nint coinChangeIIDP(int[] coins, int amt) {\n    int n = coins.length;\n    // dp テーブルを初期化\n    int[][] dp = new int[n + 1][amt + 1];\n    // 先頭列を初期化する\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.cs
/* コイン両替 II:動的計画法 */\nint CoinChangeIIDP(int[] coins, int amt) {\n    int n = coins.Length;\n    // dp テーブルを初期化\n    int[,] dp = new int[n + 1, amt + 1];\n    // 先頭列を初期化する\n    for (int i = 0; i <= n; i++) {\n        dp[i, 0] = 1;\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i, a] = dp[i - 1, a] + dp[i, a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n, amt];\n}\n
coin_change_ii.go
/* コイン両替 II:動的計画法 */\nfunc coinChangeIIDP(coins []int, amt int) int {\n    n := len(coins)\n    // dp テーブルを初期化\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // 先頭列を初期化する\n    for i := 0; i <= n; i++ {\n        dp[i][0] = 1\n    }\n    // 状態遷移: 残りの行と列\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.swift
/* コイン両替 II:動的計画法 */\nfunc coinChangeIIDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // 先頭列を初期化する\n    for i in 0 ... n {\n        dp[i][0] = 1\n    }\n    // 状態遷移\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.js
/* コイン両替 II:動的計画法 */\nfunction coinChangeIIDP(coins, amt) {\n    const n = coins.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 先頭列を初期化する\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.ts
/* コイン両替 II:動的計画法 */\nfunction coinChangeIIDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 先頭列を初期化する\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.dart
/* コイン両替 II:動的計画法 */\nint coinChangeIIDP(List<int> coins, int amt) {\n  int n = coins.length;\n  // dp テーブルを初期化\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // 先頭列を初期化する\n  for (int i = 0; i <= n; i++) {\n    dp[i][0] = 1;\n  }\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 目標金額を超えるなら硬貨 i は選ばない\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // コイン i を選ばない場合と選ぶ場合の和\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[n][amt];\n}\n
coin_change_ii.rs
/* コイン両替 II:動的計画法 */\nfn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // dp テーブルを初期化\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // 先頭列を初期化する\n    for i in 0..=n {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[n][amt]\n}\n
coin_change_ii.c
/* コイン両替 II:動的計画法 */\nint coinChangeIIDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // dp テーブルを初期化\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // 先頭列を初期化する\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[n][amt];\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
coin_change_ii.kt
/* コイン両替 II:動的計画法 */\nfun coinChangeIIDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // dp テーブルを初期化\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // 先頭列を初期化する\n    for (i in 0..n) {\n        dp[i][0] = 1\n    }\n    // 状態遷移\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.rb
### コイン両替 II:動的計画法 ###\ndef coin_change_ii_dp(coins, amt)\n  n = coins.length\n  # dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # 先頭列を初期化する\n  (0...(n + 1)).each { |i| dp[i][0] = 1 }\n  # 状態遷移\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 目標金額を超えるなら硬貨 i は選ばない\n        dp[i][a] = dp[i - 1][a]\n      else\n        # コイン i を選ばない場合と選ぶ場合の和\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n      end\n    end\n  end\n  dp[n][amt]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_2","level":3,"title":"3.   空間最適化","text":"

空間最適化の方法も同様で、硬貨の次元を削除するだけです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"コイン両替 II:空間最適化した動的計画法\"\"\"\n    n = len(coins)\n    # dp テーブルを初期化\n    dp = [0] * (amt + 1)\n    dp[0] = 1\n    # 状態遷移\n    for i in range(1, n + 1):\n        # 順方向に走査する\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            else:\n                # コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n    return dp[amt]\n
coin_change_ii.cpp
/* コイン両替 II:空間最適化した動的計画法 */\nint coinChangeIIDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // dp テーブルを初期化\n    vector<int> dp(amt + 1, 0);\n    dp[0] = 1;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.java
/* コイン両替 II:空間最適化した動的計画法 */\nint coinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    // dp テーブルを初期化\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.cs
/* コイン両替 II:空間最適化した動的計画法 */\nint CoinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    // dp テーブルを初期化\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.go
/* コイン両替 II:空間最適化した動的計画法 */\nfunc coinChangeIIDPComp(coins []int, amt int) int {\n    n := len(coins)\n    // dp テーブルを初期化\n    dp := make([]int, amt+1)\n    dp[0] = 1\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        // 順方向に走査する\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a-coins[i-1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.swift
/* コイン両替 II:空間最適化した動的計画法 */\nfunc coinChangeIIDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: 0, count: amt + 1)\n    dp[0] = 1\n    // 状態遷移\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.js
/* コイン両替 II:空間最適化した動的計画法 */\nfunction coinChangeIIDPComp(coins, amt) {\n    const n = coins.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.ts
/* コイン両替 II:空間最適化した動的計画法 */\nfunction coinChangeIIDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.dart
/* コイン両替 II:空間最適化した動的計画法 */\nint coinChangeIIDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  // dp テーブルを初期化\n  List<int> dp = List.filled(amt + 1, 0);\n  dp[0] = 1;\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 目標金額を超えるなら硬貨 i は選ばない\n        dp[a] = dp[a];\n      } else {\n        // コイン i を選ばない場合と選ぶ場合の和\n        dp[a] = dp[a] + dp[a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[amt];\n}\n
coin_change_ii.rs
/* コイン両替 II:空間最適化した動的計画法 */\nfn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // dp テーブルを初期化\n    let mut dp = vec![0; amt + 1];\n    dp[0] = 1;\n    // 状態遷移\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[amt]\n}\n
coin_change_ii.c
/* コイン両替 II:空間最適化した動的計画法 */\nint coinChangeIIDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // dp テーブルを初期化\n    int *dp = calloc(amt + 1, sizeof(int));\n    dp[0] = 1;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[amt];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
coin_change_ii.kt
/* コイン両替 II:空間最適化した動的計画法 */\nfun coinChangeIIDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // dp テーブルを初期化\n    val dp = IntArray(amt + 1)\n    dp[0] = 1\n    // 状態遷移\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.rb
### コイン両替 II:空間最適化した動的計画法 ###\ndef coin_change_ii_dp_comp(coins, amt)\n  n = coins.length\n  # dp テーブルを初期化\n  dp = Array.new(amt + 1, 0)\n  dp[0] = 1\n  # 状態遷移\n  for i in 1...(n + 1)\n    # 順方向に走査する\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 目標金額を超えるなら硬貨 i は選ばない\n        dp[a] = dp[a]\n      else\n        # コイン i を選ばない場合と選ぶ場合の和\n        dp[a] = dp[a] + dp[a - coins[i - 1]]\n      end\n    end\n  end\n  dp[amt]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_graph/","level":1,"title":"第 9 章   グラフ","text":"

Abstract

人生の旅路において、私たちはそれぞれ一つひとつのノードのようなものであり、無数の見えない辺によって結ばれています。

出会いと別れのたびに、この巨大なネットワークグラフの中に固有の足跡が刻まれます。

","path":["第 9 章   グラフ"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"章の内容","text":"
  • 9.1   グラフ
  • 9.2   グラフの基本操作
  • 9.3   グラフの走査
  • 9.4   まとめ
","path":["第 9 章   グラフ"],"tags":[]},{"location":"chapter_graph/graph/","level":1,"title":"9.1   グラフ","text":"

グラフ(graph)は、頂点(vertex)と辺(edge)から構成される非線形データ構造です。グラフ \\(G\\) は、頂点集合 \\(V\\) と辺集合 \\(E\\) からなる集合として抽象的に表せます。以下の例は、5 個の頂点と 7 本の辺を含むグラフを示しています。

\\[ \\begin{aligned} V & = \\{ 1, 2, 3, 4, 5 \\} \\newline E & = \\{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \\} \\newline G & = \\{ V, E \\} \\newline \\end{aligned} \\]

頂点をノード、辺を各ノードをつなぐ参照(ポインタ)とみなせば、グラフは連結リストを拡張したデータ構造の一種と捉えられます。次の図に示すように、線形関係(連結リスト)や分治関係(木)と比べて、ネットワーク関係(グラフ)は自由度が高く、そのぶん複雑です。

図 9-1   連結リスト、木、グラフの関係

","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#911","level":2,"title":"9.1.1   グラフの一般的な種類と用語","text":"

辺が方向性を持つかどうかに応じて、無向グラフ(undirected graph)と有向グラフ(directed graph)に分けられます。次の図のとおりです。

  • 無向グラフでは、辺は 2 つの頂点間の「双方向」の接続関係を表します。例えば WeChat や QQ における「友だち関係」です。
  • 有向グラフでは、辺は方向性を持ち、すなわち \\(A \\rightarrow B\\) と \\(A \\leftarrow B\\) の 2 方向の辺は互いに独立です。例えば Weibo や Douyin における「フォロー」と「フォロワー」の関係です。

図 9-2   有向グラフと無向グラフ

すべての頂点が連結しているかどうかに応じて、連結グラフ(connected graph)と非連結グラフ(disconnected graph)に分けられます。次の図のとおりです。

  • 連結グラフでは、ある頂点から出発すると、ほかの任意の頂点に到達できます。
  • 非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません。

図 9-3   連結グラフと非連結グラフ

辺に「重み」の変数を追加すると、次の図に示すような重み付きグラフ(weighted graph)が得られます。例えば『Honor of Kings』のようなモバイルゲームでは、システムが共にプレイした時間に基づいてプレイヤー間の「親密度」を計算します。この親密度ネットワークは重み付きグラフで表せます。

図 9-4   重み付きグラフと重みなしグラフ

グラフというデータ構造には、次のような基本用語があります。

  • 隣接(adjacency):2 つの頂点の間に辺が存在するとき、この 2 つの頂点は「隣接している」といいます。上図では、頂点 1 に隣接する頂点は 2、3、5 です。
  • 経路(path):頂点 A から頂点 B までに通過する辺で構成された列を、A から B への「経路」と呼びます。上図では、辺の列 1-5-2-4 は頂点 1 から頂点 4 への 1 本の経路です。
  • 次数(degree):ある頂点が持つ辺の本数です。有向グラフでは、入次数(in-degree)はその頂点に向かう辺の本数を表し、出次数(out-degree)はその頂点から出る辺の本数を表します。
","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#912","level":2,"title":"9.1.2   グラフの表現","text":"

グラフの一般的な表現方法には「隣接行列」と「隣接リスト」があります。以下では無向グラフを例に説明します。

","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#1","level":3,"title":"1.   隣接行列","text":"

グラフの頂点数を \\(n\\) とすると、隣接行列(adjacency matrix)は \\(n \\times n\\) の行列を用いてグラフを表します。各行(列)は 1 つの頂点を表し、行列要素は辺を表します。\\(1\\) または \\(0\\) を用いて、2 つの頂点の間に辺があるかどうかを示します。

次の図のように、隣接行列を \\(M\\)、頂点リストを \\(V\\) とすると、行列要素 \\(M[i, j] = 1\\) は頂点 \\(V[i]\\) から頂点 \\(V[j]\\) への辺が存在することを表し、逆に \\(M[i, j] = 0\\) は 2 つの頂点の間に辺がないことを表します。

図 9-5   グラフの隣接行列による表現

隣接行列には次の特徴があります。

  • 単純グラフでは、頂点は自分自身とは接続できないため、このとき隣接行列の主対角線上の要素には意味がありません。
  • 無向グラフでは、2 方向の辺は等価であるため、このとき隣接行列は主対角線に関して対称です。
  • 隣接行列の要素を \\(1\\) と \\(0\\) から重みに置き換えると、重み付きグラフを表せます。

隣接行列でグラフを表す場合、行列要素に直接アクセスして辺を取得できるため、追加・削除・検索・更新の操作効率は高く、時間計算量はいずれも \\(O(1)\\) です。しかし、行列の空間計算量は \\(O(n^2)\\) であり、メモリ使用量は多くなります。

","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#2","level":3,"title":"2.   隣接リスト","text":"

隣接リスト(adjacency list)は、\\(n\\) 本の連結リストを使ってグラフを表します。連結リストのノードは頂点を表します。第 \\(i\\) 本の連結リストは頂点 \\(i\\) に対応し、その頂点に隣接するすべての頂点(その頂点と接続された頂点)を格納します。次の図は、隣接リストで保存したグラフの例です。

図 9-6   グラフの隣接リストによる表現

隣接リストは実際に存在する辺だけを格納し、辺の総数は通常 \\(n^2\\) よりはるかに小さいため、より省スペースです。しかし、隣接リストでは辺を見つけるために連結リストを走査する必要があるため、時間効率は隣接行列に及びません。

上図を見ると、隣接リストの構造はハッシュテーブルにおける「連鎖アドレス法」と非常によく似ているため、同様の方法で効率を最適化できます。例えば、連結リストが長い場合は AVL 木や赤黒木に変換して時間効率を \\(O(n)\\) から \\(O(\\log n)\\) に改善できます。さらに、連結リストをハッシュテーブルに変換すれば、時間計算量を \\(O(1)\\) まで下げられます。

","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#913","level":2,"title":"9.1.3   グラフの一般的な応用","text":"

次の表のように、多くの現実のシステムはグラフでモデル化でき、対応する問題もグラフ計算の問題に帰着できます。

表 9-1   現実世界でよく見られるグラフ

頂点 辺 グラフ計算問題 ソーシャルネットワーク ユーザー 友だち関係 潜在的な友だちの推薦 地下鉄路線 駅 駅間の接続性 最短経路の推薦 太陽系 天体 天体間の万有引力作用 惑星軌道の計算","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   グラフの基本操作","text":"

グラフの基本操作は、「辺」に対する操作と「頂点」に対する操作に分けられます。「隣接行列」と「隣接リスト」の 2 つの表現方法では、実装方法が異なります。

","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#921","level":2,"title":"9.2.1   隣接行列に基づく実装","text":"

頂点数が \\(n\\) の無向グラフを与えると、各種操作の実装方法は次図のとおりです。

  • 辺の追加または削除:隣接行列で指定した辺を直接変更すればよく、\\(O(1)\\) 時間です。無向グラフであるため、2 方向の辺を同時に更新する必要があります。
  • 頂点の追加:隣接行列の末尾に 1 行 1 列を追加し、すべてを \\(0\\) で埋めればよく、\\(O(n)\\) 時間です。
  • 頂点の削除:隣接行列から 1 行 1 列を削除します。先頭行と先頭列を削除する場合が最悪で、\\((n-1)^2\\) 個の要素を「左上へ移動」させる必要があるため、\\(O(n^2)\\) 時間です。
  • 初期化:\\(n\\) 個の頂点を受け取り、長さ \\(n\\) の頂点リスト vertices を初期化するのに \\(O(n)\\) 時間、サイズ \\(n \\times n\\) の隣接行列 adjMat を初期化するのに \\(O(n^2)\\) 時間かかります。
隣接行列の初期化辺の追加辺の削除頂点の追加頂点の削除

図 9-7   隣接行列の初期化、辺の追加と削除、頂点の追加と削除

以下は、隣接行列でグラフを表した実装コードです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_matrix.py
class GraphAdjMat:\n    \"\"\"隣接行列に基づく無向グラフクラス\"\"\"\n\n    def __init__(self, vertices: list[int], edges: list[list[int]]):\n        \"\"\"コンストラクタ\"\"\"\n        # 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n        self.vertices: list[int] = []\n        # 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n        self.adj_mat: list[list[int]] = []\n        # 頂点を追加\n        for val in vertices:\n            self.add_vertex(val)\n        # 辺を追加\n        # 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for e in edges:\n            self.add_edge(e[0], e[1])\n\n    def size(self) -> int:\n        \"\"\"頂点数を取得\"\"\"\n        return len(self.vertices)\n\n    def add_vertex(self, val: int):\n        \"\"\"頂点を追加\"\"\"\n        n = self.size()\n        # 頂点リストに新しい頂点の値を追加\n        self.vertices.append(val)\n        # 隣接行列に 1 行追加\n        new_row = [0] * n\n        self.adj_mat.append(new_row)\n        # 隣接行列に 1 列追加\n        for row in self.adj_mat:\n            row.append(0)\n\n    def remove_vertex(self, index: int):\n        \"\"\"頂点を削除\"\"\"\n        if index >= self.size():\n            raise IndexError()\n        # 頂点リストから index の頂点を削除する\n        self.vertices.pop(index)\n        # 隣接行列で index 行を削除する\n        self.adj_mat.pop(index)\n        # 隣接行列で index 列を削除する\n        for row in self.adj_mat:\n            row.pop(index)\n\n    def add_edge(self, i: int, j: int):\n        \"\"\"辺を追加\"\"\"\n        # パラメータ i, j は vertices の要素インデックスに対応する\n        # 範囲外と同値の場合の処理\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        # 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        self.adj_mat[i][j] = 1\n        self.adj_mat[j][i] = 1\n\n    def remove_edge(self, i: int, j: int):\n        \"\"\"辺を削除\"\"\"\n        # パラメータ i, j は vertices の要素インデックスに対応する\n        # 範囲外と同値の場合の処理\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        self.adj_mat[i][j] = 0\n        self.adj_mat[j][i] = 0\n\n    def print(self):\n        \"\"\"隣接行列を出力\"\"\"\n        print(\"頂点リスト =\", self.vertices)\n        print(\"隣接行列 =\")\n        print_matrix(self.adj_mat)\n
graph_adjacency_matrix.cpp
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    vector<int> vertices;       // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    vector<vector<int>> adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n  public:\n    /* コンストラクタ */\n    GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {\n        // 頂点を追加\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for (const vector<int> &edge : edges) {\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    int size() const {\n        return vertices.size();\n    }\n\n    /* 頂点を追加 */\n    void addVertex(int val) {\n        int n = size();\n        // 頂点リストに新しい頂点の値を追加\n        vertices.push_back(val);\n        // 隣接行列に 1 行追加\n        adjMat.emplace_back(vector<int>(n, 0));\n        // 隣接行列に 1 列追加\n        for (vector<int> &row : adjMat) {\n            row.push_back(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    void removeVertex(int index) {\n        if (index >= size()) {\n            throw out_of_range(\"頂点が存在しません\");\n        }\n        // 頂点リストから index の頂点を削除する\n        vertices.erase(vertices.begin() + index);\n        // 隣接行列で index 行を削除する\n        adjMat.erase(adjMat.begin() + index);\n        // 隣接行列で index 列を削除する\n        for (vector<int> &row : adjMat) {\n            row.erase(row.begin() + index);\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    void addEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"頂点が存在しません\");\n        }\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    void removeEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"頂点が存在しません\");\n        }\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* 隣接行列を出力 */\n    void print() {\n        cout << \"頂点リスト = \";\n        printVector(vertices);\n        cout << \"隣接行列 =\" << endl;\n        printVectorMatrix(adjMat);\n    }\n};\n
graph_adjacency_matrix.java
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    List<Integer> vertices; // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    List<List<Integer>> adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = new ArrayList<>();\n        this.adjMat = new ArrayList<>();\n        // 頂点を追加\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for (int[] e : edges) {\n            addEdge(e[0], e[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    public int size() {\n        return vertices.size();\n    }\n\n    /* 頂点を追加 */\n    public void addVertex(int val) {\n        int n = size();\n        // 頂点リストに新しい頂点の値を追加\n        vertices.add(val);\n        // 隣接行列に 1 行追加\n        List<Integer> newRow = new ArrayList<>(n);\n        for (int j = 0; j < n; j++) {\n            newRow.add(0);\n        }\n        adjMat.add(newRow);\n        // 隣接行列に 1 列追加\n        for (List<Integer> row : adjMat) {\n            row.add(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    public void removeVertex(int index) {\n        if (index >= size())\n            throw new IndexOutOfBoundsException();\n        // 頂点リストから index の頂点を削除する\n        vertices.remove(index);\n        // 隣接行列で index 行を削除する\n        adjMat.remove(index);\n        // 隣接行列で index 列を削除する\n        for (List<Integer> row : adjMat) {\n            row.remove(index);\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    public void addEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        adjMat.get(i).set(j, 1);\n        adjMat.get(j).set(i, 1);\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    public void removeEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        adjMat.get(i).set(j, 0);\n        adjMat.get(j).set(i, 0);\n    }\n\n    /* 隣接行列を出力 */\n    public void print() {\n        System.out.print(\"頂点リスト = \");\n        System.out.println(vertices);\n        System.out.println(\"隣接行列 =\");\n        PrintUtil.printMatrix(adjMat);\n    }\n}\n
graph_adjacency_matrix.cs
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    List<int> vertices;     // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    List<List<int>> adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // 頂点を追加\n        foreach (int val in vertices) {\n            AddVertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        foreach (int[] e in edges) {\n            AddEdge(e[0], e[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    int Size() {\n        return vertices.Count;\n    }\n\n    /* 頂点を追加 */\n    public void AddVertex(int val) {\n        int n = Size();\n        // 頂点リストに新しい頂点の値を追加\n        vertices.Add(val);\n        // 隣接行列に 1 行追加\n        List<int> newRow = new(n);\n        for (int j = 0; j < n; j++) {\n            newRow.Add(0);\n        }\n        adjMat.Add(newRow);\n        // 隣接行列に 1 列追加\n        foreach (List<int> row in adjMat) {\n            row.Add(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    public void RemoveVertex(int index) {\n        if (index >= Size())\n            throw new IndexOutOfRangeException();\n        // 頂点リストから index の頂点を削除する\n        vertices.RemoveAt(index);\n        // 隣接行列で index 行を削除する\n        adjMat.RemoveAt(index);\n        // 隣接行列で index 列を削除する\n        foreach (List<int> row in adjMat) {\n            row.RemoveAt(index);\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    public void AddEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    public void RemoveEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* 隣接行列を出力 */\n    public void Print() {\n        Console.Write(\"頂点リスト = \");\n        PrintUtil.PrintList(vertices);\n        Console.WriteLine(\"隣接行列 =\");\n        PrintUtil.PrintMatrix(adjMat);\n    }\n}\n
graph_adjacency_matrix.go
/* 隣接行列に基づく無向グラフクラス */\ntype graphAdjMat struct {\n    // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    vertices []int\n    // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n    adjMat [][]int\n}\n\n/* コンストラクタ */\nfunc newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat {\n    // 頂点を追加\n    n := len(vertices)\n    adjMat := make([][]int, n)\n    for i := range adjMat {\n        adjMat[i] = make([]int, n)\n    }\n    // グラフを初期化する\n    g := &graphAdjMat{\n        vertices: vertices,\n        adjMat:   adjMat,\n    }\n    // 辺を追加\n    // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n    for i := range edges {\n        g.addEdge(edges[i][0], edges[i][1])\n    }\n    return g\n}\n\n/* 頂点数を取得 */\nfunc (g *graphAdjMat) size() int {\n    return len(g.vertices)\n}\n\n/* 頂点を追加 */\nfunc (g *graphAdjMat) addVertex(val int) {\n    n := g.size()\n    // 頂点リストに新しい頂点の値を追加\n    g.vertices = append(g.vertices, val)\n    // 隣接行列に 1 行追加\n    newRow := make([]int, n)\n    g.adjMat = append(g.adjMat, newRow)\n    // 隣接行列に 1 列追加\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i], 0)\n    }\n}\n\n/* 頂点を削除 */\nfunc (g *graphAdjMat) removeVertex(index int) {\n    if index >= g.size() {\n        return\n    }\n    // 頂点リストから index の頂点を削除する\n    g.vertices = append(g.vertices[:index], g.vertices[index+1:]...)\n    // 隣接行列で index 行を削除する\n    g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...)\n    // 隣接行列で index 列を削除する\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...)\n    }\n}\n\n/* 辺を追加 */\n// 引数 i, j は vertices の要素インデックスに対応する\nfunc (g *graphAdjMat) addEdge(i, j int) {\n    // インデックスの範囲外と等値の処理\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n    g.adjMat[i][j] = 1\n    g.adjMat[j][i] = 1\n}\n\n/* 辺を削除 */\n// 引数 i, j は vertices の要素インデックスに対応する\nfunc (g *graphAdjMat) removeEdge(i, j int) {\n    // インデックスの範囲外と等値の処理\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    g.adjMat[i][j] = 0\n    g.adjMat[j][i] = 0\n}\n\n/* 隣接行列を出力 */\nfunc (g *graphAdjMat) print() {\n    fmt.Printf(\"\\t頂点リスト = %v\\n\", g.vertices)\n    fmt.Printf(\"\\t隣接行列 = \\n\")\n    for i := range g.adjMat {\n        fmt.Printf(\"\\t\\t\\t%v\\n\", g.adjMat[i])\n    }\n}\n
graph_adjacency_matrix.swift
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    private var vertices: [Int] // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    private var adjMat: [[Int]] // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    init(vertices: [Int], edges: [[Int]]) {\n        self.vertices = []\n        adjMat = []\n        // 頂点を追加\n        for val in vertices {\n            addVertex(val: val)\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for e in edges {\n            addEdge(i: e[0], j: e[1])\n        }\n    }\n\n    /* 頂点数を取得 */\n    func size() -> Int {\n        vertices.count\n    }\n\n    /* 頂点を追加 */\n    func addVertex(val: Int) {\n        let n = size()\n        // 頂点リストに新しい頂点の値を追加\n        vertices.append(val)\n        // 隣接行列に 1 行追加\n        let newRow = Array(repeating: 0, count: n)\n        adjMat.append(newRow)\n        // 隣接行列に 1 列追加\n        for i in adjMat.indices {\n            adjMat[i].append(0)\n        }\n    }\n\n    /* 頂点を削除 */\n    func removeVertex(index: Int) {\n        if index >= size() {\n            fatalError(\"範囲外\")\n        }\n        // 頂点リストから index の頂点を削除する\n        vertices.remove(at: index)\n        // 隣接行列で index 行を削除する\n        adjMat.remove(at: index)\n        // 隣接行列で index 列を削除する\n        for i in adjMat.indices {\n            adjMat[i].remove(at: index)\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    func addEdge(i: Int, j: Int) {\n        // インデックスの範囲外と等値の処理\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"範囲外\")\n        }\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    func removeEdge(i: Int, j: Int) {\n        // インデックスの範囲外と等値の処理\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"範囲外\")\n        }\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* 隣接行列を出力 */\n    func print() {\n        Swift.print(\"頂点リスト = \", terminator: \"\")\n        Swift.print(vertices)\n        Swift.print(\"隣接行列 =\")\n        PrintUtil.printMatrix(matrix: adjMat)\n    }\n}\n
graph_adjacency_matrix.js
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    vertices; // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    constructor(vertices, edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // 頂点を追加\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    size() {\n        return this.vertices.length;\n    }\n\n    /* 頂点を追加 */\n    addVertex(val) {\n        const n = this.size();\n        // 頂点リストに新しい頂点の値を追加\n        this.vertices.push(val);\n        // 隣接行列に 1 行追加\n        const newRow = [];\n        for (let j = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // 隣接行列に 1 列追加\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    removeVertex(index) {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 頂点リストから index の頂点を削除する\n        this.vertices.splice(index, 1);\n\n        // 隣接行列で index 行を削除する\n        this.adjMat.splice(index, 1);\n        // 隣接行列で index 列を削除する\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    addEdge(i, j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 無向グラフでは、隣接行列は主対角線に関して対称であり、(i, j) === (j, i) を満たす\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    removeEdge(i, j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* 隣接行列を出力 */\n    print() {\n        console.log('頂点リスト = ', this.vertices);\n        console.log('隣接行列 =', this.adjMat);\n    }\n}\n
graph_adjacency_matrix.ts
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    vertices: number[]; // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    adjMat: number[][]; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    constructor(vertices: number[], edges: number[][]) {\n        this.vertices = [];\n        this.adjMat = [];\n        // 頂点を追加\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    size(): number {\n        return this.vertices.length;\n    }\n\n    /* 頂点を追加 */\n    addVertex(val: number): void {\n        const n: number = this.size();\n        // 頂点リストに新しい頂点の値を追加\n        this.vertices.push(val);\n        // 隣接行列に 1 行追加\n        const newRow: number[] = [];\n        for (let j: number = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // 隣接行列に 1 列追加\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    removeVertex(index: number): void {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 頂点リストから index の頂点を削除する\n        this.vertices.splice(index, 1);\n\n        // 隣接行列で index 行を削除する\n        this.adjMat.splice(index, 1);\n        // 隣接行列で index 列を削除する\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    addEdge(i: number, j: number): void {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 無向グラフでは、隣接行列は主対角線に関して対称であり、(i, j) === (j, i) を満たす\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    removeEdge(i: number, j: number): void {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* 隣接行列を出力 */\n    print(): void {\n        console.log('頂点リスト = ', this.vertices);\n        console.log('隣接行列 =', this.adjMat);\n    }\n}\n
graph_adjacency_matrix.dart
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n  List<int> vertices = []; // 頂点要素。要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す\n  List<List<int>> adjMat = []; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n  /* コンストラクタ */\n  GraphAdjMat(List<int> vertices, List<List<int>> edges) {\n    this.vertices = [];\n    this.adjMat = [];\n    // 頂点を追加\n    for (int val in vertices) {\n      addVertex(val);\n    }\n    // 辺を追加\n    // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n    for (List<int> e in edges) {\n      addEdge(e[0], e[1]);\n    }\n  }\n\n  /* 頂点数を取得 */\n  int size() {\n    return vertices.length;\n  }\n\n  /* 頂点を追加 */\n  void addVertex(int val) {\n    int n = size();\n    // 頂点リストに新しい頂点の値を追加\n    vertices.add(val);\n    // 隣接行列に 1 行追加\n    List<int> newRow = List.filled(n, 0, growable: true);\n    adjMat.add(newRow);\n    // 隣接行列に 1 列追加\n    for (List<int> row in adjMat) {\n      row.add(0);\n    }\n  }\n\n  /* 頂点を削除 */\n  void removeVertex(int index) {\n    if (index >= size()) {\n      throw IndexError;\n    }\n    // 頂点リストから index の頂点を削除する\n    vertices.removeAt(index);\n    // 隣接行列で index 行を削除する\n    adjMat.removeAt(index);\n    // 隣接行列で index 列を削除する\n    for (List<int> row in adjMat) {\n      row.removeAt(index);\n    }\n  }\n\n  /* 辺を追加 */\n  // 引数 i, j は vertices の要素インデックスに対応する\n  void addEdge(int i, int j) {\n    // インデックスの範囲外と等値の処理\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n    adjMat[i][j] = 1;\n    adjMat[j][i] = 1;\n  }\n\n  /* 辺を削除 */\n  // 引数 i, j は vertices の要素インデックスに対応する\n  void removeEdge(int i, int j) {\n    // インデックスの範囲外と等値の処理\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    adjMat[i][j] = 0;\n    adjMat[j][i] = 0;\n  }\n\n  /* 隣接行列を出力 */\n  void printAdjMat() {\n    print(\"頂点リスト = $vertices\");\n    print(\"隣接行列 = \");\n    printMatrix(adjMat);\n  }\n}\n
graph_adjacency_matrix.rs
/* 隣接行列に基づく無向グラフ型 */\npub struct GraphAdjMat {\n    // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    pub vertices: Vec<i32>,\n    // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n    pub adj_mat: Vec<Vec<i32>>,\n}\n\nimpl GraphAdjMat {\n    /* コンストラクタ */\n    pub fn new(vertices: Vec<i32>, edges: Vec<[usize; 2]>) -> Self {\n        let mut graph = GraphAdjMat {\n            vertices: vec![],\n            adj_mat: vec![],\n        };\n        // 頂点を追加\n        for val in vertices {\n            graph.add_vertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for edge in edges {\n            graph.add_edge(edge[0], edge[1])\n        }\n\n        graph\n    }\n\n    /* 頂点数を取得 */\n    pub fn size(&self) -> usize {\n        self.vertices.len()\n    }\n\n    /* 頂点を追加 */\n    pub fn add_vertex(&mut self, val: i32) {\n        let n = self.size();\n        // 頂点リストに新しい頂点の値を追加\n        self.vertices.push(val);\n        // 隣接行列に 1 行追加\n        self.adj_mat.push(vec![0; n]);\n        // 隣接行列に 1 列追加\n        for row in self.adj_mat.iter_mut() {\n            row.push(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    pub fn remove_vertex(&mut self, index: usize) {\n        if index >= self.size() {\n            panic!(\"index error\")\n        }\n        // 頂点リストから index の頂点を削除する\n        self.vertices.remove(index);\n        // 隣接行列で index 行を削除する\n        self.adj_mat.remove(index);\n        // 隣接行列で index 列を削除する\n        for row in self.adj_mat.iter_mut() {\n            row.remove(index);\n        }\n    }\n\n    /* 辺を追加 */\n    pub fn add_edge(&mut self, i: usize, j: usize) {\n        // パラメータ i, j は vertices の要素インデックスに対応する\n        // 範囲外と同値の場合の処理\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        self.adj_mat[i][j] = 1;\n        self.adj_mat[j][i] = 1;\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    pub fn remove_edge(&mut self, i: usize, j: usize) {\n        // パラメータ i, j は vertices の要素インデックスに対応する\n        // 範囲外と同値の場合の処理\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        self.adj_mat[i][j] = 0;\n        self.adj_mat[j][i] = 0;\n    }\n\n    /* 隣接行列を出力 */\n    pub fn print(&self) {\n        println!(\"頂点リスト = {:?}\", self.vertices);\n        println!(\"隣接行列 =\");\n        println!(\"[\");\n        for row in &self.adj_mat {\n            println!(\"  {:?},\", row);\n        }\n        println!(\"]\")\n    }\n}\n
graph_adjacency_matrix.c
/* 隣接行列に基づく無向グラフ構造体 */\ntypedef struct {\n    int vertices[MAX_SIZE];\n    int adjMat[MAX_SIZE][MAX_SIZE];\n    int size;\n} GraphAdjMat;\n\n/* コンストラクタ */\nGraphAdjMat *newGraphAdjMat() {\n    GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat));\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        for (int j = 0; j < MAX_SIZE; j++) {\n            graph->adjMat[i][j] = 0;\n        }\n    }\n    return graph;\n}\n\n/* デストラクタ */\nvoid delGraphAdjMat(GraphAdjMat *graph) {\n    free(graph);\n}\n\n/* 頂点を追加 */\nvoid addVertex(GraphAdjMat *graph, int val) {\n    if (graph->size == MAX_SIZE) {\n        fprintf(stderr, \"グラフの頂点数が最大値に達しました\\n\");\n        return;\n    }\n    // n 番目の頂点を追加し、n 行目と n 列目を 0 にする\n    int n = graph->size;\n    graph->vertices[n] = val;\n    for (int i = 0; i <= n; i++) {\n        graph->adjMat[n][i] = graph->adjMat[i][n] = 0;\n    }\n    graph->size++;\n}\n\n/* 頂点を削除 */\nvoid removeVertex(GraphAdjMat *graph, int index) {\n    if (index < 0 || index >= graph->size) {\n        fprintf(stderr, \"頂点インデックスが範囲外です\\n\");\n        return;\n    }\n    // 頂点リストから index の頂点を削除する\n    for (int i = index; i < graph->size - 1; i++) {\n        graph->vertices[i] = graph->vertices[i + 1];\n    }\n    // 隣接行列で index 行を削除する\n    for (int i = index; i < graph->size - 1; i++) {\n        for (int j = 0; j < graph->size; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i + 1][j];\n        }\n    }\n    // 隣接行列で index 列を削除する\n    for (int i = 0; i < graph->size; i++) {\n        for (int j = index; j < graph->size - 1; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i][j + 1];\n        }\n    }\n    graph->size--;\n}\n\n/* 辺を追加 */\n// 引数 i, j は vertices の要素インデックスに対応する\nvoid addEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"辺インデックスが範囲外であるか、同一です\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 1;\n    graph->adjMat[j][i] = 1;\n}\n\n/* 辺を削除 */\n// 引数 i, j は vertices の要素インデックスに対応する\nvoid removeEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"辺インデックスが範囲外であるか、同一です\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 0;\n    graph->adjMat[j][i] = 0;\n}\n\n/* 隣接行列を出力 */\nvoid printGraphAdjMat(GraphAdjMat *graph) {\n    printf(\"頂点リスト = \");\n    printArray(graph->vertices, graph->size);\n    printf(\"隣接行列 =\\n\");\n    for (int i = 0; i < graph->size; i++) {\n        printArray(graph->adjMat[i], graph->size);\n    }\n}\n
graph_adjacency_matrix.kt
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat(vertices: IntArray, edges: Array<IntArray>) {\n    val vertices = mutableListOf<Int>() // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    val adjMat = mutableListOf<MutableList<Int>>() // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    init {\n        // 頂点を追加\n        for (vertex in vertices) {\n            addVertex(vertex)\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for (edge in edges) {\n            addEdge(edge[0], edge[1])\n        }\n    }\n\n    /* 頂点数を取得 */\n    fun size(): Int {\n        return vertices.size\n    }\n\n    /* 頂点を追加 */\n    fun addVertex(_val: Int) {\n        val n = size()\n        // 頂点リストに新しい頂点の値を追加\n        vertices.add(_val)\n        // 隣接行列に 1 行追加\n        val newRow = mutableListOf<Int>()\n        for (j in 0..<n) {\n            newRow.add(0)\n        }\n        adjMat.add(newRow)\n        // 隣接行列に 1 列追加\n        for (row in adjMat) {\n            row.add(0)\n        }\n    }\n\n    /* 頂点を削除 */\n    fun removeVertex(index: Int) {\n        if (index >= size())\n            throw IndexOutOfBoundsException()\n        // 頂点リストから index の頂点を削除する\n        vertices.removeAt(index)\n        // 隣接行列で index 行を削除する\n        adjMat.removeAt(index)\n        // 隣接行列で index 列を削除する\n        for (row in adjMat) {\n            row.removeAt(index)\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    fun addEdge(i: Int, j: Int) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    fun removeEdge(i: Int, j: Int) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* 隣接行列を出力 */\n    fun print() {\n        print(\"頂点リスト = \")\n        println(vertices)\n        println(\"隣接行列 =\")\n        printMatrix(adjMat)\n    }\n}\n
graph_adjacency_matrix.rb
### 隣接行列で実装した無向グラフクラス ###\nclass GraphAdjMat\n  def initialize(vertices, edges)\n    ### コンストラクタ ###\n    # 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    @vertices = []\n    # 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n    @adj_mat = []\n    # 頂点を追加\n    vertices.each { |val| add_vertex(val) }\n    # 辺を追加\n    # 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n    edges.each { |e| add_edge(e[0], e[1]) }\n  end\n\n  ### 頂点数を取得 ###\n  def size\n    @vertices.length\n  end\n\n  ### 頂点を追加 ###\n  def add_vertex(val)\n    n = size\n    # 頂点リストに新しい頂点の値を追加\n    @vertices << val\n    # 隣接行列に 1 行追加\n    new_row = Array.new(n, 0)\n    @adj_mat << new_row\n    # 隣接行列に 1 列追加\n    @adj_mat.each { |row| row << 0 }\n  end\n\n  ### 頂点を削除 ###\n  def remove_vertex(index)\n    raise IndexError if index >= size\n\n    # 頂点リストから index の頂点を削除する\n    @vertices.delete_at(index)\n    # 隣接行列で index 行を削除する\n    @adj_mat.delete_at(index)\n    # 隣接行列で index 列を削除する\n    @adj_mat.each { |row| row.delete_at(index) }\n  end\n\n  ### 辺を追加 ###\n  def add_edge(i, j)\n    # パラメータ i, j は vertices の要素インデックスに対応する\n    # 範囲外と同値の場合の処理\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    # 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n    @adj_mat[i][j] = 1\n    @adj_mat[j][i] = 1\n  end\n\n  ### 辺を削除 ###\n  def remove_edge(i, j)\n    # パラメータ i, j は vertices の要素インデックスに対応する\n    # 範囲外と同値の場合の処理\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    @adj_mat[i][j] = 0\n    @adj_mat[j][i] = 0\n  end\n\n  ### 隣接行列を出力 ###\n  def __print__\n    puts \"頂点リスト = #{@vertices}\"\n    puts '隣接行列 ='\n    print_matrix(@adj_mat)\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#922","level":2,"title":"9.2.2   隣接リストに基づく実装","text":"

無向グラフの頂点総数を \\(n\\)、辺総数を \\(m\\) とすると、各種操作は次図の方法で実装できます。

  • 辺の追加:頂点に対応する連結リストの末尾に辺を追加すればよく、\\(O(1)\\) 時間です。無向グラフなので、2 方向の辺を同時に追加する必要があります。
  • 辺の削除:頂点に対応する連結リストから指定した辺を探して削除するため、\\(O(m)\\) 時間です。無向グラフでは、2 方向の辺を同時に削除する必要があります。
  • 頂点の追加:隣接リストに 1 つの連結リストを追加し、新しい頂点をその連結リストの先頭ノードとするため、\\(O(1)\\) 時間です。
  • 頂点の削除:隣接リスト全体を走査し、指定した頂点を含むすべての辺を削除する必要があるため、\\(O(n + m)\\) 時間です。
  • 初期化:隣接リストに \\(n\\) 個の頂点と \\(2m\\) 本の辺を作成するため、\\(O(n + m)\\) 時間です。
隣接リストの初期化辺の追加辺の削除頂点の追加頂点の削除

図 9-8   隣接リストの初期化、辺の追加と削除、頂点の追加と削除

以下は隣接リストのコード実装です。上図と比べると、実際のコードには次の違いがあります。

  • 頂点の追加と削除を容易にし、コードを簡潔にするため、連結リストの代わりにリスト(動的配列)を使用しています。
  • ハッシュテーブルを用いて隣接リストを格納しており、key は頂点インスタンス、value はその頂点に隣接する頂点のリスト(連結リスト)です。

また、隣接リストでは頂点を表すために Vertex クラスを使用しています。その理由は、もし隣接行列と同様にリストのインデックスで異なる頂点を区別すると、インデックス \\(i\\) の頂点を削除する場合、隣接リスト全体を走査して、\\(i\\) より大きいすべてのインデックスを \\(1\\) 減らす必要があり、効率が非常に低いためです。これに対して、各頂点が一意な Vertex インスタンスであれば、ある頂点を削除しても他の頂点を変更する必要はありません。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_list.py
class GraphAdjList:\n    \"\"\"隣接リストに基づく無向グラフクラス\"\"\"\n\n    def __init__(self, edges: list[list[Vertex]]):\n        \"\"\"コンストラクタ\"\"\"\n        # 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n        self.adj_list = dict[Vertex, list[Vertex]]()\n        # すべての頂点と辺を追加\n        for edge in edges:\n            self.add_vertex(edge[0])\n            self.add_vertex(edge[1])\n            self.add_edge(edge[0], edge[1])\n\n    def size(self) -> int:\n        \"\"\"頂点数を取得\"\"\"\n        return len(self.adj_list)\n\n    def add_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"辺を追加\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # 辺 vet1 - vet2 を追加\n        self.adj_list[vet1].append(vet2)\n        self.adj_list[vet2].append(vet1)\n\n    def remove_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"辺を削除\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # 辺 vet1 - vet2 を削除\n        self.adj_list[vet1].remove(vet2)\n        self.adj_list[vet2].remove(vet1)\n\n    def add_vertex(self, vet: Vertex):\n        \"\"\"頂点を追加\"\"\"\n        if vet in self.adj_list:\n            return\n        # 隣接リストに新しいリストを追加\n        self.adj_list[vet] = []\n\n    def remove_vertex(self, vet: Vertex):\n        \"\"\"頂点を削除\"\"\"\n        if vet not in self.adj_list:\n            raise ValueError()\n        # 隣接リストから頂点 vet に対応するリストを削除\n        self.adj_list.pop(vet)\n        # 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for vertex in self.adj_list:\n            if vet in self.adj_list[vertex]:\n                self.adj_list[vertex].remove(vet)\n\n    def print(self):\n        \"\"\"隣接リストを出力\"\"\"\n        print(\"隣接リスト =\")\n        for vertex in self.adj_list:\n            tmp = [v.val for v in self.adj_list[vertex]]\n            print(f\"{vertex.val}: {tmp},\")\n
graph_adjacency_list.cpp
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n  public:\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    unordered_map<Vertex *, vector<Vertex *>> adjList;\n\n    /* vector 内の指定ノードを削除 */\n    void remove(vector<Vertex *> &vec, Vertex *vet) {\n        for (int i = 0; i < vec.size(); i++) {\n            if (vec[i] == vet) {\n                vec.erase(vec.begin() + i);\n                break;\n            }\n        }\n    }\n\n    /* コンストラクタ */\n    GraphAdjList(const vector<vector<Vertex *>> &edges) {\n        // すべての頂点と辺を追加\n        for (const vector<Vertex *> &edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    int size() {\n        return adjList.size();\n    }\n\n    /* 辺を追加 */\n    void addEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"頂点が存在しません\");\n        // 辺 vet1 - vet2 を追加\n        adjList[vet1].push_back(vet2);\n        adjList[vet2].push_back(vet1);\n    }\n\n    /* 辺を削除 */\n    void removeEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"頂点が存在しません\");\n        // 辺 vet1 - vet2 を削除\n        remove(adjList[vet1], vet2);\n        remove(adjList[vet2], vet1);\n    }\n\n    /* 頂点を追加 */\n    void addVertex(Vertex *vet) {\n        if (adjList.count(vet))\n            return;\n        // 隣接リストに新しいリストを追加\n        adjList[vet] = vector<Vertex *>();\n    }\n\n    /* 頂点を削除 */\n    void removeVertex(Vertex *vet) {\n        if (!adjList.count(vet))\n            throw invalid_argument(\"頂点が存在しません\");\n        // 隣接リストから頂点 vet に対応するリストを削除\n        adjList.erase(vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for (auto &adj : adjList) {\n            remove(adj.second, vet);\n        }\n    }\n\n    /* 隣接リストを出力 */\n    void print() {\n        cout << \"隣接リスト =\" << endl;\n        for (auto &adj : adjList) {\n            const auto &key = adj.first;\n            const auto &vec = adj.second;\n            cout << key->val << \": \";\n            printVector(vetsToVals(vec));\n        }\n    }\n};\n
graph_adjacency_list.java
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    Map<Vertex, List<Vertex>> adjList;\n\n    /* コンストラクタ */\n    public GraphAdjList(Vertex[][] edges) {\n        this.adjList = new HashMap<>();\n        // すべての頂点と辺を追加\n        for (Vertex[] edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    public int size() {\n        return adjList.size();\n    }\n\n    /* 辺を追加 */\n    public void addEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // 辺 vet1 - vet2 を追加\n        adjList.get(vet1).add(vet2);\n        adjList.get(vet2).add(vet1);\n    }\n\n    /* 辺を削除 */\n    public void removeEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // 辺 vet1 - vet2 を削除\n        adjList.get(vet1).remove(vet2);\n        adjList.get(vet2).remove(vet1);\n    }\n\n    /* 頂点を追加 */\n    public void addVertex(Vertex vet) {\n        if (adjList.containsKey(vet))\n            return;\n        // 隣接リストに新しいリストを追加\n        adjList.put(vet, new ArrayList<>());\n    }\n\n    /* 頂点を削除 */\n    public void removeVertex(Vertex vet) {\n        if (!adjList.containsKey(vet))\n            throw new IllegalArgumentException();\n        // 隣接リストから頂点 vet に対応するリストを削除\n        adjList.remove(vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for (List<Vertex> list : adjList.values()) {\n            list.remove(vet);\n        }\n    }\n\n    /* 隣接リストを出力 */\n    public void print() {\n        System.out.println(\"隣接リスト =\");\n        for (Map.Entry<Vertex, List<Vertex>> pair : adjList.entrySet()) {\n            List<Integer> tmp = new ArrayList<>();\n            for (Vertex vertex : pair.getValue())\n                tmp.add(vertex.val);\n            System.out.println(pair.getKey().val + \": \" + tmp + \",\");\n        }\n    }\n}\n
graph_adjacency_list.cs
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    public Dictionary<Vertex, List<Vertex>> adjList;\n\n    /* コンストラクタ */\n    public GraphAdjList(Vertex[][] edges) {\n        adjList = [];\n        // すべての頂点と辺を追加\n        foreach (Vertex[] edge in edges) {\n            AddVertex(edge[0]);\n            AddVertex(edge[1]);\n            AddEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    int Size() {\n        return adjList.Count;\n    }\n\n    /* 辺を追加 */\n    public void AddEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // 辺 vet1 - vet2 を追加\n        adjList[vet1].Add(vet2);\n        adjList[vet2].Add(vet1);\n    }\n\n    /* 辺を削除 */\n    public void RemoveEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // 辺 vet1 - vet2 を削除\n        adjList[vet1].Remove(vet2);\n        adjList[vet2].Remove(vet1);\n    }\n\n    /* 頂点を追加 */\n    public void AddVertex(Vertex vet) {\n        if (adjList.ContainsKey(vet))\n            return;\n        // 隣接リストに新しいリストを追加\n        adjList.Add(vet, []);\n    }\n\n    /* 頂点を削除 */\n    public void RemoveVertex(Vertex vet) {\n        if (!adjList.ContainsKey(vet))\n            throw new InvalidOperationException();\n        // 隣接リストから頂点 vet に対応するリストを削除\n        adjList.Remove(vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        foreach (List<Vertex> list in adjList.Values) {\n            list.Remove(vet);\n        }\n    }\n\n    /* 隣接リストを出力 */\n    public void Print() {\n        Console.WriteLine(\"隣接リスト =\");\n        foreach (KeyValuePair<Vertex, List<Vertex>> pair in adjList) {\n            List<int> tmp = [];\n            foreach (Vertex vertex in pair.Value)\n                tmp.Add(vertex.val);\n            Console.WriteLine(pair.Key.val + \": [\" + string.Join(\", \", tmp) + \"],\");\n        }\n    }\n}\n
graph_adjacency_list.go
/* 隣接リストに基づく無向グラフクラス */\ntype graphAdjList struct {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    adjList map[Vertex][]Vertex\n}\n\n/* コンストラクタ */\nfunc newGraphAdjList(edges [][]Vertex) *graphAdjList {\n    g := &graphAdjList{\n        adjList: make(map[Vertex][]Vertex),\n    }\n    // すべての頂点と辺を追加\n    for _, edge := range edges {\n        g.addVertex(edge[0])\n        g.addVertex(edge[1])\n        g.addEdge(edge[0], edge[1])\n    }\n    return g\n}\n\n/* 頂点数を取得 */\nfunc (g *graphAdjList) size() int {\n    return len(g.adjList)\n}\n\n/* 辺を追加 */\nfunc (g *graphAdjList) addEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // 辺 `vet1 - vet2` を追加し、無名 `struct{}` を追加する\n    g.adjList[vet1] = append(g.adjList[vet1], vet2)\n    g.adjList[vet2] = append(g.adjList[vet2], vet1)\n}\n\n/* 辺を削除 */\nfunc (g *graphAdjList) removeEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // 辺 vet1 - vet2 を削除\n    g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2)\n    g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1)\n}\n\n/* 頂点を追加 */\nfunc (g *graphAdjList) addVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if ok {\n        return\n    }\n    // 隣接リストに新しいリストを追加\n    g.adjList[vet] = make([]Vertex, 0)\n}\n\n/* 頂点を削除 */\nfunc (g *graphAdjList) removeVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if !ok {\n        panic(\"error\")\n    }\n    // 隣接リストから頂点 vet に対応するリストを削除\n    delete(g.adjList, vet)\n    // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n    for v, list := range g.adjList {\n        g.adjList[v] = DeleteSliceElms(list, vet)\n    }\n}\n\n/* 隣接リストを出力 */\nfunc (g *graphAdjList) print() {\n    var builder strings.Builder\n    fmt.Printf(\"隣接リスト = \\n\")\n    for k, v := range g.adjList {\n        builder.WriteString(\"\\t\\t\" + strconv.Itoa(k.Val) + \": \")\n        for _, vet := range v {\n            builder.WriteString(strconv.Itoa(vet.Val) + \" \")\n        }\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
graph_adjacency_list.swift
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    public private(set) var adjList: [Vertex: [Vertex]]\n\n    /* コンストラクタ */\n    public init(edges: [[Vertex]]) {\n        adjList = [:]\n        // すべての頂点と辺を追加\n        for edge in edges {\n            addVertex(vet: edge[0])\n            addVertex(vet: edge[1])\n            addEdge(vet1: edge[0], vet2: edge[1])\n        }\n    }\n\n    /* 頂点数を取得 */\n    public func size() -> Int {\n        adjList.count\n    }\n\n    /* 辺を追加 */\n    public func addEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"引数エラー\")\n        }\n        // 辺 vet1 - vet2 を追加\n        adjList[vet1]?.append(vet2)\n        adjList[vet2]?.append(vet1)\n    }\n\n    /* 辺を削除 */\n    public func removeEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"引数エラー\")\n        }\n        // 辺 vet1 - vet2 を削除\n        adjList[vet1]?.removeAll { $0 == vet2 }\n        adjList[vet2]?.removeAll { $0 == vet1 }\n    }\n\n    /* 頂点を追加 */\n    public func addVertex(vet: Vertex) {\n        if adjList[vet] != nil {\n            return\n        }\n        // 隣接リストに新しいリストを追加\n        adjList[vet] = []\n    }\n\n    /* 頂点を削除 */\n    public func removeVertex(vet: Vertex) {\n        if adjList[vet] == nil {\n            fatalError(\"引数エラー\")\n        }\n        // 隣接リストから頂点 vet に対応するリストを削除\n        adjList.removeValue(forKey: vet)\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for key in adjList.keys {\n            adjList[key]?.removeAll { $0 == vet }\n        }\n    }\n\n    /* 隣接リストを出力 */\n    public func print() {\n        Swift.print(\"隣接リスト =\")\n        for (vertex, list) in adjList {\n            let list = list.map { $0.val }\n            Swift.print(\"\\(vertex.val): \\(list),\")\n        }\n    }\n}\n
graph_adjacency_list.js
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    adjList;\n\n    /* コンストラクタ */\n    constructor(edges) {\n        this.adjList = new Map();\n        // すべての頂点と辺を追加\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    size() {\n        return this.adjList.size;\n    }\n\n    /* 辺を追加 */\n    addEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 辺 vet1 - vet2 を追加\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* 辺を削除 */\n    removeEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 辺 vet1 - vet2 を削除\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* 頂点を追加 */\n    addVertex(vet) {\n        if (this.adjList.has(vet)) return;\n        // 隣接リストに新しいリストを追加\n        this.adjList.set(vet, []);\n    }\n\n    /* 頂点を削除 */\n    removeVertex(vet) {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 隣接リストから頂点 vet に対応するリストを削除\n        this.adjList.delete(vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for (const set of this.adjList.values()) {\n            const index = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* 隣接リストを出力 */\n    print() {\n        console.log('隣接リスト =');\n        for (const [key, value] of this.adjList) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
graph_adjacency_list.ts
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    adjList: Map<Vertex, Vertex[]>;\n\n    /* コンストラクタ */\n    constructor(edges: Vertex[][]) {\n        this.adjList = new Map();\n        // すべての頂点と辺を追加\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    size(): number {\n        return this.adjList.size;\n    }\n\n    /* 辺を追加 */\n    addEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 辺 vet1 - vet2 を追加\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* 辺を削除 */\n    removeEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 辺 vet1 - vet2 を削除\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* 頂点を追加 */\n    addVertex(vet: Vertex): void {\n        if (this.adjList.has(vet)) return;\n        // 隣接リストに新しいリストを追加\n        this.adjList.set(vet, []);\n    }\n\n    /* 頂点を削除 */\n    removeVertex(vet: Vertex): void {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 隣接リストから頂点 vet に対応するリストを削除\n        this.adjList.delete(vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for (const set of this.adjList.values()) {\n            const index: number = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* 隣接リストを出力 */\n    print(): void {\n        console.log('隣接リスト =');\n        for (const [key, value] of this.adjList.entries()) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
graph_adjacency_list.dart
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n  // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n  Map<Vertex, List<Vertex>> adjList = {};\n\n  /* コンストラクタ */\n  GraphAdjList(List<List<Vertex>> edges) {\n    for (List<Vertex> edge in edges) {\n      addVertex(edge[0]);\n      addVertex(edge[1]);\n      addEdge(edge[0], edge[1]);\n    }\n  }\n\n  /* 頂点数を取得 */\n  int size() {\n    return adjList.length;\n  }\n\n  /* 辺を追加 */\n  void addEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // 辺 vet1 - vet2 を追加\n    adjList[vet1]!.add(vet2);\n    adjList[vet2]!.add(vet1);\n  }\n\n  /* 辺を削除 */\n  void removeEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // 辺 vet1 - vet2 を削除\n    adjList[vet1]!.remove(vet2);\n    adjList[vet2]!.remove(vet1);\n  }\n\n  /* 頂点を追加 */\n  void addVertex(Vertex vet) {\n    if (adjList.containsKey(vet)) return;\n    // 隣接リストに新しいリストを追加\n    adjList[vet] = [];\n  }\n\n  /* 頂点を削除 */\n  void removeVertex(Vertex vet) {\n    if (!adjList.containsKey(vet)) {\n      throw ArgumentError;\n    }\n    // 隣接リストから頂点 vet に対応するリストを削除\n    adjList.remove(vet);\n    // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n    adjList.forEach((key, value) {\n      value.remove(vet);\n    });\n  }\n\n  /* 隣接リストを出力 */\n  void printAdjList() {\n    print(\"隣接リスト =\");\n    adjList.forEach((key, value) {\n      List<int> tmp = [];\n      for (Vertex vertex in value) {\n        tmp.add(vertex.val);\n      }\n      print(\"${key.val}: $tmp,\");\n    });\n  }\n}\n
graph_adjacency_list.rs
/* 隣接リストに基づく無向グラフ型 */\npub struct GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    pub adj_list: HashMap<Vertex, Vec<Vertex>>, // maybe HashSet<Vertex> for value part is better?\n}\n\nimpl GraphAdjList {\n    /* コンストラクタ */\n    pub fn new(edges: Vec<[Vertex; 2]>) -> Self {\n        let mut graph = GraphAdjList {\n            adj_list: HashMap::new(),\n        };\n        // すべての頂点と辺を追加\n        for edge in edges {\n            graph.add_vertex(edge[0]);\n            graph.add_vertex(edge[1]);\n            graph.add_edge(edge[0], edge[1]);\n        }\n\n        graph\n    }\n\n    /* 頂点数を取得 */\n    #[allow(unused)]\n    pub fn size(&self) -> usize {\n        self.adj_list.len()\n    }\n\n    /* 辺を追加 */\n    pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // 辺 vet1 - vet2 を追加\n        self.adj_list.entry(vet1).or_default().push(vet2);\n        self.adj_list.entry(vet2).or_default().push(vet1);\n    }\n\n    /* 辺を削除 */\n    #[allow(unused)]\n    pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // 辺 vet1 - vet2 を削除\n        self.adj_list\n            .entry(vet1)\n            .and_modify(|v| v.retain(|&e| e != vet2));\n        self.adj_list\n            .entry(vet2)\n            .and_modify(|v| v.retain(|&e| e != vet1));\n    }\n\n    /* 頂点を追加 */\n    pub fn add_vertex(&mut self, vet: Vertex) {\n        if self.adj_list.contains_key(&vet) {\n            return;\n        }\n        // 隣接リストに新しいリストを追加\n        self.adj_list.insert(vet, vec![]);\n    }\n\n    /* 頂点を削除 */\n    #[allow(unused)]\n    pub fn remove_vertex(&mut self, vet: Vertex) {\n        // 隣接リストから頂点 vet に対応するリストを削除\n        self.adj_list.remove(&vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for list in self.adj_list.values_mut() {\n            list.retain(|&v| v != vet);\n        }\n    }\n\n    /* 隣接リストを出力 */\n    pub fn print(&self) {\n        println!(\"隣接リスト =\");\n        for (vertex, list) in &self.adj_list {\n            let list = list.iter().map(|vertex| vertex.val).collect::<Vec<i32>>();\n            println!(\"{}: {:?},\", vertex.val, list);\n        }\n    }\n}\n
graph_adjacency_list.c
/* ノード構造体 */\ntypedef struct AdjListNode {\n    Vertex *vertex;           // 頂点\n    struct AdjListNode *next; // 後続ノード\n} AdjListNode;\n\n/* 頂点に対応するノードを検索 */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* 辺を追加する補助関数 */\nvoid addEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode));\n    node->vertex = vet;\n    // 先頭挿入法\n    node->next = head->next;\n    head->next = node;\n}\n\n/* 辺削除の補助関数 */\nvoid removeEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *pre = head;\n    AdjListNode *cur = head->next;\n    // 連結リスト内で vet に対応するノードを探索\n    while (cur != NULL && cur->vertex != vet) {\n        pre = cur;\n        cur = cur->next;\n    }\n    if (cur == NULL)\n        return;\n    // vet に対応するノードを連結リストから削除\n    pre->next = cur->next;\n    // メモリを解放する\n    free(cur);\n}\n\n/* 隣接リストに基づく無向グラフクラス */\ntypedef struct {\n    AdjListNode *heads[MAX_SIZE]; // ノード配列\n    int size;                     // ノード数\n} GraphAdjList;\n\n/* コンストラクタ */\nGraphAdjList *newGraphAdjList() {\n    GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList));\n    if (!graph) {\n        return NULL;\n    }\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        graph->heads[i] = NULL;\n    }\n    return graph;\n}\n\n/* デストラクタ */\nvoid delGraphAdjList(GraphAdjList *graph) {\n    for (int i = 0; i < graph->size; i++) {\n        AdjListNode *cur = graph->heads[i];\n        while (cur != NULL) {\n            AdjListNode *next = cur->next;\n            if (cur != graph->heads[i]) {\n                free(cur);\n            }\n            cur = next;\n        }\n        free(graph->heads[i]->vertex);\n        free(graph->heads[i]);\n    }\n    free(graph);\n}\n\n/* 頂点に対応するノードを検索 */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* 辺を追加 */\nvoid addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL && head1 != head2);\n    // 辺 vet1 - vet2 を追加\n    addEdgeHelper(head1, vet2);\n    addEdgeHelper(head2, vet1);\n}\n\n/* 辺を削除 */\nvoid removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL);\n    // 辺 vet1 - vet2 を削除\n    removeEdgeHelper(head1, head2->vertex);\n    removeEdgeHelper(head2, head1->vertex);\n}\n\n/* 頂点を追加 */\nvoid addVertex(GraphAdjList *graph, Vertex *vet) {\n    assert(graph != NULL && graph->size < MAX_SIZE);\n    AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode));\n    head->vertex = vet;\n    head->next = NULL;\n    // 隣接リストに新しいリストを追加\n    graph->heads[graph->size++] = head;\n}\n\n/* 頂点を削除 */\nvoid removeVertex(GraphAdjList *graph, Vertex *vet) {\n    AdjListNode *node = findNode(graph, vet);\n    assert(node != NULL);\n    // 隣接リストから頂点 vet に対応するリストを削除\n    AdjListNode *cur = node, *pre = NULL;\n    while (cur) {\n        pre = cur;\n        cur = cur->next;\n        free(pre);\n    }\n    // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n    for (int i = 0; i < graph->size; i++) {\n        cur = graph->heads[i];\n        pre = NULL;\n        while (cur) {\n            pre = cur;\n            cur = cur->next;\n            if (cur && cur->vertex == vet) {\n                pre->next = cur->next;\n                free(cur);\n                break;\n            }\n        }\n    }\n    // この頂点より後ろの頂点を前に詰めて欠損を埋める\n    int i;\n    for (i = 0; i < graph->size; i++) {\n        if (graph->heads[i] == node)\n            break;\n    }\n    for (int j = i; j < graph->size - 1; j++) {\n        graph->heads[j] = graph->heads[j + 1];\n    }\n    graph->size--;\n    free(vet);\n}\n
graph_adjacency_list.kt
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList(edges: Array<Array<Vertex?>>) {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    val adjList = HashMap<Vertex, MutableList<Vertex>>()\n\n    /* コンストラクタ */\n    init {\n        // すべての頂点と辺を追加\n        for (edge in edges) {\n            addVertex(edge[0]!!)\n            addVertex(edge[1]!!)\n            addEdge(edge[0]!!, edge[1]!!)\n        }\n    }\n\n    /* 頂点数を取得 */\n    fun size(): Int {\n        return adjList.size\n    }\n\n    /* 辺を追加 */\n    fun addEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // 辺 vet1 - vet2 を追加\n        adjList[vet1]?.add(vet2)\n        adjList[vet2]?.add(vet1)\n    }\n\n    /* 辺を削除 */\n    fun removeEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // 辺 vet1 - vet2 を削除\n        adjList[vet1]?.remove(vet2)\n        adjList[vet2]?.remove(vet1)\n    }\n\n    /* 頂点を追加 */\n    fun addVertex(vet: Vertex) {\n        if (adjList.containsKey(vet))\n            return\n        // 隣接リストに新しいリストを追加\n        adjList[vet] = mutableListOf()\n    }\n\n    /* 頂点を削除 */\n    fun removeVertex(vet: Vertex) {\n        if (!adjList.containsKey(vet))\n            throw IllegalArgumentException()\n        // 隣接リストから頂点 vet に対応するリストを削除\n        adjList.remove(vet)\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for (list in adjList.values) {\n            list.remove(vet)\n        }\n    }\n\n    /* 隣接リストを出力 */\n    fun print() {\n        println(\"隣接リスト =\")\n        for (pair in adjList.entries) {\n            val tmp = mutableListOf<Int>()\n            for (vertex in pair.value) {\n                tmp.add(vertex._val)\n            }\n            println(\"${pair.key._val}: $tmp,\")\n        }\n    }\n}\n
graph_adjacency_list.rb
### 隣接リストで実装した無向グラフクラス ###\nclass GraphAdjList\n  attr_reader :adj_list\n\n  ### コンストラクタ ###\n  def initialize(edges)\n    # 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    @adj_list = {}\n    # すべての頂点と辺を追加\n    for edge in edges\n      add_vertex(edge[0])\n      add_vertex(edge[1])\n      add_edge(edge[0], edge[1])\n    end\n  end\n\n  ### 頂点数を取得 ###\n  def size\n    @adj_list.length\n  end\n\n  ### 辺を追加 ###\n  def add_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    @adj_list[vet1] << vet2\n    @adj_list[vet2] << vet1\n  end\n\n  ### 辺を削除 ###\n  def remove_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    # 辺 vet1 - vet2 を削除\n    @adj_list[vet1].delete(vet2)\n    @adj_list[vet2].delete(vet1)\n  end\n\n  ### 頂点を追加 ###\n  def add_vertex(vet)\n    return if @adj_list.include?(vet)\n\n    # 隣接リストに新しいリストを追加\n    @adj_list[vet] = []\n  end\n\n  ### 頂点を削除 ###\n  def remove_vertex(vet)\n    raise ArgumentError unless @adj_list.include?(vet)\n\n    # 隣接リストから頂点 vet に対応するリストを削除\n    @adj_list.delete(vet)\n    # 他の頂点のリストを走査し、vet を含むすべての辺を削除\n    for vertex in @adj_list\n      @adj_list[vertex.first].delete(vet) if @adj_list[vertex.first].include?(vet)\n    end\n  end\n\n  ### 隣接リストを出力 ###\n  def __print__\n    puts '隣接リスト ='\n    for vertex in @adj_list\n      tmp = @adj_list[vertex.first].map { |v| v.val }\n      puts \"#{vertex.first.val}: #{tmp},\"\n    end\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#923","level":2,"title":"9.2.3   効率の比較","text":"

グラフに \\(n\\) 個の頂点と \\(m\\) 本の辺があるとすると、次の表は隣接行列と隣接リストの時間効率および空間効率を比較したものです。なお、隣接リスト(連結リスト)は本記事の実装に対応し、隣接リスト(ハッシュテーブル)はすべての連結リストをハッシュテーブルに置き換えた実装を指します。

表 9-2   隣接行列と隣接リストの比較

隣接行列 隣接リスト(連結リスト) 隣接リスト(ハッシュテーブル) 隣接判定 \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) 辺の追加 \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) 辺の削除 \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) 頂点の追加 \\(O(n)\\) \\(O(1)\\) \\(O(1)\\) 頂点の削除 \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n)\\) メモリ使用量 \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n + m)\\)

上表を見ると、隣接リスト(ハッシュテーブル)の時間効率と空間効率が最も優れているように見えます。しかし実際には、隣接行列のほうが辺の操作効率は高く、必要なのは 1 回の配列アクセスまたは代入だけです。総合的に見ると、隣接行列は「空間を時間と引き換えにする」原則を体現し、隣接リストは「時間を空間と引き換えにする」原則を体現しています。

","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   グラフの走査","text":"

木は「一対多」の関係を表すのに対し、グラフはより高い自由度を持ち、任意の「多対多」の関係を表現できます。したがって、木はグラフの一種の特殊な場合とみなせます。明らかに、木の走査操作もグラフの走査操作の一種の特殊な場合です。

グラフと木はいずれも、走査操作を実現するために探索アルゴリズムを用いる必要があります。グラフの走査方法も、幅優先走査と深さ優先走査の 2 種類に分けられます。

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931","level":2,"title":"9.3.1   幅優先走査","text":"

幅優先走査は、近いところから遠いところへ向かう走査方法であり、ある頂点から出発して、常に最も近い頂点を優先して訪問し、層ごとに外側へ広がっていきます。以下の図に示すように、左上の頂点から出発し、まずその頂点のすべての隣接頂点を走査し、次に次の頂点のすべての隣接頂点を走査し、これを繰り返して、すべての頂点を訪問するまで続けます。

図 9-9   グラフの幅優先走査

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1","level":3,"title":"1.   アルゴリズムの実装","text":"

BFS は通常キューを用いて実装され、コードは以下のとおりです。キューは「先入れ先出し」という性質を持ち、これは BFS の「近いところから遠いところへ」という考え方と本質的に一致しています。

  1. 走査の開始頂点 startVet をキューに追加し、ループを開始します。
  2. ループの各反復で、キュー先頭の頂点を取り出して訪問を記録し、その後その頂点のすべての隣接頂点をキューの末尾に追加します。
  3. 手順 2. を繰り返し、すべての頂点が訪問されると終了します。

頂点の重複走査を防ぐために、どの頂点が訪問済みかを記録するハッシュ集合 visited を用います。

Tip

ハッシュ集合は、value を持たず key だけを格納するハッシュテーブルとみなせます。これは \\(O(1)\\) の時間計算量で key の追加・削除・検索・更新を行えます。key の一意性にもとづき、ハッシュ集合は通常、データの重複排除などの場面で用いられます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_bfs.py
def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"幅優先探索\"\"\"\n    # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する\n    # 頂点の走査順序\n    res = []\n    # 訪問済み頂点を記録するためのハッシュ集合\n    visited = set[Vertex]([start_vet])\n    # BFS の実装にキューを用いる\n    que = deque[Vertex]([start_vet])\n    # 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while len(que) > 0:\n        vet = que.popleft()  # 先頭の頂点をデキュー\n        res.append(vet)  # 訪問した頂点を記録\n        # この頂点のすべての隣接頂点を走査\n        for adj_vet in graph.adj_list[vet]:\n            if adj_vet in visited:\n                continue  # 訪問済みの頂点をスキップ\n            que.append(adj_vet)  # 未訪問の頂点のみをキューに追加\n            visited.add(adj_vet)  # この頂点を訪問済みにする\n    # 頂点の走査順を返す\n    return res\n
graph_bfs.cpp
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nvector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {\n    // 頂点の走査順序\n    vector<Vertex *> res;\n    // 訪問済み頂点を記録するためのハッシュ集合\n    unordered_set<Vertex *> visited = {startVet};\n    // BFS の実装にキューを用いる\n    queue<Vertex *> que;\n    que.push(startVet);\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (!que.empty()) {\n        Vertex *vet = que.front();\n        que.pop();          // 先頭の頂点をデキュー\n        res.push_back(vet); // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for (auto adjVet : graph.adjList[vet]) {\n            if (visited.count(adjVet))\n                continue;            // 訪問済みの頂点をスキップ\n            que.push(adjVet);        // 未訪問の頂点のみをキューに追加\n            visited.emplace(adjVet); // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res;\n}\n
graph_bfs.java
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n    // 頂点の走査順序\n    List<Vertex> res = new ArrayList<>();\n    // 訪問済み頂点を記録するためのハッシュ集合\n    Set<Vertex> visited = new HashSet<>();\n    visited.add(startVet);\n    // BFS の実装にキューを用いる\n    Queue<Vertex> que = new LinkedList<>();\n    que.offer(startVet);\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (!que.isEmpty()) {\n        Vertex vet = que.poll(); // 先頭の頂点をデキュー\n        res.add(vet);            // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for (Vertex adjVet : graph.adjList.get(vet)) {\n            if (visited.contains(adjVet))\n                continue;        // 訪問済みの頂点をスキップ\n            que.offer(adjVet);   // 未訪問の頂点のみをキューに追加\n            visited.add(adjVet); // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res;\n}\n
graph_bfs.cs
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nList<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {\n    // 頂点の走査順序\n    List<Vertex> res = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    HashSet<Vertex> visited = [startVet];\n    // BFS の実装にキューを用いる\n    Queue<Vertex> que = new();\n    que.Enqueue(startVet);\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (que.Count > 0) {\n        Vertex vet = que.Dequeue(); // 先頭の頂点をデキュー\n        res.Add(vet);               // 訪問した頂点を記録\n        foreach (Vertex adjVet in graph.adjList[vet]) {\n            if (visited.Contains(adjVet)) {\n                continue;          // 訪問済みの頂点をスキップ\n            }\n            que.Enqueue(adjVet);   // 未訪問の頂点のみをキューに追加\n            visited.Add(adjVet);   // この頂点を訪問済みにする\n        }\n    }\n\n    // 頂点の走査順を返す\n    return res;\n}\n
graph_bfs.go
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunc graphBFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // 頂点の走査順序\n    res := make([]Vertex, 0)\n    // 訪問済み頂点を記録するためのハッシュ集合\n    visited := make(map[Vertex]struct{})\n    visited[startVet] = struct{}{}\n    // キューは BFS の実装に用い、スライスでキューをシミュレートする\n    queue := make([]Vertex, 0)\n    queue = append(queue, startVet)\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    for len(queue) > 0 {\n        // 先頭の頂点をデキュー\n        vet := queue[0]\n        queue = queue[1:]\n        // 訪問した頂点を記録\n        res = append(res, vet)\n        // この頂点のすべての隣接頂点を走査\n        for _, adjVet := range g.adjList[vet] {\n            _, isExist := visited[adjVet]\n            // 未訪問の頂点のみをキューに追加\n            if !isExist {\n                queue = append(queue, adjVet)\n                visited[adjVet] = struct{}{}\n            }\n        }\n    }\n    // 頂点の走査順を返す\n    return res\n}\n
graph_bfs.swift
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunc graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // 頂点の走査順序\n    var res: [Vertex] = []\n    // 訪問済み頂点を記録するためのハッシュ集合\n    var visited: Set<Vertex> = [startVet]\n    // BFS の実装にキューを用いる\n    var que: [Vertex] = [startVet]\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while !que.isEmpty {\n        let vet = que.removeFirst() // 先頭の頂点をデキュー\n        res.append(vet) // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for adjVet in graph.adjList[vet] ?? [] {\n            if visited.contains(adjVet) {\n                continue // 訪問済みの頂点をスキップ\n            }\n            que.append(adjVet) // 未訪問の頂点のみをキューに追加\n            visited.insert(adjVet) // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res\n}\n
graph_bfs.js
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunction graphBFS(graph, startVet) {\n    // 頂点の走査順序\n    const res = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    const visited = new Set();\n    visited.add(startVet);\n    // BFS の実装にキューを用いる\n    const que = [startVet];\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (que.length) {\n        const vet = que.shift(); // 先頭の頂点をデキュー\n        res.push(vet); // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // 訪問済みの頂点をスキップ\n            }\n            que.push(adjVet); // 未訪問の頂点のみをキューに追加\n            visited.add(adjVet); // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res;\n}\n
graph_bfs.ts
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunction graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // 頂点の走査順序\n    const res: Vertex[] = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    const visited: Set<Vertex> = new Set();\n    visited.add(startVet);\n    // BFS の実装にキューを用いる\n    const que = [startVet];\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (que.length) {\n        const vet = que.shift(); // 先頭の頂点をデキュー\n        res.push(vet); // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // 訪問済みの頂点をスキップ\n            }\n            que.push(adjVet); // 未訪問のものだけをキューに入れる\n            visited.add(adjVet); // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res;\n}\n
graph_bfs.dart
/* 幅優先探索 */\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n  // 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する\n  // 頂点の走査順序\n  List<Vertex> res = [];\n  // 訪問済み頂点を記録するためのハッシュ集合\n  Set<Vertex> visited = {};\n  visited.add(startVet);\n  // BFS の実装にキューを用いる\n  Queue<Vertex> que = Queue();\n  que.add(startVet);\n  // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n  while (que.isNotEmpty) {\n    Vertex vet = que.removeFirst(); // 先頭の頂点をデキュー\n    res.add(vet); // 訪問した頂点を記録\n    // この頂点のすべての隣接頂点を走査\n    for (Vertex adjVet in graph.adjList[vet]!) {\n      if (visited.contains(adjVet)) {\n        continue; // 訪問済みの頂点をスキップ\n      }\n      que.add(adjVet); // 未訪問の頂点のみをキューに追加\n      visited.add(adjVet); // この頂点を訪問済みにする\n    }\n  }\n  // 頂点の走査順を返す\n  return res;\n}\n
graph_bfs.rs
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // 頂点の走査順序\n    let mut res = vec![];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    let mut visited = HashSet::new();\n    visited.insert(start_vet);\n    // BFS の実装にキューを用いる\n    let mut que = VecDeque::new();\n    que.push_back(start_vet);\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while let Some(vet) = que.pop_front() {\n        res.push(vet); // 訪問した頂点を記録\n\n        // この頂点のすべての隣接頂点を走査\n        if let Some(adj_vets) = graph.adj_list.get(&vet) {\n            for &adj_vet in adj_vets {\n                if visited.contains(&adj_vet) {\n                    continue; // 訪問済みの頂点をスキップ\n                }\n                que.push_back(adj_vet); // 未訪問の頂点のみをキューに追加\n                visited.insert(adj_vet); // この頂点を訪問済みにする\n            }\n        }\n    }\n    // 頂点の走査順を返す\n    res\n}\n
graph_bfs.c
/* ノードキュー構造体 */\ntypedef struct {\n    Vertex *vertices[MAX_SIZE];\n    int front, rear, size;\n} Queue;\n\n/* コンストラクタ */\nQueue *newQueue() {\n    Queue *q = (Queue *)malloc(sizeof(Queue));\n    q->front = q->rear = q->size = 0;\n    return q;\n}\n\n/* キューが空かどうかを判定 */\nint isEmpty(Queue *q) {\n    return q->size == 0;\n}\n\n/* エンキュー操作 */\nvoid enqueue(Queue *q, Vertex *vet) {\n    q->vertices[q->rear] = vet;\n    q->rear = (q->rear + 1) % MAX_SIZE;\n    q->size++;\n}\n\n/* デキュー操作 */\nVertex *dequeue(Queue *q) {\n    Vertex *vet = q->vertices[q->front];\n    q->front = (q->front + 1) % MAX_SIZE;\n    q->size--;\n    return vet;\n}\n\n/* 頂点が訪問済みかを確認 */\nint isVisited(Vertex **visited, int size, Vertex *vet) {\n    // 走査してノードを探すため、O(n) 時間を要する\n    for (int i = 0; i < size; i++) {\n        if (visited[i] == vet)\n            return 1;\n    }\n    return 0;\n}\n\n/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nvoid graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) {\n    // BFS の実装にキューを用いる\n    Queue *queue = newQueue();\n    enqueue(queue, startVet);\n    visited[(*visitedSize)++] = startVet;\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (!isEmpty(queue)) {\n        Vertex *vet = dequeue(queue); // 先頭の頂点をデキュー\n        res[(*resSize)++] = vet;      // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        AdjListNode *node = findNode(graph, vet);\n        while (node != NULL) {\n            // 訪問済みの頂点をスキップ\n            if (!isVisited(visited, *visitedSize, node->vertex)) {\n                enqueue(queue, node->vertex);             // 未訪問の頂点のみをキューに追加\n                visited[(*visitedSize)++] = node->vertex; // この頂点を訪問済みにする\n            }\n            node = node->next;\n        }\n    }\n    // メモリを解放する\n    free(queue);\n}\n
graph_bfs.kt
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {\n    // 頂点の走査順序\n    val res = mutableListOf<Vertex?>()\n    // 訪問済み頂点を記録するためのハッシュ集合\n    val visited = HashSet<Vertex>()\n    visited.add(startVet)\n    // BFS の実装にキューを用いる\n    val que = LinkedList<Vertex>()\n    que.offer(startVet)\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (!que.isEmpty()) {\n        val vet = que.poll() // 先頭の頂点をデキュー\n        res.add(vet)         // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for (adjVet in graph.adjList[vet]!!) {\n            if (visited.contains(adjVet))\n                continue        // 訪問済みの頂点をスキップ\n            que.offer(adjVet)   // 未訪問の頂点のみをキューに追加\n            visited.add(adjVet) // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res\n}\n
graph_bfs.rb
### 幅優先探索 ###\ndef graph_bfs(graph, start_vet)\n  # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する\n  # 頂点の走査順序\n  res = []\n  # 訪問済み頂点を記録するためのハッシュ集合\n  visited = Set.new([start_vet])\n  # BFS の実装にキューを用いる\n  que = [start_vet]\n  # 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n  while que.length > 0\n    vet = que.shift # 先頭の頂点をデキュー\n    res << vet # 訪問した頂点を記録\n    # この頂点のすべての隣接頂点を走査\n    for adj_vet in graph.adj_list[vet]\n      next if visited.include?(adj_vet) # 訪問済みの頂点をスキップ\n      que << adj_vet # 未訪問の頂点のみをキューに追加\n      visited.add(adj_vet) # この頂点を訪問済みにする\n    end\n  end\n  # 頂点の走査順を返す\n  res\nend\n
コードの可視化

全画面で見る >

コードはやや抽象的なので、以下の図と照らし合わせて理解を深めることを勧めます。

<1><2><3><4><5><6><7><8><9><10><11>

図 9-10   グラフの幅優先走査の手順

幅優先走査の順序列は一意ですか?

一意ではありません。幅優先走査は「近いところから遠いところへ」の順で走査することだけを要求し、同じ距離にある複数の頂点の走査順は任意に入れ替えて構いません。上図を例にすると、頂点 \\(1\\) と \\(3\\) の訪問順は交換でき、頂点 \\(2\\)、\\(4\\)、\\(6\\) の訪問順も任意に入れ替えられます。

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2","level":3,"title":"2.   計算量の分析","text":"

時間計算量:すべての頂点は 1 回ずつキューに入り、1 回ずつキューから出るため、\\(O(|V|)\\) 時間です。隣接頂点を走査する過程では、無向グラフであるため、すべての辺が \\(2\\) 回訪問され、\\(O(2|E|)\\) 時間です。したがって全体では \\(O(|V| + |E|)\\) 時間です。

空間計算量:リスト res、ハッシュ集合 visited、キュー que に含まれる頂点数は最大で \\(|V|\\) であるため、\\(O(|V|)\\) 空間です。

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#932","level":2,"title":"9.3.2   深さ優先走査","text":"

深さ優先走査は、まず行けるところまで進み、進めなくなったら戻る走査方法です。以下の図に示すように、左上の頂点から出発し、現在の頂点のある隣接頂点を訪問して、行き止まりに達するまで進んだら戻り、再び別の方向へ進んで行き止まりまで進んで戻る、ということを繰り返し、すべての頂点の走査が完了するまで続けます。

図 9-11   グラフの深さ優先走査

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1_1","level":3,"title":"1.   アルゴリズムの実装","text":"

この「行き止まりまで進んでから戻る」アルゴリズムのパターンは、通常再帰にもとづいて実装されます。幅優先走査と同様に、深さ優先走査でも、頂点の重複訪問を避けるために、訪問済みの頂点を記録するハッシュ集合 visited を用います。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_dfs.py
def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):\n    \"\"\"深さ優先走査の補助関数\"\"\"\n    res.append(vet)  # 訪問した頂点を記録\n    visited.add(vet)  # この頂点を訪問済みにする\n    # この頂点のすべての隣接頂点を走査\n    for adjVet in graph.adj_list[vet]:\n        if adjVet in visited:\n            continue  # 訪問済みの頂点をスキップ\n        # 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet)\n\ndef graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"深さ優先探索\"\"\"\n    # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する\n    # 頂点の走査順序\n    res = []\n    # 訪問済み頂点を記録するためのハッシュ集合\n    visited = set[Vertex]()\n    dfs(graph, visited, res, start_vet)\n    return res\n
graph_dfs.cpp
/* 深さ優先走査の補助関数 */\nvoid dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {\n    res.push_back(vet);   // 訪問した頂点を記録\n    visited.emplace(vet); // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for (Vertex *adjVet : graph.adjList[vet]) {\n        if (visited.count(adjVet))\n            continue; // 訪問済みの頂点をスキップ\n        // 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nvector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {\n    // 頂点の走査順序\n    vector<Vertex *> res;\n    // 訪問済み頂点を記録するためのハッシュ集合\n    unordered_set<Vertex *> visited;\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.java
/* 深さ優先走査の補助関数 */\nvoid dfs(GraphAdjList graph, Set<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.add(vet);     // 訪問した頂点を記録\n    visited.add(vet); // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for (Vertex adjVet : graph.adjList.get(vet)) {\n        if (visited.contains(adjVet))\n            continue; // 訪問済みの頂点をスキップ\n        // 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n    // 頂点の走査順序\n    List<Vertex> res = new ArrayList<>();\n    // 訪問済み頂点を記録するためのハッシュ集合\n    Set<Vertex> visited = new HashSet<>();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.cs
/* 深さ優先走査の補助関数 */\nvoid DFS(GraphAdjList graph, HashSet<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.Add(vet);     // 訪問した頂点を記録\n    visited.Add(vet); // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    foreach (Vertex adjVet in graph.adjList[vet]) {\n        if (visited.Contains(adjVet)) {\n            continue; // 訪問済みの頂点をスキップ\n        }\n        // 隣接頂点を再帰的に訪問\n        DFS(graph, visited, res, adjVet);\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nList<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {\n    // 頂点の走査順序\n    List<Vertex> res = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    HashSet<Vertex> visited = [];\n    DFS(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.go
/* 深さ優先走査の補助関数 */\nfunc dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) {\n    // append 操作は新しい参照を返すため、元の参照を新しい slice の参照で再代入する必要がある\n    *res = append(*res, vet)\n    visited[vet] = struct{}{}\n    // この頂点のすべての隣接頂点を走査\n    for _, adjVet := range g.adjList[vet] {\n        _, isExist := visited[adjVet]\n        // 隣接頂点を再帰的に訪問\n        if !isExist {\n            dfs(g, visited, res, adjVet)\n        }\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunc graphDFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // 頂点の走査順序\n    res := make([]Vertex, 0)\n    // 訪問済み頂点を記録するためのハッシュ集合\n    visited := make(map[Vertex]struct{})\n    dfs(g, visited, &res, startVet)\n    // 頂点の走査順を返す\n    return res\n}\n
graph_dfs.swift
/* 深さ優先走査の補助関数 */\nfunc dfs(graph: GraphAdjList, visited: inout Set<Vertex>, res: inout [Vertex], vet: Vertex) {\n    res.append(vet) // 訪問した頂点を記録\n    visited.insert(vet) // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for adjVet in graph.adjList[vet] ?? [] {\n        if visited.contains(adjVet) {\n            continue // 訪問済みの頂点をスキップ\n        }\n        // 隣接頂点を再帰的に訪問\n        dfs(graph: graph, visited: &visited, res: &res, vet: adjVet)\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunc graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // 頂点の走査順序\n    var res: [Vertex] = []\n    // 訪問済み頂点を記録するためのハッシュ集合\n    var visited: Set<Vertex> = []\n    dfs(graph: graph, visited: &visited, res: &res, vet: startVet)\n    return res\n}\n
graph_dfs.js
/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunction dfs(graph, visited, res, vet) {\n    res.push(vet); // 訪問した頂点を記録\n    visited.add(vet); // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // 訪問済みの頂点をスキップ\n        }\n        // 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunction graphDFS(graph, startVet) {\n    // 頂点の走査順序\n    const res = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    const visited = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.ts
/* 深さ優先走査の補助関数 */\nfunction dfs(\n    graph: GraphAdjList,\n    visited: Set<Vertex>,\n    res: Vertex[],\n    vet: Vertex\n): void {\n    res.push(vet); // 訪問した頂点を記録\n    visited.add(vet); // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // 訪問済みの頂点をスキップ\n        }\n        // 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunction graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // 頂点の走査順序\n    const res: Vertex[] = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    const visited: Set<Vertex> = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.dart
/* 深さ優先走査の補助関数 */\nvoid dfs(\n  GraphAdjList graph,\n  Set<Vertex> visited,\n  List<Vertex> res,\n  Vertex vet,\n) {\n  res.add(vet); // 訪問した頂点を記録\n  visited.add(vet); // この頂点を訪問済みにする\n  // この頂点のすべての隣接頂点を走査\n  for (Vertex adjVet in graph.adjList[vet]!) {\n    if (visited.contains(adjVet)) {\n      continue; // 訪問済みの頂点をスキップ\n    }\n    // 隣接頂点を再帰的に訪問\n    dfs(graph, visited, res, adjVet);\n  }\n}\n\n/* 深さ優先探索 */\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n  // 頂点の走査順序\n  List<Vertex> res = [];\n  // 訪問済み頂点を記録するためのハッシュ集合\n  Set<Vertex> visited = {};\n  dfs(graph, visited, res, startVet);\n  return res;\n}\n
graph_dfs.rs
/* 深さ優先走査の補助関数 */\nfn dfs(graph: &GraphAdjList, visited: &mut HashSet<Vertex>, res: &mut Vec<Vertex>, vet: Vertex) {\n    res.push(vet); // 訪問した頂点を記録\n    visited.insert(vet); // この頂点を訪問済みにする\n                         // この頂点のすべての隣接頂点を走査\n    if let Some(adj_vets) = graph.adj_list.get(&vet) {\n        for &adj_vet in adj_vets {\n            if visited.contains(&adj_vet) {\n                continue; // 訪問済みの頂点をスキップ\n            }\n            // 隣接頂点を再帰的に訪問\n            dfs(graph, visited, res, adj_vet);\n        }\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // 頂点の走査順序\n    let mut res = vec![];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    let mut visited = HashSet::new();\n    dfs(&graph, &mut visited, &mut res, start_vet);\n\n    res\n}\n
graph_dfs.c
/* 頂点が訪問済みかを確認 */\nint isVisited(Vertex **res, int size, Vertex *vet) {\n    // 走査してノードを探すため、O(n) 時間を要する\n    for (int i = 0; i < size; i++) {\n        if (res[i] == vet) {\n            return 1;\n        }\n    }\n    return 0;\n}\n\n/* 深さ優先走査の補助関数 */\nvoid dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) {\n    // 訪問した頂点を記録\n    res[(*resSize)++] = vet;\n    // この頂点のすべての隣接頂点を走査\n    AdjListNode *node = findNode(graph, vet);\n    while (node != NULL) {\n        // 訪問済みの頂点をスキップ\n        if (!isVisited(res, *resSize, node->vertex)) {\n            // 隣接頂点を再帰的に訪問\n            dfs(graph, res, resSize, node->vertex);\n        }\n        node = node->next;\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nvoid graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) {\n    dfs(graph, res, resSize, startVet);\n}\n
graph_dfs.kt
/* 深さ優先走査の補助関数 */\nfun dfs(\n    graph: GraphAdjList,\n    visited: MutableSet<Vertex?>,\n    res: MutableList<Vertex?>,\n    vet: Vertex?\n) {\n    res.add(vet)     // 訪問した頂点を記録\n    visited.add(vet) // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for (adjVet in graph.adjList[vet]!!) {\n        if (visited.contains(adjVet))\n            continue  // 訪問済みの頂点をスキップ\n        // 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet)\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {\n    // 頂点の走査順序\n    val res = mutableListOf<Vertex?>()\n    // 訪問済み頂点を記録するためのハッシュ集合\n    val visited = HashSet<Vertex?>()\n    dfs(graph, visited, res, startVet)\n    return res\n}\n
graph_dfs.rb
### 深さ優先探索の補助関数 ###\ndef dfs(graph, visited, res, vet)\n  res << vet # 訪問した頂点を記録\n  visited.add(vet) # この頂点を訪問済みにする\n  # この頂点のすべての隣接頂点を走査\n  for adj_vet in graph.adj_list[vet]\n    next if visited.include?(adj_vet) # 訪問済みの頂点をスキップ\n    # 隣接頂点を再帰的に訪問\n    dfs(graph, visited, res, adj_vet)\n  end\nend\n\n### 深さ優先探索 ###\ndef graph_dfs(graph, start_vet)\n  # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する\n  # 頂点の走査順序\n  res = []\n  # 訪問済み頂点を記録するためのハッシュ集合\n  visited = Set.new\n  dfs(graph, visited, res, start_vet)\n  res\nend\n
コードの可視化

全画面で見る >

深さ優先走査のアルゴリズムの流れは以下の図のとおりです。

  • **直線の破線は下向きの再帰呼び出し**を表し、新しい頂点を訪問するために新たな再帰メソッドが開始されたことを意味します。
  • **曲線の破線は上向きのバックトラック**を表し、この再帰メソッドがすでに戻って、呼び出し元の位置までたどり着いたことを意味します。

理解を深めるために、以下の図とコードを結びつけて、DFS 全体の過程を頭の中でシミュレーションする(あるいは紙に書き出す)ことを勧めます。各再帰メソッドがいつ開始し、いつ戻るかも含めて追ってみてください。

<1><2><3><4><5><6><7><8><9><10><11>

図 9-12   グラフの深さ優先走査の手順

深さ優先走査の順序列は一意ですか?

幅優先走査と同様に、深さ優先走査の順序列も一意ではありません。ある頂点が与えられたとき、どの方向を先に探索してもよく、つまり隣接頂点の順序は任意に入れ替えられ、それでも深さ優先走査になります。

木の走査を例にすると、「根 \\(\\rightarrow\\) 左 \\(\\rightarrow\\) 右」「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」「左 \\(\\rightarrow\\) 右 \\(\\rightarrow\\) 根」は、それぞれ先行順、中間順、後行順走査に対応します。これらは 3 種類の走査優先順位を示していますが、いずれも深さ優先走査に属します。

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2_1","level":3,"title":"2.   計算量の分析","text":"

時間計算量:すべての頂点は \\(1\\) 回ずつ訪問されるため、\\(O(|V|)\\) 時間です。すべての辺は \\(2\\) 回ずつ訪問されるため、\\(O(2|E|)\\) 時間です。したがって全体では \\(O(|V| + |E|)\\) 時間です。

空間計算量:リスト res とハッシュ集合 visited に含まれる頂点数は最大で \\(|V|\\) であり、再帰の深さも最大で \\(|V|\\) であるため、\\(O(|V|)\\) 空間です。

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/summary/","level":1,"title":"9.4   まとめ","text":"","path":["第 9 章   グラフ","9.4   まとめ"],"tags":[]},{"location":"chapter_graph/summary/#1","level":3,"title":"1.   重要なポイントの振り返り","text":"
  • グラフは頂点と辺から構成され、一組の頂点と一組の辺からなる集合として表せます。
  • 線形関係(連結リスト)や分治関係(木)と比べて、ネットワーク関係(グラフ)は自由度が高く、そのぶん複雑です。
  • 有向グラフの辺は方向性を持ち、連結グラフでは任意の頂点に到達でき、重み付きグラフの各辺は重み変数を含みます。
  • 隣接行列は行列を用いてグラフを表し、各行(列)が 1 つの頂点を表し、行列要素が辺を表します。\\(1\\) または \\(0\\) を用いて、2 つの頂点の間に辺があるかないかを示します。隣接行列は追加・削除・検索・更新の操作効率が高い一方で、より多くの空間を消費します。
  • 隣接リストは複数の連結リストを使ってグラフを表し、第 \\(i\\) 個の連結リストが頂点 \\(i\\) に対応し、その頂点に隣接するすべての頂点を格納します。隣接リストは隣接行列よりも省スペースですが、辺を探すために連結リストを走査する必要があるため、時間効率は低くなります。
  • 隣接リスト内の連結リストが長くなりすぎた場合は、赤黒木やハッシュテーブルに変換することで、検索効率を高められます。
  • アルゴリズムの考え方という観点では、隣接行列は「空間を時間と引き換えにする」ことを体現し、隣接リストは「時間を空間と引き換えにする」ことを体現します。
  • グラフは、ソーシャルネットワークや地下鉄路線など、さまざまな現実のシステムをモデル化するために使えます。
  • 木はグラフの特殊な一例であり、木の走査もグラフ走査の特殊な一例です。
  • グラフの幅優先探索は、近いところから遠いところへ、層ごとに広がっていく探索方法であり、通常はキューを使って実装します。
  • グラフの深さ優先探索は、まず行けるところまで進み、進めなくなったらバックトラックする探索方法であり、通常は再帰に基づいて実装します。
","path":["第 9 章   グラフ","9.4   まとめ"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:経路の定義は頂点列ですか、それとも辺列ですか?

Wikipedia では言語版ごとに定義が一致していません。英語版では「経路は辺の列」であり、中国語版では「経路は頂点の列」です。以下は英語版の原文です:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

本書では、経路を頂点列ではなく辺列とみなします。これは、2 つの頂点の間に複数の辺が存在する可能性があり、その場合は各辺がそれぞれ 1 本の経路に対応するためです。

Q:非連結グラフには到達できない頂点がありますか?

非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません。非連結グラフ全体を走査するには、グラフ内のすべての連結成分をたどれるように複数の始点を設定する必要があります。

Q:隣接リストにおいて、「その頂点に接続されたすべての頂点」の順序に決まりはありますか?

順序は任意でかまいません。ただし実際の応用では、頂点を追加した順序や頂点値の大小順など、特定の規則に従って並べ替える必要がある場合があります。そうすることで、「ある種の極値を持つ」頂点をすばやく見つけやすくなります。

","path":["第 9 章   グラフ","9.4   まとめ"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"第 15 章   貪欲法","text":"

Abstract

ヒマワリは太陽に向かって回り、自らが最も大きく成長できる可能性を常に追い求める。

貪欲戦略は、一回ごとの単純な選択を通じて、徐々に最適な答えへと導く。

","path":["第 15 章   貪欲法"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"章の内容","text":"
  • 15.1   貪欲法
  • 15.2   分数ナップサック問題
  • 15.3   最大容量問題
  • 15.4   最大積分割問題
  • 15.5   まとめ
","path":["第 15 章   貪欲法"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   分数ナップサック問題","text":"

Question

\\(n\\) 個の品物が与えられ、第 \\(i\\) 個の品物の重さは \\(wgt[i-1]\\)、価値は \\(val[i-1]\\) であり、容量が \\(cap\\) のナップサックがある。各品物は 1 回だけ選択できるが、品物の一部を選ぶこともでき、価値は選択した重量の割合に応じて計算される。容量制限の下でナップサック内の品物の最大価値を求めよ。例を以下に示す。

図 15-3   分数ナップサック問題の例データ

分数ナップサック問題は 0-1 ナップサック問題と全体として非常によく似ており、状態には現在の品物 \\(i\\) と容量 \\(c\\) が含まれ、目標は容量制限下での最大価値を求めることである。

異なる点は、本問では品物の一部だけを選べることである。以下に示すように、品物は任意に分割でき、対応する価値は重量の割合に応じて計算される。

  1. 品物 \\(i\\) について、単位重量あたりの価値は \\(val[i-1] / wgt[i-1]\\) であり、これを単位価値と呼ぶ。
  2. 品物 \\(i\\) の一部を重さ \\(w\\) だけ入れると、ナップサックに増える価値は \\(w \\times val[i-1] / wgt[i-1]\\) となる。

図 15-4   品物の単位重量あたりの価値

","path":["第 15 章   貪欲法","15.2   分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#1","level":3,"title":"1.   貪欲戦略の決定","text":"

ナップサック内の品物の総価値を最大化することは、**本質的には単位重量あたりの品物価値を最大化すること**である。そこから、以下に示す貪欲戦略を導ける。

  1. 品物を単位価値の高い順にソートする。
  2. すべての品物を走査し、各回で単位価値が最も高い品物を貪欲に選択する。
  3. 残りのナップサック容量が足りない場合は、現在の品物の一部を使ってナップサックを満たす。

図 15-5   分数ナップサック問題の貪欲戦略

","path":["第 15 章   貪欲法","15.2   分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#2","level":3,"title":"2.   コード実装","text":"

品物を単位価値でソートできるように、Item クラスを定義する。貪欲選択を繰り返し、ナップサックが満杯になったら終了して解を返す。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fractional_knapsack.py
class Item:\n    \"\"\"品物\"\"\"\n\n    def __init__(self, w: int, v: int):\n        self.w = w  # 品物の重さ\n        self.v = v  # 品物の価値\n\ndef fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"分数ナップサック:貪欲法\"\"\"\n    # 重さと価値の 2 属性を持つ品物リストを作成\n    items = [Item(w, v) for w, v in zip(wgt, val)]\n    # 単位価値 item.v / item.w の高い順にソートする\n    items.sort(key=lambda item: item.v / item.w, reverse=True)\n    # 貪欲選択を繰り返す\n    res = 0\n    for item in items:\n        if item.w <= cap:\n            # 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v\n            cap -= item.w\n        else:\n            # 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (item.v / item.w) * cap\n            # 残り容量がないため、ループを抜ける\n            break\n    return res\n
fractional_knapsack.cpp
/* 品物 */\nclass Item {\n  public:\n    int w; // 品物の重さ\n    int v; // 品物の価値\n\n    Item(int w, int v) : w(w), v(v) {\n    }\n};\n\n/* 分数ナップサック:貪欲法 */\ndouble fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    vector<Item> items;\n    for (int i = 0; i < wgt.size(); i++) {\n        items.push_back(Item(wgt[i], val[i]));\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });\n    // 貪欲選択を繰り返す\n    double res = 0;\n    for (auto &item : items) {\n        if (item.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (double)item.v / item.w * cap;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.java
/* 品物 */\nclass Item {\n    int w; // 品物の重さ\n    int v; // 品物の価値\n\n    public Item(int w, int v) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* 分数ナップサック:貪欲法 */\ndouble fractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    Item[] items = new Item[wgt.length];\n    for (int i = 0; i < wgt.length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));\n    // 貪欲選択を繰り返す\n    double res = 0;\n    for (Item item : items) {\n        if (item.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (double) item.v / item.w * cap;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.cs
/* 品物 */\nclass Item(int w, int v) {\n    public int w = w; // 品物の重さ\n    public int v = v; // 品物の価値\n}\n\n/* 分数ナップサック:貪欲法 */\ndouble FractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    Item[] items = new Item[wgt.Length];\n    for (int i = 0; i < wgt.Length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));\n    // 貪欲選択を繰り返す\n    double res = 0;\n    foreach (Item item in items) {\n        if (item.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (double)item.v / item.w * cap;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.go
/* 品物 */\ntype Item struct {\n    w int // 品物の重さ\n    v int // 品物の価値\n}\n\n/* 分数ナップサック:貪欲法 */\nfunc fractionalKnapsack(wgt []int, val []int, cap int) float64 {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    items := make([]Item, len(wgt))\n    for i := 0; i < len(wgt); i++ {\n        items[i] = Item{wgt[i], val[i]}\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    sort.Slice(items, func(i, j int) bool {\n        return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)\n    })\n    // 貪欲選択を繰り返す\n    res := 0.0\n    for _, item := range items {\n        if item.w <= cap {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += float64(item.v)\n            cap -= item.w\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += float64(item.v) / float64(item.w) * float64(cap)\n            // 残り容量がないため、ループを抜ける\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.swift
/* 品物 */\nclass Item {\n    var w: Int // 品物の重さ\n    var v: Int // 品物の価値\n\n    init(w: Int, v: Int) {\n        self.w = w\n        self.v = v\n    }\n}\n\n/* 分数ナップサック:貪欲法 */\nfunc fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    var items = zip(wgt, val).map { Item(w: $0, v: $1) }\n    // 単位価値 item.v / item.w の高い順にソートする\n    items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }\n    // 貪欲選択を繰り返す\n    var res = 0.0\n    var cap = cap\n    for item in items {\n        if item.w <= cap {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += Double(item.v)\n            cap -= item.w\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += Double(item.v) / Double(item.w) * Double(cap)\n            // 残り容量がないため、ループを抜ける\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.js
/* 品物 */\nclass Item {\n    constructor(w, v) {\n        this.w = w; // 品物の重さ\n        this.v = v; // 品物の価値\n    }\n}\n\n/* 分数ナップサック:貪欲法 */\nfunction fractionalKnapsack(wgt, val, cap) {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    const items = wgt.map((w, i) => new Item(w, val[i]));\n    // 単位価値 item.v / item.w の高い順にソートする\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // 貪欲選択を繰り返す\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (item.v / item.w) * cap;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.ts
/* 品物 */\nclass Item {\n    w: number; // 品物の重さ\n    v: number; // 品物の価値\n\n    constructor(w: number, v: number) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* 分数ナップサック:貪欲法 */\nfunction fractionalKnapsack(wgt: number[], val: number[], cap: number): number {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));\n    // 単位価値 item.v / item.w の高い順にソートする\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // 貪欲選択を繰り返す\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (item.v / item.w) * cap;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.dart
/* 品物 */\nclass Item {\n  int w; // 品物の重さ\n  int v; // 品物の価値\n\n  Item(this.w, this.v);\n}\n\n/* 分数ナップサック:貪欲法 */\ndouble fractionalKnapsack(List<int> wgt, List<int> val, int cap) {\n  // 重さと価値の 2 属性を持つ品物リストを作成\n  List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));\n  // 単位価値 item.v / item.w の高い順にソートする\n  items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));\n  // 貪欲選択を繰り返す\n  double res = 0;\n  for (Item item in items) {\n    if (item.w <= cap) {\n      // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n      res += item.v;\n      cap -= item.w;\n    } else {\n      // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n      res += item.v / item.w * cap;\n      // 残り容量がないため、ループを抜ける\n      break;\n    }\n  }\n  return res;\n}\n
fractional_knapsack.rs
/* 品物 */\nstruct Item {\n    w: i32, // 品物の重さ\n    v: i32, // 品物の価値\n}\n\nimpl Item {\n    fn new(w: i32, v: i32) -> Self {\n        Self { w, v }\n    }\n}\n\n/* 分数ナップサック:貪欲法 */\nfn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    let mut items = wgt\n        .iter()\n        .zip(val.iter())\n        .map(|(&w, &v)| Item::new(w, v))\n        .collect::<Vec<Item>>();\n    // 単位価値 item.v / item.w の高い順にソートする\n    items.sort_by(|a, b| {\n        (b.v as f64 / b.w as f64)\n            .partial_cmp(&(a.v as f64 / a.w as f64))\n            .unwrap()\n    });\n    // 貪欲選択を繰り返す\n    let mut res = 0.0;\n    for item in &items {\n        if item.w <= cap {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v as f64;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += item.v as f64 / item.w as f64 * cap as f64;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    res\n}\n
fractional_knapsack.c
/* 品物 */\ntypedef struct {\n    int w; // 品物の重さ\n    int v; // 品物の価値\n} Item;\n\n/* 分数ナップサック:貪欲法 */\nfloat fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    Item *items = malloc(sizeof(Item) * itemCount);\n    for (int i = 0; i < itemCount; i++) {\n        items[i] = (Item){.w = wgt[i], .v = val[i]};\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);\n    // 貪欲選択を繰り返す\n    float res = 0.0;\n    for (int i = 0; i < itemCount; i++) {\n        if (items[i].w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += items[i].v;\n            cap -= items[i].w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (float)cap / items[i].w * items[i].v;\n            cap = 0;\n            break;\n        }\n    }\n    free(items);\n    return res;\n}\n
fractional_knapsack.kt
/* 品物 */\nclass Item(\n    val w: Int, // 品物\n    val v: Int  // 品物の価値\n)\n\n/* 分数ナップサック:貪欲法 */\nfun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    var cap = c\n    val items = arrayOfNulls<Item>(wgt.size)\n    for (i in wgt.indices) {\n        items[i] = Item(wgt[i], _val[i])\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }\n    // 貪欲選択を繰り返す\n    var res = 0.0\n    for (item in items) {\n        if (item!!.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v\n            cap -= item.w\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += item.v.toDouble() / item.w * cap\n            // 残り容量がないため、ループを抜ける\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.rb
### アイテム ###\nclass Item\n  attr_accessor :w # 品物の重さ\n  attr_accessor :v # 品物の価値\n\n  def initialize(w, v)\n    @w = w\n    @v = v\n  end\nend\n\n### 分数ナップサック:貪欲法 ###\ndef fractional_knapsack(wgt, val, cap)\n  # 重さと価値の 2 属性を持つ品物リストを作成する\n  items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }\n  # 単位価値 item.v / item.w の高い順にソートする\n  items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }\n  # 貪欲選択を繰り返す\n  res = 0\n  for item in items\n    if item.w <= cap\n      # 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n      res += item.v\n      cap -= item.w\n    else\n      # 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n      res += (item.v.to_f / item.w) * cap\n      # 残り容量がないため、ループを抜ける\n      break\n    end\n  end\n  res\nend\n
コードの可視化

全画面で見る >

組み込みのソートアルゴリズムの時間計算量は通常 \\(O(\\log n)\\)、空間計算量は通常 \\(O(\\log n)\\) または \\(O(n)\\) であり、具体的な値はプログラミング言語の実装に依存する。

ソートを除けば、最悪の場合は品物リスト全体を走査する必要があるため、時間計算量は \\(O(n)\\) であり、ここで \\(n\\) は品物数である。

Item オブジェクトのリストを初期化しているため、空間計算量は \\(O(n)\\) である。

","path":["第 15 章   貪欲法","15.2   分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#3","level":3,"title":"3.   正しさの証明","text":"

背理法を用いる。品物 \\(x\\) が単位価値最大の品物であり、あるアルゴリズムで得られた最大価値を res とするが、その解には品物 \\(x\\) が含まれていないと仮定する。

ここでナップサックから単位重量の任意の品物を取り出し、単位重量の品物 \\(x\\) に置き換える。品物 \\(x\\) の単位価値が最大であるため、置き換え後の総価値は必ず res より大きくなる。これは res が最適解であることに矛盾し、最適解には必ず品物 \\(x\\) が含まれなければならないことを示す。

この解に含まれる他の品物についても、同様の矛盾を構成できる。要するに、単位価値がより大きい品物は常により良い選択である。これは貪欲戦略が有効であることを示している。

以下に示すように、品物の重さと品物の単位価値をそれぞれ二次元グラフの横軸と縦軸とみなすと、分数ナップサック問題は「有限な横軸区間で囲まれる最大面積を求める問題」に変換できる。この類比は、幾何学的な観点から貪欲戦略の有効性を理解する助けになる。

図 15-6   分数ナップサック問題の幾何学的表現

","path":["第 15 章   貪欲法","15.2   分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/","level":1,"title":"15.1   貪欲法","text":"

貪欲法(greedy algorithm)は、最適化問題を解くための一般的なアルゴリズムです。その基本的な考え方は、問題の各意思決定段階において、その時点で最善に見える選択を行い、すなわち貪欲に局所最適な決定を下すことで、大域最適解を得ようとするものです。貪欲法は簡潔で効率的であり、多くの実際の問題で広く用いられています。

貪欲法と動的計画法は、どちらも最適化問題を解く際によく用いられます。両者には、最適部分構造に依存するなどの共通点がありますが、その動作原理は異なります。

  • 動的計画法は、前の段階までのすべての決定に基づいて現在の決定を考え、過去の部分問題の解を用いて現在の部分問題の解を構築します。
  • 貪欲法は過去の決定を考慮せず、ひたすら前に進みながら貪欲な選択を行い、問題の範囲を縮小し続けて、最終的に問題を解決します。

まずは例題「コイン両替」を通して、貪欲法の仕組みを理解しましょう。この問題はすでに「完全ナップサック問題」の節で紹介しているので、見覚えがあるはずです。

Question

\\(n\\) 種類の硬貨が与えられ、\\(i\\) 番目の硬貨の額面は \\(coins[i - 1]\\) 、目標金額は \\(amt\\) です。各硬貨は何度でも選べるとき、目標金額を作るために必要な最小の硬貨枚数を求めてください。目標金額を作れない場合は \\(-1\\) を返します。

この問題で採用する貪欲戦略は下図のとおりです。目標金額が与えられたら、それを超えず、かつ最も近い硬貨を貪欲に選択し、この手順を目標金額を作り切るまで繰り返します。

図 15-1   コイン両替の貪欲戦略

実装コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_greedy.py
def coin_change_greedy(coins: list[int], amt: int) -> int:\n    \"\"\"コイン交換:貪欲法\"\"\"\n    # coins リストはソート済みと仮定する\n    i = len(coins) - 1\n    count = 0\n    # 残額がなくなるまで貪欲選択を繰り返す\n    while amt > 0:\n        # 残額以下で最も近い硬貨を見つける\n        while i > 0 and coins[i] > amt:\n            i -= 1\n        # coins[i] を選択する\n        amt -= coins[i]\n        count += 1\n    # 実行可能な解が見つからなければ -1 を返す\n    return count if amt == 0 else -1\n
coin_change_greedy.cpp
/* コイン交換:貪欲法 */\nint coinChangeGreedy(vector<int> &coins, int amt) {\n    // coins リストはソート済みと仮定する\n    int i = coins.size() - 1;\n    int count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.java
/* コイン交換:貪欲法 */\nint coinChangeGreedy(int[] coins, int amt) {\n    // coins リストはソート済みと仮定する\n    int i = coins.length - 1;\n    int count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.cs
/* コイン交換:貪欲法 */\nint CoinChangeGreedy(int[] coins, int amt) {\n    // coins リストはソート済みと仮定する\n    int i = coins.Length - 1;\n    int count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.go
/* コイン交換:貪欲法 */\nfunc coinChangeGreedy(coins []int, amt int) int {\n    // coins リストはソート済みと仮定する\n    i := len(coins) - 1\n    count := 0\n    // 残額がなくなるまで貪欲選択を繰り返す\n    for amt > 0 {\n        // 残額以下で最も近い硬貨を見つける\n        for i > 0 && coins[i] > amt {\n            i--\n        }\n        // coins[i] を選択する\n        amt -= coins[i]\n        count++\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    if amt != 0 {\n        return -1\n    }\n    return count\n}\n
coin_change_greedy.swift
/* コイン交換:貪欲法 */\nfunc coinChangeGreedy(coins: [Int], amt: Int) -> Int {\n    // coins リストはソート済みと仮定する\n    var i = coins.count - 1\n    var count = 0\n    var amt = amt\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while amt > 0 {\n        // 残額以下で最も近い硬貨を見つける\n        while i > 0 && coins[i] > amt {\n            i -= 1\n        }\n        // coins[i] を選択する\n        amt -= coins[i]\n        count += 1\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt == 0 ? count : -1\n}\n
coin_change_greedy.js
/* コイン交換:貪欲法 */\nfunction coinChangeGreedy(coins, amt) {\n    // coins 配列はソート済みと仮定する\n    let i = coins.length - 1;\n    let count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt === 0 ? count : -1;\n}\n
coin_change_greedy.ts
/* コイン交換:貪欲法 */\nfunction coinChangeGreedy(coins: number[], amt: number): number {\n    // coins 配列はソート済みと仮定する\n    let i = coins.length - 1;\n    let count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt === 0 ? count : -1;\n}\n
coin_change_greedy.dart
/* コイン交換:貪欲法 */\nint coinChangeGreedy(List<int> coins, int amt) {\n  // coins リストはソート済みと仮定する\n  int i = coins.length - 1;\n  int count = 0;\n  // 残額がなくなるまで貪欲選択を繰り返す\n  while (amt > 0) {\n    // 残額以下で最も近い硬貨を見つける\n    while (i > 0 && coins[i] > amt) {\n      i--;\n    }\n    // coins[i] を選択する\n    amt -= coins[i];\n    count++;\n  }\n  // 実行可能な解が見つからなければ -1 を返す\n  return amt == 0 ? count : -1;\n}\n
coin_change_greedy.rs
/* コイン交換:貪欲法 */\nfn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {\n    // coins リストはソート済みと仮定する\n    let mut i = coins.len() - 1;\n    let mut count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while amt > 0 {\n        // 残額以下で最も近い硬貨を見つける\n        while i > 0 && coins[i] > amt {\n            i -= 1;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count += 1;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    if amt == 0 {\n        count\n    } else {\n        -1\n    }\n}\n
coin_change_greedy.c
/* コイン交換:貪欲法 */\nint coinChangeGreedy(int *coins, int size, int amt) {\n    // coins リストはソート済みと仮定する\n    int i = size - 1;\n    int count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.kt
/* コイン交換:貪欲法 */\nfun coinChangeGreedy(coins: IntArray, amt: Int): Int {\n    // coins リストはソート済みと仮定する\n    var am = amt\n    var i = coins.size - 1\n    var count = 0\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (am > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > am) {\n            i--\n        }\n        // coins[i] を選択する\n        am -= coins[i]\n        count++\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return if (am == 0) count else -1\n}\n
coin_change_greedy.rb
### コイン両替:貪欲法 ###\ndef coin_change_greedy(coins, amt)\n  # coins リストはソート済みと仮定する\n  i = coins.length - 1\n  count = 0\n  # 残額がなくなるまで貪欲選択を繰り返す\n  while amt > 0\n    # 残額以下で最も近い硬貨を見つける\n    while i > 0 && coins[i] > amt\n      i -= 1\n    end\n    # coins[i] を選択する\n    amt -= coins[i]\n    count += 1\n  end\n  # 実行可能な解が見つからなければ `-1` を返す\n  amt == 0 ? count : -1\nend\n
コードの可視化

全画面で見る >

思わずこう言いたくなるかもしれません。So clean!貪欲法はわずか十行ほどのコードでコイン両替問題を解いてしまいます。

","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1511","level":2,"title":"15.1.1   貪欲法の利点と限界","text":"

**貪欲法は操作が直接的で実装が簡単なだけでなく、通常は効率も高い**です。上のコードでは、硬貨の最小額面を \\(\\min(coins)\\) とすると、貪欲選択のループ回数は高々 \\(amt / \\min(coins)\\) 回であり、時間計算量は \\(O(amt / \\min(coins))\\) です。これは動的計画法による解法の時間計算量 \\(O(n \\times amt)\\) より 1 桁小さいオーダーです。

しかし、硬貨の額面の組み合わせによっては、貪欲法では最適解を見つけられません。下図に 2 つの例を示します。

  • 正例 \\(coins = [1, 5, 10, 20, 50, 100]\\):この硬貨の組み合わせでは、任意の \\(amt\\) に対して貪欲法で最適解を見つけられます。
  • 反例 \\(coins = [1, 20, 50]\\):\\(amt = 60\\) とすると、貪欲法では \\(50 + 1 \\times 10\\) という両替しか見つからず、硬貨は合計 \\(11\\) 枚になります。しかし動的計画法なら最適解 \\(20 + 20 + 20\\) を見つけられ、必要なのはわずか \\(3\\) 枚です。
  • 反例 \\(coins = [1, 49, 50]\\):\\(amt = 98\\) とすると、貪欲法では \\(50 + 1 \\times 48\\) という両替しか見つからず、硬貨は合計 \\(49\\) 枚になります。しかし動的計画法なら最適解 \\(49 + 49\\) を見つけられ、必要なのはわずか \\(2\\) 枚です。

図 15-2   貪欲法では最適解を見つけられない例

つまり、コイン両替問題に対して、貪欲法は大域最適解を保証できず、非常に悪い解を見つけてしまうこともあります。この問題は動的計画法で解くほうが適しています。

一般に、貪欲法が適用できる状況は次の 2 つに分けられます。

  1. 最適解を保証できる場合:この場合、貪欲法はしばしば最良の選択です。多くの場合、バックトラッキングや動的計画法より効率的だからです。
  2. 近似最適解を見つけられる場合:この場合も貪欲法は有効です。多くの複雑な問題では、大域最適解を求めること自体が非常に難しく、より高い効率で準最適解を得られるだけでも十分価値があります。
","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512","level":2,"title":"15.1.2   貪欲法の特性","text":"

では、どのような問題が貪欲法に適しているのでしょうか。言い換えると、貪欲法はどのような場合に最適解を保証できるのでしょうか。

動的計画法と比べると、貪欲法の適用条件はより厳しく、主に次の 2 つの性質に注目します。

  • 貪欲選択性:局所最適な選択が常に大域最適解につながる場合にのみ、貪欲法は最適解を保証できます。
  • 最適部分構造:元の問題の最適解が、部分問題の最適解を含むことです。

最適部分構造については「動的計画法」の節ですでに紹介したので、ここでは繰り返しません。なお、問題によっては最適部分構造が明確でなくても、貪欲法で解ける場合があります。

ここでは主に、貪欲選択性をどのように判定するかを考えます。説明だけを見ると単純そうですが、実際には多くの問題で、貪欲選択性を証明するのは容易ではありません。

たとえばコイン両替問題では、反例を挙げて貪欲選択性が成り立たないことを示すのは簡単ですが、成り立つことを証明するのは難しいです。もし、**どのような条件を満たす硬貨の組み合わせなら貪欲法で解けるのか**と問われると、直感や例示に頼った曖昧な答えしか出せず、厳密な数学的証明を与えるのは困難です。

Quote

ある論文では、ある硬貨の組み合わせについて、任意の金額に対する最適解を貪欲法で求められるかどうかを判定する、時間計算量 \\(O(n^3)\\) のアルゴリズムが示されています。

Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.

","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1513","level":2,"title":"15.1.3   貪欲法の問題解決手順","text":"

貪欲法による問題解決の流れは、おおむね次の 3 段階に分けられます。

  1. 問題分析:状態の定義、最適化目標、制約条件などを整理し、問題の性質を理解します。この段階はバックトラッキングや動的計画法でも共通して現れます。
  2. 貪欲戦略の決定:各ステップでどのように貪欲選択を行うかを定めます。この戦略により各ステップで問題規模を縮小し、最終的に問題全体を解決します。
  3. 正しさの証明:通常は、その問題が貪欲選択性と最適部分構造を持つことを示す必要があります。この段階では、帰納法や背理法などの数学的証明が必要になることがあります。

貪欲戦略を定めることは問題解決の核心ですが、実際には簡単ではないことも多く、主な理由は次のとおりです。

  • 問題ごとに貪欲戦略の差が大きい。多くの問題では貪欲戦略は比較的わかりやすく、おおまかな考察や試行だけで見つけられます。しかし複雑な問題では、貪欲戦略が非常に見えにくいことがあり、その場合は解法経験やアルゴリズム力が大きく問われます。
  • 一見もっともらしい貪欲戦略もある。自信を持って貪欲戦略を設計し、コードを書いて提出しても、一部のテストケースを通過できないことがあります。これは、その貪欲戦略が「部分的にしか正しくない」ためであり、先ほどのコイン両替は典型例です。

正しさを保証するためには、貪欲戦略に対して厳密な数学的証明を行うべきであり、通常は背理法や数学的帰納法が必要になります。

しかし、正しさの証明もまた簡単とは限りません。手がかりがない場合には、テストケースを使ってコードをデバッグしながら、貪欲戦略を少しずつ修正して検証していくことがよくあります。

","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514","level":2,"title":"15.1.4   貪欲法の典型問題","text":"

貪欲法は、貪欲選択性と最適部分構造を満たす最適化問題によく用いられます。以下に典型的な貪欲法の問題をいくつか挙げます。

  • 硬貨のお釣り問題:ある種の硬貨の組み合わせでは、貪欲法で常に最適解が得られます。
  • 区間スケジューリング問題:いくつかのタスクがあり、それぞれがある時間区間で実行されるとします。できるだけ多くのタスクを完了することが目標で、毎回終了時刻が最も早いタスクを選ぶなら、貪欲法で最適解を得られます。
  • 分数ナップサック問題:一群の品物と積載容量が与えられたとき、総重量が容量を超えず、かつ総価値が最大になるように品物を選ぶ問題です。毎回、価値対重量比(価値 / 重量)が最も高い品物を選ぶなら、ある条件下で貪欲法は最適解を得られます。
  • 株式売買問題:株価の履歴が与えられ、複数回の売買が可能ですが、すでに株を保有している場合は売却前に再度購入することはできません。目標は最大利益を得ることです。
  • ハフマン符号化:ハフマン符号化は、可逆データ圧縮に用いられる貪欲法です。ハフマン木を構築する際、毎回出現頻度が最も低い 2 つのノードを選んで併合すると、最終的に得られるハフマン木の重み付きパス長(符号長)は最小になります。
  • Dijkstra アルゴリズム:与えられた始点から他の各頂点への最短経路問題を解く貪欲法です。
","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   最大容量問題","text":"

Question

配列 \\(ht\\) が与えられ、各要素は垂直な仕切り板の高さを表します。配列内の任意の 2 枚の仕切り板と、その間の空間で容器を構成できます。

容器の容量は高さと幅の積(面積)に等しく、高さは短い方の仕切り板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。

配列から 2 枚の仕切り板を選び、構成される容器の容量が最大となるようにしてください。最大容量を返します。例を以下の図に示します。

図 15-7   最大容量問題のサンプルデータ

容器は任意の 2 枚の仕切り板で囲まれるため、本問の状態は 2 枚の仕切り板のインデックスで表され、\\([i, j]\\) と記します。

問題の条件より、容量は高さと幅の積に等しく、高さは短い板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。容量を \\(cap[i, j]\\) とすると、計算式は次のようになります。

\\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\]

配列の長さを \\(n\\) とすると、2 枚の仕切り板の組合せ数(状態総数)は \\(C_n^2 = \\frac{n(n - 1)}{2}\\) 個です。最も直接的には、すべての状態を総当たりできます。これにより最大容量を求められ、時間計算量は \\(O(n^2)\\) です。

","path":["第 15 章   貪欲法","15.3   最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#1","level":3,"title":"1.   貪欲戦略の決定","text":"

この問題にはさらに効率的な解法があります。以下の図のように、状態 \\([i, j]\\) を 1 つ選び、インデックスが \\(i < j\\) かつ高さが \\(ht[i] < ht[j]\\) を満たすとします。つまり、\\(i\\) が短い板、\\(j\\) が長い板です。

図 15-8   初期状態

以下の図のように、このとき長い板 \\(j\\) を短い板 \\(i\\) に近づけると、容量は必ず小さくなります。

これは、長い板 \\(j\\) を動かした後は幅 \\(j-i\\) が必ず小さくなるためです。また、高さは短い板で決まるので、高さは変わらない( \\(i\\) が依然として短い板)か、小さくなる(移動後の \\(j\\) が短い板になる)ことしかありません。

図 15-9   長い板を内側へ動かした後の状態

逆に考えると、短い板 \\(i\\) を内側へ縮めた場合にのみ、容量が大きくなる可能性があります。幅は必ず小さくなりますが、**高さは大きくなる可能性がある**からです(移動後の短い板 \\(i\\) がより長くなる可能性があります)。たとえば次の図では、短い板を動かした後に面積が大きくなっています。

図 15-10   短い板を内側へ動かした後の状態

以上から、本問の貪欲戦略を導けます。2 本のポインタを初期化して容器の両端に置き、各ラウンドで短い板に対応するポインタを内側へ縮め、2 本のポインタが出会うまで続けます。

以下の図は、貪欲戦略の実行過程を示しています。

  1. 初期状態では、ポインタ \\(i\\) と \\(j\\) は配列の両端にあります。
  2. 現在の状態の容量 \\(cap[i, j]\\) を計算し、最大容量を更新します。
  3. 板 \\(i\\) と板 \\(j\\) の高さを比較し、短い板を内側へ 1 マス移動します。
  4. 2.3. を繰り返し実行し、\\(i\\) と \\(j\\) が出会ったら終了します。
<1><2><3><4><5><6><7><8><9>

図 15-11   最大容量問題の貪欲な過程

","path":["第 15 章   貪欲法","15.3   最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#2","level":3,"title":"2.   コード実装","text":"

コードのループ回数は最大でも \\(n\\) 回であるため、時間計算量は \\(O(n)\\) です。

変数 \\(i\\)、\\(j\\)、\\(res\\) が使う追加領域は定数サイズなので、空間計算量は \\(O(1)\\) です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_capacity.py
def max_capacity(ht: list[int]) -> int:\n    \"\"\"最大容量:貪欲法\"\"\"\n    # i, j を初期化し、それぞれ配列の両端に置く\n    i, j = 0, len(ht) - 1\n    # 初期の最大容量は 0\n    res = 0\n    # 2 枚の板が出会うまで貪欲選択を繰り返す\n    while i < j:\n        # 最大容量を更新する\n        cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        # 短い方を内側へ動かす\n        if ht[i] < ht[j]:\n            i += 1\n        else:\n            j -= 1\n    return res\n
max_capacity.cpp
/* 最大容量:貪欲法 */\nint maxCapacity(vector<int> &ht) {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    int i = 0, j = ht.size() - 1;\n    // 初期の最大容量は 0\n    int res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        int cap = min(ht[i], ht[j]) * (j - i);\n        res = max(res, cap);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.java
/* 最大容量:貪欲法 */\nint maxCapacity(int[] ht) {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    int i = 0, j = ht.length - 1;\n    // 初期の最大容量は 0\n    int res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        int cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.cs
/* 最大容量:貪欲法 */\nint MaxCapacity(int[] ht) {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    int i = 0, j = ht.Length - 1;\n    // 初期の最大容量は 0\n    int res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        int cap = Math.Min(ht[i], ht[j]) * (j - i);\n        res = Math.Max(res, cap);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.go
/* 最大容量:貪欲法 */\nfunc maxCapacity(ht []int) int {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    i, j := 0, len(ht)-1\n    // 初期の最大容量は 0\n    res := 0\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    for i < j {\n        // 最大容量を更新する\n        capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)\n        res = int(math.Max(float64(res), float64(capacity)))\n        // 短い方を内側へ動かす\n        if ht[i] < ht[j] {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
max_capacity.swift
/* 最大容量:貪欲法 */\nfunc maxCapacity(ht: [Int]) -> Int {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    var i = ht.startIndex, j = ht.endIndex - 1\n    // 初期の最大容量は 0\n    var res = 0\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while i < j {\n        // 最大容量を更新する\n        let cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // 短い方を内側へ動かす\n        if ht[i] < ht[j] {\n            i += 1\n        } else {\n            j -= 1\n        }\n    }\n    return res\n}\n
max_capacity.js
/* 最大容量:貪欲法 */\nfunction maxCapacity(ht) {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    let i = 0,\n        j = ht.length - 1;\n    // 初期の最大容量は 0\n    let res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        const cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
max_capacity.ts
/* 最大容量:貪欲法 */\nfunction maxCapacity(ht: number[]): number {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    let i = 0,\n        j = ht.length - 1;\n    // 初期の最大容量は 0\n    let res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        const cap: number = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
max_capacity.dart
/* 最大容量:貪欲法 */\nint maxCapacity(List<int> ht) {\n  // i, j を初期化し、それぞれ配列の両端に置く\n  int i = 0, j = ht.length - 1;\n  // 初期の最大容量は 0\n  int res = 0;\n  // 2 枚の板が出会うまで貪欲選択を繰り返す\n  while (i < j) {\n    // 最大容量を更新する\n    int cap = min(ht[i], ht[j]) * (j - i);\n    res = max(res, cap);\n    // 短い方を内側へ動かす\n    if (ht[i] < ht[j]) {\n      i++;\n    } else {\n      j--;\n    }\n  }\n  return res;\n}\n
max_capacity.rs
/* 最大容量:貪欲法 */\nfn max_capacity(ht: &[i32]) -> i32 {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    let mut i = 0;\n    let mut j = ht.len() - 1;\n    // 初期の最大容量は 0\n    let mut res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while i < j {\n        // 最大容量を更新する\n        let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;\n        res = std::cmp::max(res, cap);\n        // 短い方を内側へ動かす\n        if ht[i] < ht[j] {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    res\n}\n
max_capacity.c
/* 最大容量:貪欲法 */\nint maxCapacity(int ht[], int htLength) {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    int i = 0;\n    int j = htLength - 1;\n    // 初期の最大容量は 0\n    int res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        int capacity = myMin(ht[i], ht[j]) * (j - i);\n        res = myMax(res, capacity);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.kt
/* 最大容量:貪欲法 */\nfun maxCapacity(ht: IntArray): Int {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    var i = 0\n    var j = ht.size - 1\n    // 初期の最大容量は 0\n    var res = 0\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        val cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
max_capacity.rb
### 最大容量:貪欲法 ###\ndef max_capacity(ht)\n  # i, j を初期化し、それぞれ配列の両端に置く\n  i, j = 0, ht.length - 1\n  # 初期の最大容量は 0\n  res = 0\n\n  # 2 枚の板が出会うまで貪欲選択を繰り返す\n  while i < j\n    # 最大容量を更新する\n    cap = [ht[i], ht[j]].min * (j - i)\n    res = [res, cap].max\n    # 短い方を内側へ動かす\n    if ht[i] < ht[j]\n      i += 1\n    else\n      j -= 1\n    end\n  end\n\n  res\nend\n
コードの可視化

全画面で見る >

","path":["第 15 章   貪欲法","15.3   最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#3","level":3,"title":"3.   正しさの証明","text":"

貪欲法が総当たりより速いのは、各ラウンドの貪欲な選択がいくつかの状態を「スキップ」するためです。

たとえば状態 \\(cap[i, j]\\) において、\\(i\\) が短い板、\\(j\\) が長い板だとします。貪欲に短い板 \\(i\\) を内側へ 1 マス動かすと、次の図に示す状態が「スキップ」されます。これは、その後それらの状態の容量を検証できないことを意味します。

\\[ cap[i, i+1], cap[i, i+2], \\dots, cap[i, j-2], cap[i, j-1] \\]

図 15-12   短い板の移動によってスキップされる状態

観察すると、これらのスキップされた状態は、実際には長い板 \\(j\\) を内側へ動かしたすべての状態そのものです。前述のとおり、長い板を内側へ動かすと容量は必ず小さくなります。つまり、スキップされた状態はいずれも最適解にはなりえず、それらを飛ばしても最適解を逃すことはありません。

以上の分析から、短い板を動かす操作は「安全」であり、貪欲戦略は有効であると分かります。

","path":["第 15 章   貪欲法","15.3   最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/","level":1,"title":"15.4   最大積分割問題","text":"

Question

正整数 \\(n\\) が与えられたとき、それを少なくとも 2 つの正整数の和に分割し、分割後のすべての整数の積の最大値を求めよ。下図に示す。

図 15-13   最大積分割問題の定義

仮に \\(n\\) を \\(m\\) 個の整数因子に分割し、そのうち第 \\(i\\) 個の因子を \\(n_i\\) と記すと、

\\[ n = \\sum_{i=1}^{m}n_i \\]

本問題の目的は、すべての整数因子の積の最大値を求めることであり、すなわち

\\[ \\max(\\prod_{i=1}^{m}n_i) \\]

考えるべきことは、分割数 \\(m\\) をいくつにすべきか、各 \\(n_i\\) をいくつにすべきかである。

","path":["第 15 章   貪欲法","15.4   最大積分割問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#1","level":3,"title":"1.   貪欲戦略の決定","text":"

経験的に、2 つの整数の積はその和より大きくなることが多い。\\(n\\) から因子 \\(2\\) を 1 つ切り出すと、それらの積は \\(2(n-2)\\) となる。この積を \\(n\\) と比較すると、

\\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\]

下図のように、\\(n \\geq 4\\) のとき、\\(2\\) を 1 つ切り出すと積は大きくなる。これは、\\(4\\) 以上の整数はすべて分割すべきことを意味する。

貪欲戦略一:分割方法に \\(\\geq 4\\) の因子が含まれるなら、それはさらに分割すべきである。最終的な分割方法に現れる因子は \\(1\\)、\\(2\\)、\\(3\\) の 3 種類だけである。

図 15-14   分割により積が大きくなる

次に、どの因子が最適かを考える。\\(1\\)、\\(2\\)、\\(3\\) の 3 つの因子のうち、明らかに \\(1\\) が最も悪い。なぜなら \\(1 \\times (n-1) < n\\) は常に成り立ち、\\(1\\) を切り出すとかえって積が小さくなるからである。

下図のように、\\(n = 6\\) のとき、\\(3 \\times 3 > 2 \\times 2 \\times 2\\) が成り立つ。これは、\\(2\\) を切り出すより \\(3\\) を切り出すほうが有利であることを意味する。

貪欲戦略二:分割方法の中に存在してよい \\(2\\) は高々 2 つである。なぜなら、3 つの \\(2\\) は常に 2 つの \\(3\\) に置き換えられ、より大きな積を得られるからである。

図 15-15   最適な分割因子

以上より、次の貪欲戦略が導かれる。

  1. 整数 \\(n\\) を入力し、余りが \\(0\\)、\\(1\\)、\\(2\\) になるまで、そこから因子 \\(3\\) を繰り返し切り出す。
  2. 余りが \\(0\\) のとき、\\(n\\) は \\(3\\) の倍数であることを表すため、何も処理しない。
  3. 余りが \\(2\\) のときは、それ以上分割せず、そのまま残す。
  4. 余りが \\(1\\) のとき、\\(2 \\times 2 > 1 \\times 3\\) であるため、最後の \\(3\\) を \\(2\\) に置き換えるべきである。
","path":["第 15 章   貪欲法","15.4   最大積分割問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#2","level":3,"title":"2.   コード実装","text":"

下図のように、ループで整数を分割する必要はなく、切り捨て除算によって \\(3\\) の個数 \\(a\\) を、剰余演算によって余り \\(b\\) を得られる。このとき、

\\[ n = 3 a + b \\]

なお、\\(n \\leq 3\\) の境界ケースでは、必ず \\(1\\) を 1 つ分割する必要があり、積は \\(1 \\times (n - 1)\\) となる。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_product_cutting.py
def max_product_cutting(n: int) -> int:\n    \"\"\"最大切断積:貪欲法\"\"\"\n    # n <= 3 のときは、必ず 1 を切り出す\n    if n <= 3:\n        return 1 * (n - 1)\n    # 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    a, b = n // 3, n % 3\n    if b == 1:\n        # 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return int(math.pow(3, a - 1)) * 2 * 2\n    if b == 2:\n        # 余りが 2 のときは、そのままにする\n        return int(math.pow(3, a)) * 2\n    # 余りが 0 のときは、そのままにする\n    return int(math.pow(3, a))\n
max_product_cutting.cpp
/* 最大切断積:貪欲法 */\nint maxProductCutting(int n) {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return (int)pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 余りが 2 のときは、そのままにする\n        return (int)pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return (int)pow(3, a);\n}\n
max_product_cutting.java
/* 最大切断積:貪欲法 */\nint maxProductCutting(int n) {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return (int) Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 余りが 2 のときは、そのままにする\n        return (int) Math.pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return (int) Math.pow(3, a);\n}\n
max_product_cutting.cs
/* 最大切断積:貪欲法 */\nint MaxProductCutting(int n) {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return (int)Math.Pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 余りが 2 のときは、そのままにする\n        return (int)Math.Pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return (int)Math.Pow(3, a);\n}\n
max_product_cutting.go
/* 最大切断積:貪欲法 */\nfunc maxProductCutting(n int) int {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    a := n / 3\n    b := n % 3\n    if b == 1 {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return int(math.Pow(3, float64(a-1))) * 2 * 2\n    }\n    if b == 2 {\n        // 余りが 2 のときは、そのままにする\n        return int(math.Pow(3, float64(a))) * 2\n    }\n    // 余りが 0 のときは、そのままにする\n    return int(math.Pow(3, float64(a)))\n}\n
max_product_cutting.swift
/* 最大切断積:貪欲法 */\nfunc maxProductCutting(n: Int) -> Int {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    let a = n / 3\n    let b = n % 3\n    if b == 1 {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return pow(3, a - 1) * 2 * 2\n    }\n    if b == 2 {\n        // 余りが 2 のときは、そのままにする\n        return pow(3, a) * 2\n    }\n    // 余りが 0 のときは、そのままにする\n    return pow(3, a)\n}\n
max_product_cutting.js
/* 最大切断積:貪欲法 */\nfunction maxProductCutting(n) {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    let a = Math.floor(n / 3);\n    let b = n % 3;\n    if (b === 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // 余りが 2 のときは、そのままにする\n        return Math.pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return Math.pow(3, a);\n}\n
max_product_cutting.ts
/* 最大切断積:貪欲法 */\nfunction maxProductCutting(n: number): number {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    let a: number = Math.floor(n / 3);\n    let b: number = n % 3;\n    if (b === 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // 余りが 2 のときは、そのままにする\n        return Math.pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return Math.pow(3, a);\n}\n
max_product_cutting.dart
/* 最大切断積:貪欲法 */\nint maxProductCutting(int n) {\n  // n <= 3 のときは、必ず 1 を切り出す\n  if (n <= 3) {\n    return 1 * (n - 1);\n  }\n  // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n  int a = n ~/ 3;\n  int b = n % 3;\n  if (b == 1) {\n    // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n    return (pow(3, a - 1) * 2 * 2).toInt();\n  }\n  if (b == 2) {\n    // 余りが 2 のときは、そのままにする\n    return (pow(3, a) * 2).toInt();\n  }\n  // 余りが 0 のときは、そのままにする\n  return pow(3, a).toInt();\n}\n
max_product_cutting.rs
/* 最大切断積:貪欲法 */\nfn max_product_cutting(n: i32) -> i32 {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if n <= 3 {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    let a = n / 3;\n    let b = n % 3;\n    if b == 1 {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        3_i32.pow(a as u32 - 1) * 2 * 2\n    } else if b == 2 {\n        // 余りが 2 のときは、そのままにする\n        3_i32.pow(a as u32) * 2\n    } else {\n        // 余りが 0 のときは、そのままにする\n        3_i32.pow(a as u32)\n    }\n}\n
max_product_cutting.c
/* 最大切断積:貪欲法 */\nint maxProductCutting(int n) {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 余りが 2 のときは、そのままにする\n        return pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return pow(3, a);\n}\n
max_product_cutting.kt
/* 最大切断積:貪欲法 */\nfun maxProductCutting(n: Int): Int {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1)\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    val a = n / 3\n    val b = n % 3\n    if (b == 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return 3.0.pow((a - 1)).toInt() * 2 * 2\n    }\n    if (b == 2) {\n        // 余りが 2 のときは、そのままにする\n        return 3.0.pow(a).toInt() * 2 * 2\n    }\n    // 余りが 0 のときは、そのままにする\n    return 3.0.pow(a).toInt()\n}\n
max_product_cutting.rb
### 最大分割積:貪欲法 ###\ndef max_product_cutting(n)\n  # n <= 3 のときは、必ず 1 を切り出す\n  return 1 * (n - 1) if n <= 3\n  # 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n  a, b = n / 3, n % 3\n  # 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n  return (3.pow(a - 1) * 2 * 2).to_i if b == 1\n  # 余りが 2 のときは、そのままにする\n  return (3.pow(a) * 2).to_i if b == 2\n  # 余りが 0 のときは、そのままにする\n  3.pow(a).to_i\nend\n
コードの可視化

全画面で見る >

図 15-16   最大積分割の計算方法

時間計算量は、プログラミング言語におけるべき乗演算の実装方法に依存する。Python を例に取ると、よく使われるべき乗計算関数は 3 種類ある。

  • 演算子 ** と関数 pow() の時間計算量はいずれも \\(O(\\log⁡ a)\\) である。
  • 関数 math.pow() は内部で C 言語ライブラリの pow() 関数を呼び出し、浮動小数点のべき乗を実行するため、時間計算量は \\(O(1)\\) である。

変数 \\(a\\) と \\(b\\) が使う追加領域は定数サイズであり、したがって空間計算量は \\(O(1)\\) である。

","path":["第 15 章   貪欲法","15.4   最大積分割問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#3","level":3,"title":"3.   正しさの証明","text":"

背理法を用い、\\(n \\geq 4\\) の場合のみを考える。

  1. すべての因子は \\(\\leq 3\\) :最適な分割方法に \\(\\geq 4\\) の因子 \\(x\\) が存在すると仮定すると、それは必ずさらに \\(2(x-2)\\) に分割でき、より大きい(または等しい)積が得られる。これは仮定に矛盾する。
  2. 分割方法に \\(1\\) は含まれない :最適な分割方法に因子 \\(1\\) が 1 つ存在すると仮定すると、それは必ず別の因子に併合でき、より大きい積を得られる。これは仮定に矛盾する。
  3. 分割方法に含まれる \\(2\\) は高々 2 つ :最適な分割方法に 3 つの \\(2\\) が含まれると仮定すると、それは必ず 2 つの \\(3\\) に置き換えられ、積はより大きくなる。これは仮定に矛盾する。
","path":["第 15 章   貪欲法","15.4   最大積分割問題"],"tags":[]},{"location":"chapter_greedy/summary/","level":1,"title":"15.5   まとめ","text":"","path":["第 15 章   貪欲法","15.5   まとめ"],"tags":[]},{"location":"chapter_greedy/summary/#1","level":3,"title":"1.   重要な振り返り","text":"
  • 貪欲法は通常、最適化問題を解くために用いられ、その原理は各意思決定段階で局所最適な決定を行い、全体最適解を得ることを目指すというものである。
  • 貪欲法は反復的に次々と貪欲な選択を行い、各ラウンドで問題をより小さな部分問題へと変換し、最終的に問題を解決する。
  • 貪欲法は実装が簡単であるだけでなく、問題を解く効率も高い。動的計画法と比べると、貪欲法の時間計算量は通常より低い。
  • 硬貨両替問題では、ある種の硬貨の組み合わせに対しては貪欲法で最適解を保証できるが、別の組み合わせではそうではなく、非常に悪い解を見つけてしまう可能性がある。
  • 貪欲法による解法に適した問題は、貪欲選択性と最適部分構造という 2 つの性質を備えている。貪欲選択性は、貪欲戦略の有効性を表している。
  • 一部の複雑な問題では、貪欲選択性を証明するのは容易ではない。相対的には、反例による否定のほうが簡単であり、硬貨両替問題がその一例である。
  • 貪欲法の問題を解く流れは主に 3 段階に分かれる。すなわち、問題分析、貪欲戦略の決定、正しさの証明である。このうち、貪欲戦略の決定が中核であり、正しさの証明はしばしば難所となる。
  • 分数ナップサック問題は 0-1 ナップサックを基に、品物の一部を選ぶことを許しているため、貪欲法で解くことができる。貪欲戦略の正しさは背理法で証明できる。
  • 最大容量問題は全探索で解くことができ、時間計算量は \\(O(n^2)\\) である。貪欲戦略を設計し、各ラウンドで短い板を内側へ動かすことで、時間計算量を \\(O(n)\\) に最適化できる。
  • 最大分割積問題では、2 つの貪欲戦略を順に導いた。すなわち、\\(\\geq 4\\) の整数はすべてさらに分割すべきであり、最適な分割因子は \\(3\\) である。コードにはべき乗演算が含まれており、時間計算量はその実装方法に依存し、通常は \\(O(1)\\) または \\(O(\\log n)\\) である。
","path":["第 15 章   貪欲法","15.5   まとめ"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"第 6 章   ハッシュテーブル","text":"

Abstract

コンピュータの世界では、ハッシュテーブルは聡明な図書館員のような存在です。

彼は請求記号の計算方法を知っており、そのため目的の本を素早く見つけられます。

","path":["第 6 章   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"章の内容","text":"
  • 6.1   ハッシュテーブル
  • 6.2   ハッシュ衝突
  • 6.3   ハッシュアルゴリズム
  • 6.4   まとめ
","path":["第 6 章   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   ハッシュアルゴリズム","text":"

前の 2 節では、ハッシュテーブルの動作原理とハッシュ衝突の処理方法を紹介しました。しかし、オープンアドレス法であれ連鎖方式であれ、それらが保証できるのは衝突発生時でもハッシュテーブルが正常に動作することだけであり、ハッシュ衝突そのものを減らすことはできません。

ハッシュ衝突があまりにも頻繁に発生すると、ハッシュテーブルの性能は急激に劣化します。下図のように、連鎖方式のハッシュテーブルでは、理想的な場合にはキーと値のペアが各バケットに均等に分布し、最良の検索効率を達成します。最悪の場合には、すべてのキーと値のペアが同じバケットに格納され、時間計算量は \\(O(n)\\) に劣化します。

図 6-8   ハッシュ衝突の最良ケースと最悪ケース

キーと値のペアの分布はハッシュ関数によって決まります。ハッシュ関数の計算手順を思い出すと、まずハッシュ値を計算し、その後で配列長に対して剰余を取ります。

index = hash(key) % capacity\n

上の式から分かるように、ハッシュテーブルの容量 capacity が固定されているとき、出力値を決めるのはハッシュアルゴリズム hash() です。したがって、それがキーと値のペアのハッシュテーブル内での分布も決定します。

これは、ハッシュ衝突の発生確率を下げるには、ハッシュアルゴリズム hash() の設計に注目すべきだということを意味します。

","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#631","level":2,"title":"6.3.1   ハッシュアルゴリズムの目標","text":"

「高速かつ安定した」ハッシュテーブルというデータ構造を実現するために、ハッシュアルゴリズムは次の特徴を備える必要があります。

  • 決定性:同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成しなければなりません。そうして初めて、ハッシュテーブルの信頼性が保たれます。
  • 高効率:ハッシュ値の計算過程は十分に高速であるべきです。計算コストが小さいほど、ハッシュテーブルの実用性は高くなります。
  • 均一分布:ハッシュアルゴリズムは、キーと値のペアがハッシュテーブル内に均等に分布するようにすべきです。分布が均一であるほど、ハッシュ衝突の確率は低くなります。

実際には、ハッシュアルゴリズムはハッシュテーブルの実装だけでなく、ほかの多くの分野でも広く利用されています。

  • パスワード保存:ユーザーのパスワードを保護するために、システムは通常、平文パスワードを直接保存せず、そのハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力内容のハッシュ値を計算し、保存済みのハッシュ値と比較します。一致すれば、そのパスワードは正しいと見なされます。
  • データ完全性検査:送信側はデータのハッシュ値を計算してデータと一緒に送信できます。受信側は受け取ったデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。両者が一致すれば、そのデータは完全だと見なされます。

暗号分野の応用では、ハッシュ値から元のパスワードを推測するといった逆解析を防ぐために、ハッシュアルゴリズムにはさらに高いレベルの安全性が求められます。

  • 一方向性:ハッシュ値から入力データに関するいかなる情報も逆算できないこと。
  • 耐衝突性:異なる 2 つの入力で同じハッシュ値になるものを見つけることが、極めて困難であること。
  • アバランシェ効果:入力のわずかな変化が、出力の大きく予測不能な変化を引き起こすこと。

注意してほしいのは、**「均一分布」と「耐衝突性」は独立した 2 つの概念である**という点です。均一分布を満たしていても、耐衝突性を満たすとは限りません。たとえば、入力 key がランダムである場合、ハッシュ関数 key % 100 は均一に分布した出力を生成できます。しかし、このハッシュアルゴリズムはあまりにも単純で、下 2 桁が同じ key はすべて同じ出力になります。そのため、ハッシュ値から利用可能な key を容易に逆算でき、結果としてパスワードが破られてしまいます。

","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632","level":2,"title":"6.3.2   ハッシュアルゴリズムの設計","text":"

ハッシュアルゴリズムの設計は、多くの要素を考慮しなければならない複雑な問題です。しかし、要求の高くない場面であれば、いくつかの単純なハッシュアルゴリズムを設計することもできます。

  • 加算ハッシュ:入力の各文字の ASCII コードを足し合わせ、その合計をハッシュ値とします。
  • 乗算ハッシュ:乗算の非相関性を利用し、各ラウンドで定数を掛けながら、各文字の ASCII コードをハッシュ値に累積します。
  • XOR ハッシュ:入力データの各要素を XOR 演算で 1 つのハッシュ値に累積します。
  • 回転ハッシュ:各文字の ASCII コードを 1 つのハッシュ値に累積し、各回の累積前にハッシュ値を回転させます。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby simple_hash.py
def add_hash(key: str) -> int:\n    \"\"\"加算ハッシュ\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash += ord(c)\n    return hash % modulus\n\ndef mul_hash(key: str) -> int:\n    \"\"\"乗算ハッシュ\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = 31 * hash + ord(c)\n    return hash % modulus\n\ndef xor_hash(key: str) -> int:\n    \"\"\"XOR ハッシュ\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash ^= ord(c)\n    return hash % modulus\n\ndef rot_hash(key: str) -> int:\n    \"\"\"回転ハッシュ\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = (hash << 4) ^ (hash >> 28) ^ ord(c)\n    return hash % modulus\n
simple_hash.cpp
/* 加算ハッシュ */\nint addHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 乗算ハッシュ */\nint mulHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (31 * hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR ハッシュ */\nint xorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash ^= (int)c;\n    }\n    return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint rotHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n
simple_hash.java
/* 加算ハッシュ */\nint addHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* 乗算ハッシュ */\nint mulHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (31 * hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* XOR ハッシュ */\nint xorHash(String key) {\n    int hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash ^= (int) c;\n    }\n    return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint rotHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n
simple_hash.cs
/* 加算ハッシュ */\nint AddHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 乗算ハッシュ */\nint MulHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (31 * hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR ハッシュ */\nint XorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash ^= c;\n    }\n    return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint RotHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS;\n    }\n    return (int)hash;\n}\n
simple_hash.go
/* 加算ハッシュ */\nfunc addHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* 乗算ハッシュ */\nfunc mulHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (31*hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* XOR ハッシュ */\nfunc xorHash(key string) int {\n    hash := 0\n    modulus := 1000000007\n    for _, b := range []byte(key) {\n        fmt.Println(int(b))\n        hash ^= int(b)\n        hash = (31*hash + int(b)) % modulus\n    }\n    return hash & modulus\n}\n\n/* 回転ハッシュ */\nfunc rotHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus\n    }\n    return int(hash)\n}\n
simple_hash.swift
/* 加算ハッシュ */\nfunc addHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* 乗算ハッシュ */\nfunc mulHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (31 * hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* XOR ハッシュ */\nfunc xorHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash ^= Int(scalar.value)\n        }\n    }\n    return hash & MODULUS\n}\n\n/* 回転ハッシュ */\nfunc rotHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n
simple_hash.js
/* 加算ハッシュ */\nfunction addHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* 乗算ハッシュ */\nfunction mulHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR ハッシュ */\nfunction xorHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* 回転ハッシュ */\nfunction rotHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
simple_hash.ts
/* 加算ハッシュ */\nfunction addHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* 乗算ハッシュ */\nfunction mulHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR ハッシュ */\nfunction xorHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* 回転ハッシュ */\nfunction rotHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
simple_hash.dart
/* 加算ハッシュ */\nint addHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* 乗算ハッシュ */\nint mulHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (31 * hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* XOR ハッシュ */\nint xorHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash ^= key.codeUnitAt(i);\n  }\n  return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint rotHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n
simple_hash.rs
/* 加算ハッシュ */\nfn add_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* 乗算ハッシュ */\nfn mul_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (31 * hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* XOR ハッシュ */\nfn xor_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash ^= c as i64;\n    }\n\n    (hash & MODULUS) as i32\n}\n\n/* 回転ハッシュ */\nfn rot_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n
simple_hash.c
/* 加算ハッシュ */\nint addHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 乗算ハッシュ */\nint mulHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (31 * hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR ハッシュ */\nint xorHash(char *key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n\n    for (int i = 0; i < strlen(key); i++) {\n        hash ^= (unsigned char)key[i];\n    }\n    return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint rotHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS;\n    }\n\n    return (int)hash;\n}\n
simple_hash.kt
/* 加算ハッシュ */\nfun addHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* 乗算ハッシュ */\nfun mulHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (31 * hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* XOR ハッシュ */\nfun xorHash(key: String): Int {\n    var hash = 0\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = hash xor c.code\n    }\n    return hash and MODULUS\n}\n\n/* 回転ハッシュ */\nfun rotHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS\n    }\n    return hash.toInt()\n}\n
simple_hash.rb
### 加算ハッシュ ###\ndef add_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash += c.ord }\n\n  hash % modulus\nend\n\n### 乗算ハッシュ ###\ndef mul_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = 31 * hash + c.ord }\n\n  hash % modulus\nend\n\n### XOR ハッシュ ###\ndef xor_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash ^= c.ord }\n\n  hash % modulus\nend\n\n### 回転ハッシュ ###\ndef rot_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord }\n\n  hash % modulus\nend\n
コードの可視化

全画面で見る >

見て分かるように、各ハッシュアルゴリズムの最後のステップでは、大きな素数 \\(1000000007\\) で剰余を取り、ハッシュ値が適切な範囲に収まるようにしています。ここで考えてみる価値があるのは、なぜ素数での剰余を強調するのか、あるいは合成数で剰余を取ることにどんな欠点があるのか、という点です。これは興味深い問題です。

先に結論を述べると、法として大きな素数を使うと、ハッシュ値が均一に分布することを最大限に保証できます。素数はほかの数と公約数を持たないため、剰余演算によって生じる周期的なパターンを減らし、ハッシュ衝突を避けやすくなります。

たとえば、法として合成数 \\(9\\) を選ぶとします。これは \\(3\\) で割り切れるため、\\(3\\) で割り切れるすべての key は、\\(0\\)、\\(3\\)、\\(6\\) の 3 つのハッシュ値に写像されます。

\\[ \\begin{aligned} \\text{modulus} & = 9 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\\dots \\} \\end{aligned} \\]

入力 key がたまたまこのような等差数列の分布をしていると、ハッシュ値に偏りが生じ、ハッシュ衝突がさらに深刻になります。そこで modulus を素数 \\(13\\) に置き換えると仮定すると、keymodulus の間に公約数が存在しないため、出力されるハッシュ値の均一性は明らかに向上します。

\\[ \\begin{aligned} \\text{modulus} & = 13 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \\dots \\} \\end{aligned} \\]

補足すると、key がランダムかつ均一に分布していると保証できるなら、法に素数を選んでも合成数を選んでも構いません。どちらでも均一に分布したハッシュ値を出力できます。しかし、key の分布に何らかの周期性がある場合、合成数で剰余を取るほうが偏りが生じやすくなります。

要するに、通常は法として素数を選び、その素数はできるだけ大きいほうが望ましいです。そうすることで周期的なパターンをできる限り取り除き、ハッシュアルゴリズムの堅牢性を高められます。

","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#633","level":2,"title":"6.3.3   一般的なハッシュアルゴリズム","text":"

上で紹介した単純なハッシュアルゴリズムは、どれも比較的「脆弱」であり、ハッシュアルゴリズムの設計目標にはほど遠いことが分かります。たとえば、加算と XOR は交換法則を満たすため、加算ハッシュと XOR ハッシュでは、内容が同じで順序だけ異なる文字列を区別できません。これはハッシュ衝突を悪化させ、一部の安全上の問題を引き起こす可能性があります。

実際には、MD5、SHA-1、SHA-2、SHA-3 などの標準的なハッシュアルゴリズムを用いることが一般的です。これらは任意長の入力データを、固定長のハッシュ値へ写像できます。

ここ 1 世紀近くの間、ハッシュアルゴリズムは継続的に改良と最適化が進められてきました。ある研究者たちは性能向上に取り組み、別の研究者やハッカーたちは安全性の弱点を探し続けてきました。次の表は、実際の応用でよく使われるハッシュアルゴリズムを示したものです。

  • MD5 と SHA-1 は何度も攻撃に成功されているため、各種のセキュリティ用途では廃止されています。
  • SHA-2 系列の SHA-256 は最も安全なハッシュアルゴリズムの 1 つであり、いまだに成功した攻撃例がないため、多くのセキュリティ用途やプロトコルで広く使われています。
  • SHA-3 は SHA-2 と比べて実装コストが低く、計算効率も高い一方で、現時点での普及度は SHA-2 系列に及びません。

表 6-2   一般的なハッシュアルゴリズム

MD5 SHA-1 SHA-2 SHA-3 発表年 1992 1995 2002 2008 出力長 128 bit 160 bit 256/512 bit 224/256/384/512 bit ハッシュ衝突 多い 多い 非常に少ない 非常に少ない セキュリティレベル 低く、攻撃に成功されている 低く、攻撃に成功されている 高い 高い 用途 廃止済みだが、データ完全性検査には使われる 廃止済み 暗号資産の取引検証、デジタル署名など SHA-2 の代替に使える","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#634","level":2,"title":"6.3.4   データ構造のハッシュ値","text":"

ご存じのように、ハッシュテーブルの key には整数、小数、文字列などのデータ型を使えます。プログラミング言語は通常、これらのデータ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュテーブル内のバケットインデックス計算に利用します。Python を例にすると、hash() 関数を呼び出して各種データ型のハッシュ値を計算できます。

  • 整数と真理値のハッシュ値は、その値自身です。
  • 浮動小数点数と文字列のハッシュ値の計算はやや複雑なので、興味がある読者は自分で調べてみてください。
  • タプルのハッシュ値は、各要素のハッシュ値を求めてから、それらを組み合わせて 1 つのハッシュ値にしたものです。
  • オブジェクトのハッシュ値は、そのメモリアドレスに基づいて生成されます。オブジェクトのハッシュメソッドをオーバーライドすれば、内容に基づくハッシュ値を実装できます。

Tip

注意してください。組み込みのハッシュ値計算関数の定義や方法は、プログラミング言語ごとに異なります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby built_in_hash.py
num = 3\nhash_num = hash(num)\n# 整数 3 のハッシュ値は 3\n\nbol = True\nhash_bol = hash(bol)\n# 真理値 True のハッシュ値は 1\n\ndec = 3.14159\nhash_dec = hash(dec)\n# 小数 3.14159 のハッシュ値は 326484311674566659\n\nstr = \"Hello アルゴリズム\"\nhash_str = hash(str)\n# 文字列「Hello アルゴリズム」のハッシュ値は 4617003410720528961\n\ntup = (12836, \"シャオハ\")\nhash_tup = hash(tup)\n# タプル (12836, 'シャオハ') のハッシュ値は 1029005403108185979\n\nobj = ListNode(0)\nhash_obj = hash(obj)\n# ノードオブジェクト <ListNode object at 0x1058fd810> のハッシュ値は 274267521\n
built_in_hash.cpp
int num = 3;\nsize_t hashNum = hash<int>()(num);\n// 整数 3 のハッシュ値は 3\n\nbool bol = true;\nsize_t hashBol = hash<bool>()(bol);\n// 真理値 1 のハッシュ値は 1\n\ndouble dec = 3.14159;\nsize_t hashDec = hash<double>()(dec);\n// 小数 3.14159 のハッシュ値は 4614256650576692846\n\nstring str = \"Hello アルゴリズム\";\nsize_t hashStr = hash<string>()(str);\n// 文字列「Hello アルゴリズム」のハッシュ値は 15466937326284535026\n\n// C++ では、組み込みの std:hash() は基本データ型のハッシュ値計算のみを提供する\n// 配列やオブジェクトのハッシュ値計算は自分で実装する必要がある\n
built_in_hash.java
int num = 3;\nint hashNum = Integer.hashCode(num);\n// 整数 3 のハッシュ値は 3\n\nboolean bol = true;\nint hashBol = Boolean.hashCode(bol);\n// 真理値 true のハッシュ値は 1231\n\ndouble dec = 3.14159;\nint hashDec = Double.hashCode(dec);\n// 小数 3.14159 のハッシュ値は -1340954729\n\nString str = \"Hello アルゴリズム\";\nint hashStr = str.hashCode();\n// 文字列「Hello アルゴリズム」のハッシュ値は -727081396\n\nObject[] arr = { 12836, \"シャオハ\" };\nint hashTup = Arrays.hashCode(arr);\n// 配列 [12836, シャオハ] のハッシュ値は 1151158\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode();\n// ノードオブジェクト utils.ListNode@7dc5e7b4 のハッシュ値は 2110121908\n
built_in_hash.cs
int num = 3;\nint hashNum = num.GetHashCode();\n// 整数 3 のハッシュ値は 3;\n\nbool bol = true;\nint hashBol = bol.GetHashCode();\n// 真理値 true のハッシュ値は 1;\n\ndouble dec = 3.14159;\nint hashDec = dec.GetHashCode();\n// 小数 3.14159 のハッシュ値は -1340954729;\n\nstring str = \"Hello アルゴリズム\";\nint hashStr = str.GetHashCode();\n// 文字列「Hello アルゴリズム」のハッシュ値は -586107568;\n\nobject[] arr = [12836, \"シャオハ\"];\nint hashTup = arr.GetHashCode();\n// 配列 [12836, シャオハ] のハッシュ値は 42931033;\n\nListNode obj = new(0);\nint hashObj = obj.GetHashCode();\n// ノードオブジェクト 0 のハッシュ値は 39053774;\n
built_in_hash.go
// Go は組み込みの hash code 関数を提供していない\n
built_in_hash.swift
let num = 3\nlet hashNum = num.hashValue\n// 整数 3 のハッシュ値は 9047044699613009734\n\nlet bol = true\nlet hashBol = bol.hashValue\n// 真理値 true のハッシュ値は -4431640247352757451\n\nlet dec = 3.14159\nlet hashDec = dec.hashValue\n// 小数 3.14159 のハッシュ値は -2465384235396674631\n\nlet str = \"Hello アルゴリズム\"\nlet hashStr = str.hashValue\n// 文字列「Hello アルゴリズム」のハッシュ値は -7850626797806988787\n\nlet arr = [AnyHashable(12836), AnyHashable(\"シャオハ\")]\nlet hashTup = arr.hashValue\n// 配列 [AnyHashable(12836), AnyHashable(\"シャオハ\")] のハッシュ値は -2308633508154532996\n\nlet obj = ListNode(x: 0)\nlet hashObj = obj.hashValue\n// ノードオブジェクト utils.ListNode のハッシュ値は -2434780518035996159\n
built_in_hash.js
// JavaScript は組み込みの hash code 関数を提供していない\n
built_in_hash.ts
// TypeScript は組み込みの hash code 関数を提供していない\n
built_in_hash.dart
int num = 3;\nint hashNum = num.hashCode;\n// 整数 3 のハッシュ値は 34803\n\nbool bol = true;\nint hashBol = bol.hashCode;\n// 真理値 true のハッシュ値は 1231\n\ndouble dec = 3.14159;\nint hashDec = dec.hashCode;\n// 小数 3.14159 のハッシュ値は 2570631074981783\n\nString str = \"Hello アルゴリズム\";\nint hashStr = str.hashCode;\n// 文字列「Hello アルゴリズム」のハッシュ値は 468167534\n\nList arr = [12836, \"シャオハ\"];\nint hashArr = arr.hashCode;\n// 配列 [12836, シャオハ] のハッシュ値は 976512528\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode;\n// ノードオブジェクト Instance of 'ListNode' のハッシュ値は 1033450432\n
built_in_hash.rs
use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\nlet num = 3;\nlet mut num_hasher = DefaultHasher::new();\nnum.hash(&mut num_hasher);\nlet hash_num = num_hasher.finish();\n// 整数 3 のハッシュ値は 568126464209439262\n\nlet bol = true;\nlet mut bol_hasher = DefaultHasher::new();\nbol.hash(&mut bol_hasher);\nlet hash_bol = bol_hasher.finish();\n// 真理値 true のハッシュ値は 4952851536318644461\n\nlet dec: f32 = 3.14159;\nlet mut dec_hasher = DefaultHasher::new();\ndec.to_bits().hash(&mut dec_hasher);\nlet hash_dec = dec_hasher.finish();\n// 小数 3.14159 のハッシュ値は 2566941990314602357\n\nlet str = \"Hello アルゴリズム\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// 文字列「Hello アルゴリズム」のハッシュ値は 16092673739211250988\n\nlet arr = (&12836, &\"シャオハ\");\nlet mut tup_hasher = DefaultHasher::new();\narr.hash(&mut tup_hasher);\nlet hash_tup = tup_hasher.finish();\n// タプル (12836, \"シャオハ\") のハッシュ値は 1885128010422702749\n\nlet node = ListNode::new(42);\nlet mut hasher = DefaultHasher::new();\nnode.borrow().val.hash(&mut hasher);\nlet hash = hasher.finish();\n// ノードオブジェクト RefCell { value: ListNode { val: 42, next: None } } のハッシュ値は15387811073369036852\n
built_in_hash.c
// C は組み込みの hash code 関数を提供していない\n
built_in_hash.kt
val num = 3\nval hashNum = num.hashCode()\n// 整数 3 のハッシュ値は 3\n\nval bol = true\nval hashBol = bol.hashCode()\n// 真理値 true のハッシュ値は 1231\n\nval dec = 3.14159\nval hashDec = dec.hashCode()\n// 小数 3.14159 のハッシュ値は -1340954729\n\nval str = \"Hello アルゴリズム\"\nval hashStr = str.hashCode()\n// 文字列「Hello アルゴリズム」のハッシュ値は -727081396\n\nval arr = arrayOf<Any>(12836, \"シャオハ\")\nval hashTup = arr.hashCode()\n// 配列 [12836, シャオハ] のハッシュ値は 189568618\n\nval obj = ListNode(0)\nval hashObj = obj.hashCode()\n// ノードオブジェクト utils.ListNode@1d81eb93 のハッシュ値は 495053715\n
built_in_hash.rb
num = 3\nhash_num = num.hash\n# 整数 3 のハッシュ値は -4385856518450339636\n\nbol = true\nhash_bol = bol.hash\n# 真理値 true のハッシュ値は -1617938112149317027\n\ndec = 3.14159\nhash_dec = dec.hash\n# 小数 3.14159 のハッシュ値は -1479186995943067893\n\nstr = \"Hello アルゴリズム\"\nhash_str = str.hash\n# 文字列「Hello アルゴリズム」のハッシュ値は -4075943250025831763\n\ntup = [12836, 'シャオハ']\nhash_tup = tup.hash\n# タプル (12836, 'シャオハ') のハッシュ値は 1999544809202288822\n\nobj = ListNode.new(0)\nhash_obj = obj.hash\n# ノードオブジェクト #<ListNode:0x000078133140ab70> のハッシュ値は 4302940560806366381\n
可視化実行

https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

多くのプログラミング言語では、不変オブジェクトだけがハッシュテーブルの key として使えます。仮にリスト(動的配列)を key とすると、その内容が変化したときにハッシュ値も変わってしまうため、もとの value をハッシュテーブルから検索できなくなります。

カスタムオブジェクト(たとえば連結リストのノード)のメンバ変数は可変ですが、それでもハッシュ可能です。これは、オブジェクトのハッシュ値が通常はメモリアドレスに基づいて生成されるためです。オブジェクトの内容が変化しても、メモリアドレスが変わらなければ、ハッシュ値も変わりません。

注意深い人なら、異なるコンソールでプログラムを実行したときに、出力されるハッシュ値が異なることに気づくかもしれません。これは、Python インタプリタが起動のたびに文字列ハッシュ関数へランダムな salt 値を追加しているためです。この方法によって HashDoS 攻撃を効果的に防ぎ、ハッシュアルゴリズムの安全性を高めています。

","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_collision/","level":1,"title":"6.2   ハッシュ衝突","text":"

前節で述べたように、**通常、ハッシュ関数の入力空間は出力空間よりもはるかに大きい**ため、理論上ハッシュ衝突は避けられません。例えば、入力空間がすべての整数で、出力空間が配列の容量サイズである場合、必然的に複数の整数が同じバケットインデックスに写像されます。

ハッシュ衝突は検索結果の誤りを招き、ハッシュテーブルの利用可能性に深刻な影響を与えます。この問題を解決するために、ハッシュ衝突が発生するたびにハッシュテーブルを拡張し、衝突が消えるまで続けることが考えられます。この方法は単純で効果的ですが、効率が低すぎます。なぜなら、ハッシュテーブルの拡張には大量のデータ移動とハッシュ値の計算が必要だからです。効率を高めるために、次の戦略を採用できます。

  1. ハッシュテーブルのデータ構造を改良し、ハッシュ衝突が発生してもハッシュテーブルが正常に動作できるようにする。
  2. 必要な場合、すなわちハッシュ衝突が比較的深刻なときにのみ、拡張操作を実行する。

ハッシュテーブルの構造改善方法には、主に「チェイン法」と「オープンアドレッシング」があります。

","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#621","level":2,"title":"6.2.1   チェイン法","text":"

元のハッシュテーブルでは、各バケットには 1 つのキーと値のペアしか格納できません。チェイン法(separate chaining)では、単一要素を連結リストに置き換え、キーと値のペアを連結リストのノードとして扱い、衝突したすべてのキーと値のペアを同じ連結リストに格納します。下図はチェイン法によるハッシュテーブルの例を示しています。

図 6-5   チェイン法ハッシュテーブル

チェイン法で実装されたハッシュテーブルでは、操作方法が次のように変わります。

  • 要素の検索:入力 key をハッシュ関数に通してバケットインデックスを得ると、連結リストの先頭ノードにアクセスできます。その後、連結リストを走査して key を比較し、目的のキーと値のペアを探します。
  • 要素の追加:まずハッシュ関数で連結リストの先頭ノードにアクセスし、その後ノード(キーと値のペア)を連結リストに追加します。
  • 要素の削除:ハッシュ関数の結果に基づいて連結リストの先頭にアクセスし、続いて連結リストを走査して対象ノードを探し、削除します。

チェイン法には次の制約があります。

  • 使用メモリの増加:連結リストにはノードポインタが含まれるため、配列よりも多くのメモリを消費します。
  • 検索効率の低下:対応する要素を見つけるために連結リストを線形走査する必要があるためです。

以下のコードはチェイン法ハッシュテーブルの簡単な実装を示しています。注意すべき点は 2 つあります。

  • 連結リストの代わりにリスト(動的配列)を使って、コードを簡潔にしています。この設定では、ハッシュテーブル(配列)は複数のバケットを含み、各バケットは 1 つのリストです。
  • 以下の実装にはハッシュテーブルの拡張メソッドが含まれています。負荷率が \\(\\frac{2}{3}\\) を超えたとき、ハッシュテーブルを元の \\(2\\) 倍に拡張します。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_chaining.py
class HashMapChaining:\n    \"\"\"チェイン法ハッシュテーブル\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self.size = 0  # キーと値のペア数\n        self.capacity = 4  # ハッシュテーブル容量\n        self.load_thres = 2.0 / 3.0  # リサイズを発動する負荷率のしきい値\n        self.extend_ratio = 2  # 拡張倍率\n        self.buckets = [[] for _ in range(self.capacity)]  # バケット配列\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"ハッシュ関数\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"負荷率\"\"\"\n        return self.size / self.capacity\n\n    def get(self, key: int) -> str | None:\n        \"\"\"検索操作\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # バケットを走査し、key が見つかれば対応する val を返す\n        for pair in bucket:\n            if pair.key == key:\n                return pair.val\n        # key が見つからない場合は None を返す\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"追加操作\"\"\"\n        # 負荷率がしきい値を超えたら、リサイズを実行\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for pair in bucket:\n            if pair.key == key:\n                pair.val = val\n                return\n        # その key が存在しなければ、キーと値のペアを末尾に追加\n        pair = Pair(key, val)\n        bucket.append(pair)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"削除操作\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # バケットを走査してキーと値のペアを削除\n        for pair in bucket:\n            if pair.key == key:\n                bucket.remove(pair)\n                self.size -= 1\n                break\n\n    def extend(self):\n        \"\"\"ハッシュテーブルを拡張\"\"\"\n        # 元のハッシュテーブルを一時保存\n        buckets = self.buckets\n        # リサイズ後の新しいハッシュテーブルを初期化\n        self.capacity *= self.extend_ratio\n        self.buckets = [[] for _ in range(self.capacity)]\n        self.size = 0\n        # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for bucket in buckets:\n            for pair in bucket:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"ハッシュテーブルを出力\"\"\"\n        for bucket in self.buckets:\n            res = []\n            for pair in bucket:\n                res.append(str(pair.key) + \" -> \" + pair.val)\n            print(res)\n
hash_map_chaining.cpp
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n  private:\n    int size;                       // キーと値のペア数\n    int capacity;                   // ハッシュテーブル容量\n    double loadThres;               // リサイズを発動する負荷率のしきい値\n    int extendRatio;                // 拡張倍率\n    vector<vector<Pair *>> buckets; // バケット配列\n\n  public:\n    /* コンストラクタ */\n    HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {\n        buckets.resize(capacity);\n    }\n\n    /* デストラクタメソッド */\n    ~HashMapChaining() {\n        for (auto &bucket : buckets) {\n            for (Pair *pair : bucket) {\n                // メモリを解放する\n                delete pair;\n            }\n        }\n    }\n\n    /* ハッシュ関数 */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    double loadFactor() {\n        return (double)size / (double)capacity;\n    }\n\n    /* 検索操作 */\n    string get(int key) {\n        int index = hashFunc(key);\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                return pair->val;\n            }\n        }\n        // key が見つからない場合は空文字列を返す\n        return \"\";\n    }\n\n    /* 追加操作 */\n    void put(int key, string val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                pair->val = val;\n                return;\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        buckets[index].push_back(new Pair(key, val));\n        size++;\n    }\n\n    /* 削除操作 */\n    void remove(int key) {\n        int index = hashFunc(key);\n        auto &bucket = buckets[index];\n        // バケットを走査してキーと値のペアを削除\n        for (int i = 0; i < bucket.size(); i++) {\n            if (bucket[i]->key == key) {\n                Pair *tmp = bucket[i];\n                bucket.erase(bucket.begin() + i); // そこからキーと値の組を削除する\n                delete tmp;                       // メモリを解放する\n                size--;\n                return;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    void extend() {\n        // 元のハッシュテーブルを一時保存\n        vector<vector<Pair *>> bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets.clear();\n        buckets.resize(capacity);\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (auto &bucket : bucketsTmp) {\n            for (Pair *pair : bucket) {\n                put(pair->key, pair->val);\n                // メモリを解放する\n                delete pair;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    void print() {\n        for (auto &bucket : buckets) {\n            cout << \"[\";\n            for (Pair *pair : bucket) {\n                cout << pair->key << \" -> \" << pair->val << \", \";\n            }\n            cout << \"]\\n\";\n        }\n    }\n};\n
hash_map_chaining.java
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    int size; // キーと値のペア数\n    int capacity; // ハッシュテーブル容量\n    double loadThres; // リサイズを発動する負荷率のしきい値\n    int extendRatio; // 拡張倍率\n    List<List<Pair>> buckets; // バケット配列\n\n    /* コンストラクタ */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n    }\n\n    /* ハッシュ関数 */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* 検索操作 */\n    String get(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // key が見つからない場合は null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    void put(int key, String val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        Pair pair = new Pair(key, val);\n        bucket.add(pair);\n        size++;\n    }\n\n    /* 削除操作 */\n    void remove(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // バケットを走査してキーと値のペアを削除\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    void extend() {\n        // 元のハッシュテーブルを一時保存\n        List<List<Pair>> bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (List<Pair> bucket : bucketsTmp) {\n            for (Pair pair : bucket) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    void print() {\n        for (List<Pair> bucket : buckets) {\n            List<String> res = new ArrayList<>();\n            for (Pair pair : bucket) {\n                res.add(pair.key + \" -> \" + pair.val);\n            }\n            System.out.println(res);\n        }\n    }\n}\n
hash_map_chaining.cs
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    int size; // キーと値のペア数\n    int capacity; // ハッシュテーブル容量\n    double loadThres; // リサイズを発動する負荷率のしきい値\n    int extendRatio; // 拡張倍率\n    List<List<Pair>> buckets; // バケット配列\n\n    /* コンストラクタ */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n    }\n\n    /* ハッシュ関数 */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* 検索操作 */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        // バケットを走査し、key が見つかれば対応する val を返す\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // key が見つからない場合は null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    public void Put(int key, string val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        int index = HashFunc(key);\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        buckets[index].Add(new Pair(key, val));\n        size++;\n    }\n\n    /* 削除操作 */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // バケットを走査してキーと値のペアを削除\n        foreach (Pair pair in buckets[index].ToList()) {\n            if (pair.key == key) {\n                buckets[index].Remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    void Extend() {\n        // 元のハッシュテーブルを一時保存\n        List<List<Pair>> bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        foreach (List<Pair> bucket in bucketsTmp) {\n            foreach (Pair pair in bucket) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    public void Print() {\n        foreach (List<Pair> bucket in buckets) {\n            List<string> res = [];\n            foreach (Pair pair in bucket) {\n                res.Add(pair.key + \" -> \" + pair.val);\n            }\n            foreach (string kv in res) {\n                Console.WriteLine(kv);\n            }\n        }\n    }\n}\n
hash_map_chaining.go
/* チェイン法ハッシュテーブル */\ntype hashMapChaining struct {\n    size        int      // キーと値のペア数\n    capacity    int      // ハッシュテーブル容量\n    loadThres   float64  // リサイズを発動する負荷率のしきい値\n    extendRatio int      // 拡張倍率\n    buckets     [][]pair // バケット配列\n}\n\n/* コンストラクタ */\nfunc newHashMapChaining() *hashMapChaining {\n    buckets := make([][]pair, 4)\n    for i := 0; i < 4; i++ {\n        buckets[i] = make([]pair, 0)\n    }\n    return &hashMapChaining{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     buckets,\n    }\n}\n\n/* ハッシュ関数 */\nfunc (m *hashMapChaining) hashFunc(key int) int {\n    return key % m.capacity\n}\n\n/* 負荷率 */\nfunc (m *hashMapChaining) loadFactor() float64 {\n    return float64(m.size) / float64(m.capacity)\n}\n\n/* 検索操作 */\nfunc (m *hashMapChaining) get(key int) string {\n    idx := m.hashFunc(key)\n    bucket := m.buckets[idx]\n    // バケットを走査し、key が見つかれば対応する val を返す\n    for _, p := range bucket {\n        if p.key == key {\n            return p.val\n        }\n    }\n    // key が見つからない場合は空文字列を返す\n    return \"\"\n}\n\n/* 追加操作 */\nfunc (m *hashMapChaining) put(key int, val string) {\n    // 負荷率がしきい値を超えたら、リサイズを実行\n    if m.loadFactor() > m.loadThres {\n        m.extend()\n    }\n    idx := m.hashFunc(key)\n    // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n    for i := range m.buckets[idx] {\n        if m.buckets[idx][i].key == key {\n            m.buckets[idx][i].val = val\n            return\n        }\n    }\n    // その key が存在しなければ、キーと値のペアを末尾に追加\n    p := pair{\n        key: key,\n        val: val,\n    }\n    m.buckets[idx] = append(m.buckets[idx], p)\n    m.size += 1\n}\n\n/* 削除操作 */\nfunc (m *hashMapChaining) remove(key int) {\n    idx := m.hashFunc(key)\n    // バケットを走査してキーと値のペアを削除\n    for i, p := range m.buckets[idx] {\n        if p.key == key {\n            // スライスから削除する\n            m.buckets[idx] = append(m.buckets[idx][:i], m.buckets[idx][i+1:]...)\n            m.size -= 1\n            break\n        }\n    }\n}\n\n/* ハッシュテーブルを拡張 */\nfunc (m *hashMapChaining) extend() {\n    // 元のハッシュテーブルを一時保存\n    tmpBuckets := make([][]pair, len(m.buckets))\n    for i := 0; i < len(m.buckets); i++ {\n        tmpBuckets[i] = make([]pair, len(m.buckets[i]))\n        copy(tmpBuckets[i], m.buckets[i])\n    }\n    // リサイズ後の新しいハッシュテーブルを初期化\n    m.capacity *= m.extendRatio\n    m.buckets = make([][]pair, m.capacity)\n    for i := 0; i < m.capacity; i++ {\n        m.buckets[i] = make([]pair, 0)\n    }\n    m.size = 0\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for _, bucket := range tmpBuckets {\n        for _, p := range bucket {\n            m.put(p.key, p.val)\n        }\n    }\n}\n\n/* ハッシュテーブルを出力 */\nfunc (m *hashMapChaining) print() {\n    var builder strings.Builder\n\n    for _, bucket := range m.buckets {\n        builder.WriteString(\"[\")\n        for _, p := range bucket {\n            builder.WriteString(strconv.Itoa(p.key) + \" -> \" + p.val + \" \")\n        }\n        builder.WriteString(\"]\")\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
hash_map_chaining.swift
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    var size: Int // キーと値のペア数\n    var capacity: Int // ハッシュテーブル容量\n    var loadThres: Double // リサイズを発動する負荷率のしきい値\n    var extendRatio: Int // 拡張倍率\n    var buckets: [[Pair]] // バケット配列\n\n    /* コンストラクタ */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: [], count: capacity)\n    }\n\n    /* ハッシュ関数 */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* 負荷率 */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* 検索操作 */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for pair in bucket {\n            if pair.key == key {\n                return pair.val\n            }\n        }\n        // `key` が見つからなければ `nil` を返す\n        return nil\n    }\n\n    /* 追加操作 */\n    func put(key: Int, val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if loadFactor() > loadThres {\n            extend()\n        }\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for pair in bucket {\n            if pair.key == key {\n                pair.val = val\n                return\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        let pair = Pair(key: key, val: val)\n        buckets[index].append(pair)\n        size += 1\n    }\n\n    /* 削除操作 */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // バケットを走査してキーと値のペアを削除\n        for (pairIndex, pair) in bucket.enumerated() {\n            if pair.key == key {\n                buckets[index].remove(at: pairIndex)\n                size -= 1\n                break\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    func extend() {\n        // 元のハッシュテーブルを一時保存\n        let bucketsTmp = buckets\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio\n        buckets = Array(repeating: [], count: capacity)\n        size = 0\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for bucket in bucketsTmp {\n            for pair in bucket {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    func print() {\n        for bucket in buckets {\n            let res = bucket.map { \"\\($0.key) -> \\($0.val)\" }\n            Swift.print(res)\n        }\n    }\n}\n
hash_map_chaining.js
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    #size; // キーと値のペア数\n    #capacity; // ハッシュテーブル容量\n    #loadThres; // リサイズを発動する負荷率のしきい値\n    #extendRatio; // 拡張倍率\n    #buckets; // バケット配列\n\n    /* コンストラクタ */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* ハッシュ関数 */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* 負荷率 */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* 検索操作 */\n    get(key) {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // key が見つからない場合は null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    put(key, val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* 削除操作 */\n    remove(key) {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // バケットを走査してキーと値のペアを削除\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    #extend() {\n        // 元のハッシュテーブルを一時保存\n        const bucketsTmp = this.#buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    print() {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
hash_map_chaining.ts
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    #size: number; // キーと値のペア数\n    #capacity: number; // ハッシュテーブル容量\n    #loadThres: number; // リサイズを発動する負荷率のしきい値\n    #extendRatio: number; // 拡張倍率\n    #buckets: Pair[][]; // バケット配列\n\n    /* コンストラクタ */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* ハッシュ関数 */\n    #hashFunc(key: number): number {\n        return key % this.#capacity;\n    }\n\n    /* 負荷率 */\n    #loadFactor(): number {\n        return this.#size / this.#capacity;\n    }\n\n    /* 検索操作 */\n    get(key: number): string | null {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // key が見つからない場合は null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    put(key: number, val: string): void {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* 削除操作 */\n    remove(key: number): void {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // バケットを走査してキーと値のペアを削除\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    #extend(): void {\n        // 元のハッシュテーブルを一時保存\n        const bucketsTmp = this.#buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    print(): void {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
hash_map_chaining.dart
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n  late int size; // キーと値のペア数\n  late int capacity; // ハッシュテーブル容量\n  late double loadThres; // リサイズを発動する負荷率のしきい値\n  late int extendRatio; // 拡張倍率\n  late List<List<Pair>> buckets; // バケット配列\n\n  /* コンストラクタ */\n  HashMapChaining() {\n    size = 0;\n    capacity = 4;\n    loadThres = 2.0 / 3.0;\n    extendRatio = 2;\n    buckets = List.generate(capacity, (_) => []);\n  }\n\n  /* ハッシュ関数 */\n  int hashFunc(int key) {\n    return key % capacity;\n  }\n\n  /* 負荷率 */\n  double loadFactor() {\n    return size / capacity;\n  }\n\n  /* 検索操作 */\n  String? get(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // バケットを走査し、key が見つかれば対応する val を返す\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        return pair.val;\n      }\n    }\n    // key が見つからない場合は null を返す\n    return null;\n  }\n\n  /* 追加操作 */\n  void put(int key, String val) {\n    // 負荷率がしきい値を超えたら、リサイズを実行\n    if (loadFactor() > loadThres) {\n      extend();\n    }\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        pair.val = val;\n        return;\n      }\n    }\n    // その key が存在しなければ、キーと値のペアを末尾に追加\n    Pair pair = Pair(key, val);\n    bucket.add(pair);\n    size++;\n  }\n\n  /* 削除操作 */\n  void remove(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // バケットを走査してキーと値のペアを削除\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        bucket.remove(pair);\n        size--;\n        break;\n      }\n    }\n  }\n\n  /* ハッシュテーブルを拡張 */\n  void extend() {\n    // 元のハッシュテーブルを一時保存\n    List<List<Pair>> bucketsTmp = buckets;\n    // リサイズ後の新しいハッシュテーブルを初期化\n    capacity *= extendRatio;\n    buckets = List.generate(capacity, (_) => []);\n    size = 0;\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for (List<Pair> bucket in bucketsTmp) {\n      for (Pair pair in bucket) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* ハッシュテーブルを出力 */\n  void printHashMap() {\n    for (List<Pair> bucket in buckets) {\n      List<String> res = [];\n      for (Pair pair in bucket) {\n        res.add(\"${pair.key} -> ${pair.val}\");\n      }\n      print(res);\n    }\n  }\n}\n
hash_map_chaining.rs
/* チェイン法ハッシュテーブル */\nstruct HashMapChaining {\n    size: usize,\n    capacity: usize,\n    load_thres: f32,\n    extend_ratio: usize,\n    buckets: Vec<Vec<Pair>>,\n}\n\nimpl HashMapChaining {\n    /* コンストラクタ */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![vec![]; 4],\n        }\n    }\n\n    /* ハッシュ関数 */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % self.capacity\n    }\n\n    /* 負荷率 */\n    fn load_factor(&self) -> f32 {\n        self.size as f32 / self.capacity as f32\n    }\n\n    /* 削除操作 */\n    fn remove(&mut self, key: i32) -> Option<String> {\n        let index = self.hash_func(key);\n\n        // バケットを走査してキーと値のペアを削除\n        for (i, p) in self.buckets[index].iter_mut().enumerate() {\n            if p.key == key {\n                let pair = self.buckets[index].remove(i);\n                self.size -= 1;\n                return Some(pair.val);\n            }\n        }\n\n        // key が見つからない場合は None を返す\n        None\n    }\n\n    /* ハッシュテーブルを拡張 */\n    fn extend(&mut self) {\n        // 元のハッシュテーブルを一時保存\n        let buckets_tmp = std::mem::take(&mut self.buckets);\n\n        // リサイズ後の新しいハッシュテーブルを初期化\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![Vec::new(); self.capacity as usize];\n        self.size = 0;\n\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for bucket in buckets_tmp {\n            for pair in bucket {\n                self.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    fn print(&self) {\n        for bucket in &self.buckets {\n            let mut res = Vec::new();\n            for pair in bucket {\n                res.push(format!(\"{} -> {}\", pair.key, pair.val));\n            }\n            println!(\"{:?}\", res);\n        }\n    }\n\n    /* 追加操作 */\n    fn put(&mut self, key: i32, val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n\n        let index = self.hash_func(key);\n\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for pair in self.buckets[index].iter_mut() {\n            if pair.key == key {\n                pair.val = val;\n                return;\n            }\n        }\n\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        let pair = Pair { key, val };\n        self.buckets[index].push(pair);\n        self.size += 1;\n    }\n\n    /* 検索操作 */\n    fn get(&self, key: i32) -> Option<&str> {\n        let index = self.hash_func(key);\n\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for pair in self.buckets[index].iter() {\n            if pair.key == key {\n                return Some(&pair.val);\n            }\n        }\n\n        // key が見つからない場合は None を返す\n        None\n    }\n}\n
hash_map_chaining.c
/* 連結リストノード */\ntypedef struct Node {\n    Pair *pair;\n    struct Node *next;\n} Node;\n\n/* チェイン法ハッシュテーブル */\ntypedef struct {\n    int size;         // キーと値のペア数\n    int capacity;     // ハッシュテーブル容量\n    double loadThres; // リサイズを発動する負荷率のしきい値\n    int extendRatio;  // 拡張倍率\n    Node **buckets;   // バケット配列\n} HashMapChaining;\n\n/* コンストラクタ */\nHashMapChaining *newHashMapChaining() {\n    HashMapChaining *hashMap = (HashMapChaining *)malloc(sizeof(HashMapChaining));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    return hashMap;\n}\n\n/* デストラクタ */\nvoid delHashMapChaining(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        while (cur) {\n            Node *tmp = cur;\n            cur = cur->next;\n            free(tmp->pair);\n            free(tmp);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap);\n}\n\n/* ハッシュ関数 */\nint hashFunc(HashMapChaining *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* 負荷率 */\ndouble loadFactor(HashMapChaining *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* 検索操作 */\nchar *get(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    // バケットを走査し、key が見つかれば対応する val を返す\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            return cur->pair->val;\n        }\n        cur = cur->next;\n    }\n    return \"\"; // key が見つからない場合は空文字列を返す\n}\n\n/* 追加操作 */\nvoid put(HashMapChaining *hashMap, int key, const char *val) {\n    // 負荷率がしきい値を超えたら、リサイズを実行\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    int index = hashFunc(hashMap, key);\n    // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            strcpy(cur->pair->val, val); // 指定した `key` に遭遇したら、対応する `val` を更新して返す\n            return;\n        }\n        cur = cur->next;\n    }\n    // 該当する `key` がなければ、キーと値のペアを連結リストの先頭に追加する\n    Pair *newPair = (Pair *)malloc(sizeof(Pair));\n    newPair->key = key;\n    strcpy(newPair->val, val);\n    Node *newNode = (Node *)malloc(sizeof(Node));\n    newNode->pair = newPair;\n    newNode->next = hashMap->buckets[index];\n    hashMap->buckets[index] = newNode;\n    hashMap->size++;\n}\n\n/* ハッシュテーブルを拡張 */\nvoid extend(HashMapChaining *hashMap) {\n    // 元のハッシュテーブルを一時保存\n    int oldCapacity = hashMap->capacity;\n    Node **oldBuckets = hashMap->buckets;\n    // リサイズ後の新しいハッシュテーブルを初期化\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    hashMap->size = 0;\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for (int i = 0; i < oldCapacity; i++) {\n        Node *cur = oldBuckets[i];\n        while (cur) {\n            put(hashMap, cur->pair->key, cur->pair->val);\n            Node *temp = cur;\n            cur = cur->next;\n            // メモリを解放する\n            free(temp->pair);\n            free(temp);\n        }\n    }\n\n    free(oldBuckets);\n}\n\n/* 削除操作 */\nvoid removeItem(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    Node *cur = hashMap->buckets[index];\n    Node *pre = NULL;\n    while (cur) {\n        if (cur->pair->key == key) {\n            // そこからキーと値の組を削除する\n            if (pre) {\n                pre->next = cur->next;\n            } else {\n                hashMap->buckets[index] = cur->next;\n            }\n            // メモリを解放する\n            free(cur->pair);\n            free(cur);\n            hashMap->size--;\n            return;\n        }\n        pre = cur;\n        cur = cur->next;\n    }\n}\n\n/* ハッシュテーブルを出力 */\nvoid print(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        printf(\"[\");\n        while (cur) {\n            printf(\"%d -> %s, \", cur->pair->key, cur->pair->val);\n            cur = cur->next;\n        }\n        printf(\"]\\n\");\n    }\n}\n
hash_map_chaining.kt
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    var size: Int // キーと値のペア数\n    var capacity: Int // ハッシュテーブル容量\n    val loadThres: Double // リサイズを発動する負荷率のしきい値\n    val extendRatio: Int // 拡張倍率\n    var buckets: MutableList<MutableList<Pair>> // バケット配列\n\n    /* コンストラクタ */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n    }\n\n    /* ハッシュ関数 */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* 負荷率 */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* 検索操作 */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for (pair in bucket) {\n            if (pair.key == key) return pair._val\n        }\n        // key が見つからない場合は null を返す\n        return null\n    }\n\n    /* 追加操作 */\n    fun put(key: Int, _val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for (pair in bucket) {\n            if (pair.key == key) {\n                pair._val = _val\n                return\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        val pair = Pair(key, _val)\n        bucket.add(pair)\n        size++\n    }\n\n    /* 削除操作 */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // バケットを走査してキーと値のペアを削除\n        for (pair in bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair)\n                size--\n                break\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    fun extend() {\n        // 元のハッシュテーブルを一時保存\n        val bucketsTmp = buckets\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio\n        // mutablelist には固定サイズがない\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n        size = 0\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (bucket in bucketsTmp) {\n            for (pair in bucket) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    fun print() {\n        for (bucket in buckets) {\n            val res = mutableListOf<String>()\n            for (pair in bucket) {\n                val k = pair.key\n                val v = pair._val\n                res.add(\"$k -> $v\")\n            }\n            println(res)\n        }\n    }\n}\n
hash_map_chaining.rb
### キーアドレス法ハッシュテーブル ###\nclass HashMapChaining\n  ### コンストラクタ ###\n  def initialize\n    @size = 0 # キーと値のペア数\n    @capacity = 4 # ハッシュテーブル容量\n    @load_thres = 2.0 / 3.0 # リサイズを発動する負荷率のしきい値\n    @extend_ratio = 2 # 拡張倍率\n    @buckets = Array.new(@capacity) { [] } # バケット配列\n  end\n\n  ### ハッシュ関数 ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### 負荷率 ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### 検索操作 ###\n  def get(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # バケットを走査し、key が見つかれば対応する val を返す\n    for pair in bucket\n      return pair.val if pair.key == key\n    end\n    # `key` が見つからなければ `nil` を返す\n    nil\n  end\n\n  ### 追加操作 ###\n  def put(key, val)\n    # 負荷率がしきい値を超えたら、リサイズを実行\n    extend if load_factor > @load_thres\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n    for pair in bucket\n      if pair.key == key\n        pair.val = val\n        return\n      end\n    end\n    # その key が存在しなければ、キーと値のペアを末尾に追加\n    pair = Pair.new(key, val)\n    bucket << pair\n    @size += 1\n  end\n\n  ### 削除操作 ###\n  def remove(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # バケットを走査してキーと値のペアを削除\n    for pair in bucket\n      if pair.key == key\n        bucket.delete(pair)\n        @size -= 1\n        break\n      end\n    end\n  end\n\n  ### ハッシュテーブルを拡張 ###\n  def extend\n    # 元のハッシュテーブルを一時保存\n    buckets = @buckets\n    # リサイズ後の新しいハッシュテーブルを初期化\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity) { [] }\n    @size = 0\n    # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for bucket in buckets\n      for pair in bucket\n        put(pair.key, pair.val)\n      end\n    end\n  end\n\n  ### ハッシュテーブルを出力 ###\n  def print\n    for bucket in @buckets\n      res = []\n      for pair in bucket\n        res << \"#{pair.key} -> #{pair.val}\"\n      end\n      pp res\n    end\n  end\nend\n
コードの可視化

全画面で見る >

注意すべきなのは、連結リストが長い場合、検索効率 \\(O(n)\\) は非常に低いことです。このとき、連結リストを「AVL 木」または「赤黒木」に変換することで、検索操作の時間計算量を \\(O(\\log n)\\) に最適化できます。

","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#622","level":2,"title":"6.2.2   オープンアドレッシング","text":"

オープンアドレッシング(open addressing)では追加のデータ構造を導入せず、「複数回の探索」によってハッシュ衝突を処理します。探索方法には主に線形探索、二次探索、多重ハッシュなどがあります。

以下では線形探索を例に、オープンアドレッシングハッシュテーブルの動作の仕組みを説明します。

","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#1","level":3,"title":"1.   線形探索","text":"

線形探索では、固定ステップ長の線形探索によって探索を行います。その操作方法は通常のハッシュテーブルとは異なります。

  • 要素の挿入:ハッシュ関数によってバケットインデックスを計算し、バケット内にすでに要素がある場合は、衝突位置から後方へ線形に走査し(ステップ長は通常 \\(1\\) )、空のバケットが見つかるまで進み、その中に要素を挿入します。
  • 要素の検索:ハッシュ衝突が見つかった場合は、同じステップ長で後方へ線形走査を行い、対応する要素が見つかるまで続け、 value を返します。空のバケットに遭遇した場合は、対象要素がハッシュテーブル内に存在しないことを意味するため、 None を返します。

下図はオープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布を示しています。このハッシュ関数では、末尾 2 桁が同じ key はすべて同じバケットに写像されます。線形探索によって、それらはそのバケットとその後続のバケットに順に格納されます。

図 6-6   オープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布

しかし、**線形探索では「クラスタリング現象」が起こりやすい**です。具体的には、配列内で連続して占有された位置が長いほど、それらの連続位置でハッシュ衝突が発生する可能性が高くなり、さらにその位置の集積成長を促して悪循環を生み、最終的には追加・削除・検索・更新操作の効率低下を招きます。

注意すべきなのは、**オープンアドレッシングハッシュテーブルでは要素を直接削除できない**ことです。これは、要素を削除すると配列内に空バケット None が生じ、要素を検索するときに線形探索がその空バケットに到達した時点で返ってしまうため、その空バケットより後ろの要素には二度とアクセスできなくなるからです。結果として、プログラムがそれらの要素を存在しないと誤判定する可能性があります。下図のとおりです。

図 6-7   オープンアドレッシングで要素を削除したことによる検索問題

この問題を解決するために、遅延削除(lazy deletion)の仕組みを採用できます。これは要素をハッシュテーブルから直接取り除かず、代わりに定数 TOMBSTONE を使ってこのバケットをマークします。この仕組みでは、NoneTOMBSTONE はどちらも空バケットを表し、どちらにもキーと値のペアを配置できます。ただし異なるのは、線形探索が TOMBSTONE に到達した場合は、その先にキーと値のペアが存在する可能性があるため、探索を続けるべきだという点です。

しかし、遅延削除はハッシュテーブルの性能劣化を加速させる可能性があります。これは、削除操作のたびに削除マークが 1 つ生成され、TOMBSTONE が増えるにつれて探索時間も増加するためです。線形探索では、対象要素を見つけるまでに複数の TOMBSTONE を飛び越える必要があるかもしれません。

そのため、線形探索では、遭遇した最初の TOMBSTONE のインデックスを記録し、見つかった対象要素とその TOMBSTONE を交換することを考えます。こうする利点は、要素を検索または追加するたびに、要素が理想位置(探索開始点)により近いバケットへ移動し、検索効率が向上することです。

以下のコードは、遅延削除を含むオープンアドレッシング(線形探索)ハッシュテーブルを実装したものです。ハッシュテーブルの空間をより十分に活用するために、ハッシュテーブルを「環状配列」とみなし、配列末尾を越えたら先頭に戻って探索を続けます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_open_addressing.py
class HashMapOpenAddressing:\n    \"\"\"オープンアドレス法ハッシュテーブル\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self.size = 0  # キーと値のペア数\n        self.capacity = 4  # ハッシュテーブル容量\n        self.load_thres = 2.0 / 3.0  # リサイズを発動する負荷率のしきい値\n        self.extend_ratio = 2  # 拡張倍率\n        self.buckets: list[Pair | None] = [None] * self.capacity  # バケット配列\n        self.TOMBSTONE = Pair(-1, \"-1\")  # 削除済みマーク\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"ハッシュ関数\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"負荷率\"\"\"\n        return self.size / self.capacity\n\n    def find_bucket(self, key: int) -> int:\n        \"\"\"key に対応するバケットインデックスを探す\"\"\"\n        index = self.hash_func(key)\n        first_tombstone = -1\n        # 線形プロービングを行い、空バケットに達したら終了\n        while self.buckets[index] is not None:\n            # key が見つかったら、対応するバケットのインデックスを返す\n            if self.buckets[index].key == key:\n                # 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if first_tombstone != -1:\n                    self.buckets[first_tombstone] = self.buckets[index]\n                    self.buckets[index] = self.TOMBSTONE\n                    return first_tombstone  # 移動後のバケットインデックスを返す\n                return index  # バケットのインデックスを返す\n            # 最初に見つかった削除マークを記録\n            if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:\n                first_tombstone = index\n            # バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % self.capacity\n        # key が存在しない場合は追加位置のインデックスを返す\n        return index if first_tombstone == -1 else first_tombstone\n\n    def get(self, key: int) -> str:\n        \"\"\"検索操作\"\"\"\n        # key に対応するバケットインデックスを探す\n        index = self.find_bucket(key)\n        # キーと値の組が見つかったら、対応する val を返す\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            return self.buckets[index].val\n        # キーと値のペアが存在しない場合は `None` を返す\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"追加操作\"\"\"\n        # 負荷率がしきい値を超えたら、リサイズを実行\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        # key に対応するバケットインデックスを探す\n        index = self.find_bucket(key)\n        # キーと値の組が見つかったら、val を上書きして返す\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index].val = val\n            return\n        # キーと値の組が存在しない場合は、その組を追加する\n        self.buckets[index] = Pair(key, val)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"削除操作\"\"\"\n        # key に対応するバケットインデックスを探す\n        index = self.find_bucket(key)\n        # キーと値の組が見つかったら、削除マーカーで上書きする\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index] = self.TOMBSTONE\n            self.size -= 1\n\n    def extend(self):\n        \"\"\"ハッシュテーブルを拡張\"\"\"\n        # 元のハッシュテーブルを一時保存\n        buckets_tmp = self.buckets\n        # リサイズ後の新しいハッシュテーブルを初期化\n        self.capacity *= self.extend_ratio\n        self.buckets = [None] * self.capacity\n        self.size = 0\n        # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for pair in buckets_tmp:\n            if pair not in [None, self.TOMBSTONE]:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"ハッシュテーブルを出力\"\"\"\n        for pair in self.buckets:\n            if pair is None:\n                print(\"None\")\n            elif pair is self.TOMBSTONE:\n                print(\"TOMBSTONE\")\n            else:\n                print(pair.key, \"->\", pair.val)\n
hash_map_open_addressing.cpp
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n  private:\n    int size;                             // キーと値のペア数\n    int capacity = 4;                     // ハッシュテーブル容量\n    const double loadThres = 2.0 / 3.0;     // リサイズを発動する負荷率のしきい値\n    const int extendRatio = 2;            // 拡張倍率\n    vector<Pair *> buckets;               // バケット配列\n    Pair *TOMBSTONE = new Pair(-1, \"-1\"); // 削除済みマーク\n\n  public:\n    /* コンストラクタ */\n    HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {\n    }\n\n    /* デストラクタメソッド */\n    ~HashMapOpenAddressing() {\n        for (Pair *pair : buckets) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                delete pair;\n            }\n        }\n        delete TOMBSTONE;\n    }\n\n    /* ハッシュ関数 */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    double loadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (buckets[index] != nullptr) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (buckets[index]->key == key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* 検索操作 */\n    string get(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            return buckets[index]->val;\n        }\n        // キーと値の組が存在しない場合は空文字列を返す\n        return \"\";\n    }\n\n    /* 追加操作 */\n    void put(int key, string val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            buckets[index]->val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* 削除操作 */\n    void remove(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            delete buckets[index];\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    void extend() {\n        // 元のハッシュテーブルを一時保存\n        vector<Pair *> bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets = vector<Pair *>(capacity, nullptr);\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (Pair *pair : bucketsTmp) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                put(pair->key, pair->val);\n                delete pair;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    void print() {\n        for (Pair *pair : buckets) {\n            if (pair == nullptr) {\n                cout << \"nullptr\" << endl;\n            } else if (pair == TOMBSTONE) {\n                cout << \"TOMBSTONE\" << endl;\n            } else {\n                cout << pair->key << \" -> \" << pair->val << endl;\n            }\n        }\n    }\n};\n
hash_map_open_addressing.java
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    private int size; // キーと値のペア数\n    private int capacity = 4; // ハッシュテーブル容量\n    private final double loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値\n    private final int extendRatio = 2; // 拡張倍率\n    private Pair[] buckets; // バケット配列\n    private final Pair TOMBSTONE = new Pair(-1, \"-1\"); // 削除済みマーク\n\n    /* コンストラクタ */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* ハッシュ関数 */\n    private int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    private double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    private int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (buckets[index] != null) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (buckets[index].key == key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* 検索操作 */\n    public String get(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // キーと値の組が存在しなければ null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    public void put(int key, String val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* 削除操作 */\n    public void remove(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    private void extend() {\n        // 元のハッシュテーブルを一時保存\n        Pair[] bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (Pair pair : bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    public void print() {\n        for (Pair pair : buckets) {\n            if (pair == null) {\n                System.out.println(\"null\");\n            } else if (pair == TOMBSTONE) {\n                System.out.println(\"TOMBSTONE\");\n            } else {\n                System.out.println(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.cs
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    int size; // キーと値のペア数\n    int capacity = 4; // ハッシュテーブル容量\n    double loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値\n    int extendRatio = 2; // 拡張倍率\n    Pair[] buckets; // バケット配列\n    Pair TOMBSTONE = new(-1, \"-1\"); // 削除済みマーク\n\n    /* コンストラクタ */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* ハッシュ関数 */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    int FindBucket(int key) {\n        int index = HashFunc(key);\n        int firstTombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (buckets[index] != null) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (buckets[index].key == key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* 検索操作 */\n    public string? Get(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = FindBucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // キーと値の組が存在しなければ null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    public void Put(int key, string val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        // key に対応するバケットインデックスを探す\n        int index = FindBucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* 削除操作 */\n    public void Remove(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = FindBucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    void Extend() {\n        // 元のハッシュテーブルを一時保存\n        Pair[] bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        foreach (Pair pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    public void Print() {\n        foreach (Pair pair in buckets) {\n            if (pair == null) {\n                Console.WriteLine(\"null\");\n            } else if (pair == TOMBSTONE) {\n                Console.WriteLine(\"TOMBSTONE\");\n            } else {\n                Console.WriteLine(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.go
/* オープンアドレス法ハッシュテーブル */\ntype hashMapOpenAddressing struct {\n    size        int     // キーと値のペア数\n    capacity    int     // ハッシュテーブル容量\n    loadThres   float64 // リサイズを発動する負荷率のしきい値\n    extendRatio int     // 拡張倍率\n    buckets     []*pair // バケット配列\n    TOMBSTONE   *pair   // 削除済みマーク\n}\n\n/* コンストラクタ */\nfunc newHashMapOpenAddressing() *hashMapOpenAddressing {\n    return &hashMapOpenAddressing{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     make([]*pair, 4),\n        TOMBSTONE:   &pair{-1, \"-1\"},\n    }\n}\n\n/* ハッシュ関数 */\nfunc (h *hashMapOpenAddressing) hashFunc(key int) int {\n    return key % h.capacity // キーに基づいてハッシュ値を計算\n}\n\n/* 負荷率 */\nfunc (h *hashMapOpenAddressing) loadFactor() float64 {\n    return float64(h.size) / float64(h.capacity) // 現在の負荷率を計算\n}\n\n/* key に対応するバケットインデックスを探す */\nfunc (h *hashMapOpenAddressing) findBucket(key int) int {\n    index := h.hashFunc(key) // 初期インデックスを取得\n    firstTombstone := -1     // 最初に遭遇した `TOMBSTONE` の位置を記録する\n    for h.buckets[index] != nil {\n        if h.buckets[index].key == key {\n            if firstTombstone != -1 {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                h.buckets[firstTombstone] = h.buckets[index]\n                h.buckets[index] = h.TOMBSTONE\n                return firstTombstone // 移動後のバケットインデックスを返す\n            }\n            return index // 見つかったインデックスを返す\n        }\n        if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE {\n            firstTombstone = index // 最初に遭遇した削除マークの位置を記録する\n        }\n        index = (index + 1) % h.capacity // 線形探索を行い、末尾を越えたら先頭に戻る\n    }\n    // key が存在しない場合は追加位置のインデックスを返す\n    if firstTombstone != -1 {\n        return firstTombstone\n    }\n    return index\n}\n\n/* 検索操作 */\nfunc (h *hashMapOpenAddressing) get(key int) string {\n    index := h.findBucket(key) // key に対応するバケットインデックスを探す\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        return h.buckets[index].val // キーと値の組が見つかったら、対応する val を返す\n    }\n    return \"\" // キーと値のペアが存在しない場合は `\"\"` を返す\n}\n\n/* 追加操作 */\nfunc (h *hashMapOpenAddressing) put(key int, val string) {\n    if h.loadFactor() > h.loadThres {\n        h.extend() // 負荷率がしきい値を超えたら、リサイズを実行\n    }\n    index := h.findBucket(key) // key に対応するバケットインデックスを探す\n    if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE {\n        h.buckets[index] = &pair{key, val} // キーと値の組が存在しない場合は、その組を追加する\n        h.size++\n    } else {\n        h.buckets[index].val = val // キーと値のペアが見つかった場合は、`val` を上書きする\n    }\n}\n\n/* 削除操作 */\nfunc (h *hashMapOpenAddressing) remove(key int) {\n    index := h.findBucket(key) // key に対応するバケットインデックスを探す\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        h.buckets[index] = h.TOMBSTONE // キーと値の組が見つかったら、削除マーカーで上書きする\n        h.size--\n    }\n}\n\n/* ハッシュテーブルを拡張 */\nfunc (h *hashMapOpenAddressing) extend() {\n    oldBuckets := h.buckets               // 元のハッシュテーブルを一時保存\n    h.capacity *= h.extendRatio           // 容量を更新\n    h.buckets = make([]*pair, h.capacity) // リサイズ後の新しいハッシュテーブルを初期化\n    h.size = 0                            // サイズをリセットする\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for _, pair := range oldBuckets {\n        if pair != nil && pair != h.TOMBSTONE {\n            h.put(pair.key, pair.val)\n        }\n    }\n}\n\n/* ハッシュテーブルを出力 */\nfunc (h *hashMapOpenAddressing) print() {\n    for _, pair := range h.buckets {\n        if pair == nil {\n            fmt.Println(\"nil\")\n        } else if pair == h.TOMBSTONE {\n            fmt.Println(\"TOMBSTONE\")\n        } else {\n            fmt.Printf(\"%d -> %s\\n\", pair.key, pair.val)\n        }\n    }\n}\n
hash_map_open_addressing.swift
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    var size: Int // キーと値のペア数\n    var capacity: Int // ハッシュテーブル容量\n    var loadThres: Double // リサイズを発動する負荷率のしきい値\n    var extendRatio: Int // 拡張倍率\n    var buckets: [Pair?] // バケット配列\n    var TOMBSTONE: Pair // 削除済みマーク\n\n    /* コンストラクタ */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: nil, count: capacity)\n        TOMBSTONE = Pair(key: -1, val: \"-1\")\n    }\n\n    /* ハッシュ関数 */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* 負荷率 */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    func findBucket(key: Int) -> Int {\n        var index = hashFunc(key: key)\n        var firstTombstone = -1\n        // 線形プロービングを行い、空バケットに達したら終了\n        while buckets[index] != nil {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if buckets[index]!.key == key {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if firstTombstone != -1 {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // 移動後のバケットインデックスを返す\n                }\n                return index // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if firstTombstone == -1 && buckets[index] == TOMBSTONE {\n                firstTombstone = index\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % capacity\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone == -1 ? index : firstTombstone\n    }\n\n    /* 検索操作 */\n    func get(key: Int) -> String? {\n        // key に対応するバケットインデックスを探す\n        let index = findBucket(key: key)\n        // キーと値の組が見つかったら、対応する val を返す\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            return buckets[index]!.val\n        }\n        // キーと値の組が存在しなければ null を返す\n        return nil\n    }\n\n    /* 追加操作 */\n    func put(key: Int, val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if loadFactor() > loadThres {\n            extend()\n        }\n        // key に対応するバケットインデックスを探す\n        let index = findBucket(key: key)\n        // キーと値の組が見つかったら、val を上書きして返す\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index]!.val = val\n            return\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        buckets[index] = Pair(key: key, val: val)\n        size += 1\n    }\n\n    /* 削除操作 */\n    func remove(key: Int) {\n        // key に対応するバケットインデックスを探す\n        let index = findBucket(key: key)\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index] = TOMBSTONE\n            size -= 1\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    func extend() {\n        // 元のハッシュテーブルを一時保存\n        let bucketsTmp = buckets\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio\n        buckets = Array(repeating: nil, count: capacity)\n        size = 0\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for pair in bucketsTmp {\n            if let pair, pair != TOMBSTONE {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    func print() {\n        for pair in buckets {\n            if pair == nil {\n                Swift.print(\"null\")\n            } else if pair == TOMBSTONE {\n                Swift.print(\"TOMBSTONE\")\n            } else {\n                Swift.print(\"\\(pair!.key) -> \\(pair!.val)\")\n            }\n        }\n    }\n}\n
hash_map_open_addressing.js
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    #size; // キーと値のペア数\n    #capacity; // ハッシュテーブル容量\n    #loadThres; // リサイズを発動する負荷率のしきい値\n    #extendRatio; // 拡張倍率\n    #buckets; // バケット配列\n    #TOMBSTONE; // 削除済みマーク\n\n    /* コンストラクタ */\n    constructor() {\n        this.#size = 0; // キーと値のペア数\n        this.#capacity = 4; // ハッシュテーブル容量\n        this.#loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値\n        this.#extendRatio = 2; // 拡張倍率\n        this.#buckets = Array(this.#capacity).fill(null); // バケット配列\n        this.#TOMBSTONE = new Pair(-1, '-1'); // 削除済みマーク\n    }\n\n    /* ハッシュ関数 */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* 負荷率 */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    #findBucket(key) {\n        let index = this.#hashFunc(key);\n        let firstTombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (this.#buckets[index] !== null) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (this.#buckets[index].key === key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone !== -1) {\n                    this.#buckets[firstTombstone] = this.#buckets[index];\n                    this.#buckets[index] = this.#TOMBSTONE;\n                    return firstTombstone; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (\n                firstTombstone === -1 &&\n                this.#buckets[index] === this.#TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % this.#capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* 検索操作 */\n    get(key) {\n        // key に対応するバケットインデックスを探す\n        const index = this.#findBucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            return this.#buckets[index].val;\n        }\n        // キーと値の組が存在しなければ null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    put(key, val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        // key に対応するバケットインデックスを探す\n        const index = this.#findBucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index].val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        this.#buckets[index] = new Pair(key, val);\n        this.#size++;\n    }\n\n    /* 削除操作 */\n    remove(key) {\n        // key に対応するバケットインデックスを探す\n        const index = this.#findBucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index] = this.#TOMBSTONE;\n            this.#size--;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    #extend() {\n        // 元のハッシュテーブルを一時保存\n        const bucketsTmp = this.#buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = Array(this.#capacity).fill(null);\n        this.#size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.#TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    print() {\n        for (const pair of this.#buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.#TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.ts
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    private size: number; // キーと値のペア数\n    private capacity: number; // ハッシュテーブル容量\n    private loadThres: number; // リサイズを発動する負荷率のしきい値\n    private extendRatio: number; // 拡張倍率\n    private buckets: Array<Pair | null>; // バケット配列\n    private TOMBSTONE: Pair; // 削除済みマーク\n\n    /* コンストラクタ */\n    constructor() {\n        this.size = 0; // キーと値のペア数\n        this.capacity = 4; // ハッシュテーブル容量\n        this.loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値\n        this.extendRatio = 2; // 拡張倍率\n        this.buckets = Array(this.capacity).fill(null); // バケット配列\n        this.TOMBSTONE = new Pair(-1, '-1'); // 削除済みマーク\n    }\n\n    /* ハッシュ関数 */\n    private hashFunc(key: number): number {\n        return key % this.capacity;\n    }\n\n    /* 負荷率 */\n    private loadFactor(): number {\n        return this.size / this.capacity;\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    private findBucket(key: number): number {\n        let index = this.hashFunc(key);\n        let firstTombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (this.buckets[index] !== null) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (this.buckets[index]!.key === key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone !== -1) {\n                    this.buckets[firstTombstone] = this.buckets[index];\n                    this.buckets[index] = this.TOMBSTONE;\n                    return firstTombstone; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (\n                firstTombstone === -1 &&\n                this.buckets[index] === this.TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % this.capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* 検索操作 */\n    get(key: number): string | null {\n        // key に対応するバケットインデックスを探す\n        const index = this.findBucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            return this.buckets[index]!.val;\n        }\n        // キーと値の組が存在しなければ null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    put(key: number, val: string): void {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (this.loadFactor() > this.loadThres) {\n            this.extend();\n        }\n        // key に対応するバケットインデックスを探す\n        const index = this.findBucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index]!.val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        this.buckets[index] = new Pair(key, val);\n        this.size++;\n    }\n\n    /* 削除操作 */\n    remove(key: number): void {\n        // key に対応するバケットインデックスを探す\n        const index = this.findBucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index] = this.TOMBSTONE;\n            this.size--;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    private extend(): void {\n        // 元のハッシュテーブルを一時保存\n        const bucketsTmp = this.buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        this.capacity *= this.extendRatio;\n        this.buckets = Array(this.capacity).fill(null);\n        this.size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    print(): void {\n        for (const pair of this.buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.dart
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n  late int _size; // キーと値のペア数\n  int _capacity = 4; // ハッシュテーブル容量\n  double _loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値\n  int _extendRatio = 2; // 拡張倍率\n  late List<Pair?> _buckets; // バケット配列\n  Pair _TOMBSTONE = Pair(-1, \"-1\"); // 削除済みマーク\n\n  /* コンストラクタ */\n  HashMapOpenAddressing() {\n    _size = 0;\n    _buckets = List.generate(_capacity, (index) => null);\n  }\n\n  /* ハッシュ関数 */\n  int hashFunc(int key) {\n    return key % _capacity;\n  }\n\n  /* 負荷率 */\n  double loadFactor() {\n    return _size / _capacity;\n  }\n\n  /* key に対応するバケットインデックスを探す */\n  int findBucket(int key) {\n    int index = hashFunc(key);\n    int firstTombstone = -1;\n    // 線形プロービングを行い、空バケットに達したら終了\n    while (_buckets[index] != null) {\n      // key が見つかったら、対応するバケットのインデックスを返す\n      if (_buckets[index]!.key == key) {\n        // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n        if (firstTombstone != -1) {\n          _buckets[firstTombstone] = _buckets[index];\n          _buckets[index] = _TOMBSTONE;\n          return firstTombstone; // 移動後のバケットインデックスを返す\n        }\n        return index; // バケットのインデックスを返す\n      }\n      // 最初に見つかった削除マークを記録\n      if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) {\n        firstTombstone = index;\n      }\n      // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n      index = (index + 1) % _capacity;\n    }\n    // key が存在しない場合は追加位置のインデックスを返す\n    return firstTombstone == -1 ? index : firstTombstone;\n  }\n\n  /* 検索操作 */\n  String? get(int key) {\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(key);\n    // キーと値の組が見つかったら、対応する val を返す\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      return _buckets[index]!.val;\n    }\n    // キーと値の組が存在しなければ null を返す\n    return null;\n  }\n\n  /* 追加操作 */\n  void put(int key, String val) {\n    // 負荷率がしきい値を超えたら、リサイズを実行\n    if (loadFactor() > _loadThres) {\n      extend();\n    }\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(key);\n    // キーと値の組が見つかったら、val を上書きして返す\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index]!.val = val;\n      return;\n    }\n    // キーと値の組が存在しない場合は、その組を追加する\n    _buckets[index] = new Pair(key, val);\n    _size++;\n  }\n\n  /* 削除操作 */\n  void remove(int key) {\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(key);\n    // キーと値の組が見つかったら、削除マーカーで上書きする\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index] = _TOMBSTONE;\n      _size--;\n    }\n  }\n\n  /* ハッシュテーブルを拡張 */\n  void extend() {\n    // 元のハッシュテーブルを一時保存\n    List<Pair?> bucketsTmp = _buckets;\n    // リサイズ後の新しいハッシュテーブルを初期化\n    _capacity *= _extendRatio;\n    _buckets = List.generate(_capacity, (index) => null);\n    _size = 0;\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for (Pair? pair in bucketsTmp) {\n      if (pair != null && pair != _TOMBSTONE) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* ハッシュテーブルを出力 */\n  void printHashMap() {\n    for (Pair? pair in _buckets) {\n      if (pair == null) {\n        print(\"null\");\n      } else if (pair == _TOMBSTONE) {\n        print(\"TOMBSTONE\");\n      } else {\n        print(\"${pair.key} -> ${pair.val}\");\n      }\n    }\n  }\n}\n
hash_map_open_addressing.rs
/* オープンアドレス法ハッシュテーブル */\nstruct HashMapOpenAddressing {\n    size: usize,                // キーと値のペア数\n    capacity: usize,            // ハッシュテーブル容量\n    load_thres: f64,            // リサイズを発動する負荷率のしきい値\n    extend_ratio: usize,        // 拡張倍率\n    buckets: Vec<Option<Pair>>, // バケット配列\n    TOMBSTONE: Option<Pair>,    // 削除済みマーク\n}\n\nimpl HashMapOpenAddressing {\n    /* コンストラクタ */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![None; 4],\n            TOMBSTONE: Some(Pair {\n                key: -1,\n                val: \"-1\".to_string(),\n            }),\n        }\n    }\n\n    /* ハッシュ関数 */\n    fn hash_func(&self, key: i32) -> usize {\n        (key % self.capacity as i32) as usize\n    }\n\n    /* 負荷率 */\n    fn load_factor(&self) -> f64 {\n        self.size as f64 / self.capacity as f64\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    fn find_bucket(&mut self, key: i32) -> usize {\n        let mut index = self.hash_func(key);\n        let mut first_tombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while self.buckets[index].is_some() {\n            // `key` に遭遇したら、対応するバケットのインデックスを返す\n            if self.buckets[index].as_ref().unwrap().key == key {\n                // 以前に削除マークに遭遇していた場合は、キーと値のペアをそのインデックスへ移動する\n                if first_tombstone != -1 {\n                    self.buckets[first_tombstone as usize] = self.buckets[index].take();\n                    self.buckets[index] = self.TOMBSTONE.clone();\n                    return first_tombstone as usize; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE {\n                first_tombstone = index as i32;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % self.capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        if first_tombstone == -1 {\n            index\n        } else {\n            first_tombstone as usize\n        }\n    }\n\n    /* 検索操作 */\n    fn get(&mut self, key: i32) -> Option<&str> {\n        // key に対応するバケットインデックスを探す\n        let index = self.find_bucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            return self.buckets[index].as_ref().map(|pair| &pair.val as &str);\n        }\n        // キーと値の組が存在しなければ null を返す\n        None\n    }\n\n    /* 追加操作 */\n    fn put(&mut self, key: i32, val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n        // key に対応するバケットインデックスを探す\n        let index = self.find_bucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index].as_mut().unwrap().val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        self.buckets[index] = Some(Pair { key, val });\n        self.size += 1;\n    }\n\n    /* 削除操作 */\n    fn remove(&mut self, key: i32) {\n        // key に対応するバケットインデックスを探す\n        let index = self.find_bucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index] = self.TOMBSTONE.clone();\n            self.size -= 1;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    fn extend(&mut self) {\n        // 元のハッシュテーブルを一時保存\n        let buckets_tmp = self.buckets.clone();\n        // リサイズ後の新しいハッシュテーブルを初期化\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![None; self.capacity];\n        self.size = 0;\n\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for pair in buckets_tmp {\n            if pair.is_none() || pair == self.TOMBSTONE {\n                continue;\n            }\n            let pair = pair.unwrap();\n\n            self.put(pair.key, pair.val);\n        }\n    }\n    /* ハッシュテーブルを出力 */\n    fn print(&self) {\n        for pair in &self.buckets {\n            if pair.is_none() {\n                println!(\"null\");\n            } else if pair == &self.TOMBSTONE {\n                println!(\"TOMBSTONE\");\n            } else {\n                let pair = pair.as_ref().unwrap();\n                println!(\"{} -> {}\", pair.key, pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.c
/* オープンアドレス法ハッシュテーブル */\ntypedef struct {\n    int size;         // キーと値のペア数\n    int capacity;     // ハッシュテーブル容量\n    double loadThres; // リサイズを発動する負荷率のしきい値\n    int extendRatio;  // 拡張倍率\n    Pair **buckets;   // バケット配列\n    Pair *TOMBSTONE;  // 削除済みマーク\n} HashMapOpenAddressing;\n\n/* コンストラクタ */\nHashMapOpenAddressing *newHashMapOpenAddressing() {\n    HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->TOMBSTONE = (Pair *)malloc(sizeof(Pair));\n    hashMap->TOMBSTONE->key = -1;\n    hashMap->TOMBSTONE->val = \"-1\";\n\n    return hashMap;\n}\n\n/* デストラクタ */\nvoid delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap->TOMBSTONE);\n    free(hashMap);\n}\n\n/* ハッシュ関数 */\nint hashFunc(HashMapOpenAddressing *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* 負荷率 */\ndouble loadFactor(HashMapOpenAddressing *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* key に対応するバケットインデックスを探す */\nint findBucket(HashMapOpenAddressing *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    int firstTombstone = -1;\n    // 線形プロービングを行い、空バケットに達したら終了\n    while (hashMap->buckets[index] != NULL) {\n        // key が見つかったら、対応するバケットのインデックスを返す\n        if (hashMap->buckets[index]->key == key) {\n            // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n            if (firstTombstone != -1) {\n                hashMap->buckets[firstTombstone] = hashMap->buckets[index];\n                hashMap->buckets[index] = hashMap->TOMBSTONE;\n                return firstTombstone; // 移動後のバケットインデックスを返す\n            }\n            return index; // バケットのインデックスを返す\n        }\n        // 最初に見つかった削除マークを記録\n        if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) {\n            firstTombstone = index;\n        }\n        // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n        index = (index + 1) % hashMap->capacity;\n    }\n    // key が存在しない場合は追加位置のインデックスを返す\n    return firstTombstone == -1 ? index : firstTombstone;\n}\n\n/* 検索操作 */\nchar *get(HashMapOpenAddressing *hashMap, int key) {\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(hashMap, key);\n    // キーと値の組が見つかったら、対応する val を返す\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        return hashMap->buckets[index]->val;\n    }\n    // キーと値の組が存在しない場合は空文字列を返す\n    return \"\";\n}\n\n/* 追加操作 */\nvoid put(HashMapOpenAddressing *hashMap, int key, char *val) {\n    // 負荷率がしきい値を超えたら、リサイズを実行\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(hashMap, key);\n    // キーと値の組が見つかったら、val を上書きして返す\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        free(hashMap->buckets[index]->val);\n        hashMap->buckets[index]->val = (char *)malloc(sizeof(strlen(val) + 1));\n        strcpy(hashMap->buckets[index]->val, val);\n        hashMap->buckets[index]->val[strlen(val)] = '\\0';\n        return;\n    }\n    // キーと値の組が存在しない場合は、その組を追加する\n    Pair *pair = (Pair *)malloc(sizeof(Pair));\n    pair->key = key;\n    pair->val = (char *)malloc(sizeof(strlen(val) + 1));\n    strcpy(pair->val, val);\n    pair->val[strlen(val)] = '\\0';\n\n    hashMap->buckets[index] = pair;\n    hashMap->size++;\n}\n\n/* 削除操作 */\nvoid removeItem(HashMapOpenAddressing *hashMap, int key) {\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(hashMap, key);\n    // キーと値の組が見つかったら、削除マーカーで上書きする\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        Pair *pair = hashMap->buckets[index];\n        free(pair->val);\n        free(pair);\n        hashMap->buckets[index] = hashMap->TOMBSTONE;\n        hashMap->size--;\n    }\n}\n\n/* ハッシュテーブルを拡張 */\nvoid extend(HashMapOpenAddressing *hashMap) {\n    // 元のハッシュテーブルを一時保存\n    Pair **bucketsTmp = hashMap->buckets;\n    int oldCapacity = hashMap->capacity;\n    // リサイズ後の新しいハッシュテーブルを初期化\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->size = 0;\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for (int i = 0; i < oldCapacity; i++) {\n        Pair *pair = bucketsTmp[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            put(hashMap, pair->key, pair->val);\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(bucketsTmp);\n}\n\n/* ハッシュテーブルを出力 */\nvoid print(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair == NULL) {\n            printf(\"NULL\\n\");\n        } else if (pair == hashMap->TOMBSTONE) {\n            printf(\"TOMBSTONE\\n\");\n        } else {\n            printf(\"%d -> %s\\n\", pair->key, pair->val);\n        }\n    }\n}\n
hash_map_open_addressing.kt
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    private var size: Int               // キーと値のペア数\n    private var capacity: Int           // ハッシュテーブル容量\n    private val loadThres: Double       // リサイズを発動する負荷率のしきい値\n    private val extendRatio: Int        // 拡張倍率\n    private var buckets: Array<Pair?>   // バケット配列\n    private val TOMBSTONE: Pair         // 削除済みマーク\n\n    /* コンストラクタ */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = arrayOfNulls(capacity)\n        TOMBSTONE = Pair(-1, \"-1\")\n    }\n\n    /* ハッシュ関数 */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* 負荷率 */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    fun findBucket(key: Int): Int {\n        var index = hashFunc(key)\n        var firstTombstone = -1\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (buckets[index] != null) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (buckets[index]?.key == key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // 移動後のバケットインデックスを返す\n                }\n                return index // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % capacity\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return if (firstTombstone == -1) index else firstTombstone\n    }\n\n    /* 検索操作 */\n    fun get(key: Int): String? {\n        // key に対応するバケットインデックスを探す\n        val index = findBucket(key)\n        // キーと値の組が見つかったら、対応する val を返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index]?._val\n        }\n        // キーと値の組が存在しなければ null を返す\n        return null\n    }\n\n    /* 追加操作 */\n    fun put(key: Int, _val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        // key に対応するバケットインデックスを探す\n        val index = findBucket(key)\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index]!!._val = _val\n            return\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        buckets[index] = Pair(key, _val)\n        size++\n    }\n\n    /* 削除操作 */\n    fun remove(key: Int) {\n        // key に対応するバケットインデックスを探す\n        val index = findBucket(key)\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE\n            size--\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    fun extend() {\n        // 元のハッシュテーブルを一時保存\n        val bucketsTmp = buckets\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio\n        buckets = arrayOfNulls(capacity)\n        size = 0\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    fun print() {\n        for (pair in buckets) {\n            if (pair == null) {\n                println(\"null\")\n            } else if (pair == TOMBSTONE) {\n                println(\"TOMESTOME\")\n            } else {\n                println(\"${pair.key} -> ${pair._val}\")\n            }\n        }\n    }\n}\n
hash_map_open_addressing.rb
### オープンアドレス法ハッシュテーブル ###\nclass HashMapOpenAddressing\n  TOMBSTONE = Pair.new(-1, '-1') # 削除済みマーク\n\n  ### コンストラクタ ###\n  def initialize\n    @size = 0 # キーと値のペア数\n    @capacity = 4 # ハッシュテーブル容量\n    @load_thres = 2.0 / 3.0 # リサイズを発動する負荷率のしきい値\n    @extend_ratio = 2 # 拡張倍率\n    @buckets = Array.new(@capacity) # バケット配列\n  end\n\n  ### ハッシュ関数 ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### 負荷率 ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### key に対応するバケットインデックスを検索 ###\n  def find_bucket(key)\n    index = hash_func(key)\n    first_tombstone = -1\n    # 線形プロービングを行い、空バケットに達したら終了\n    while !@buckets[index].nil?\n      # key が見つかったら、対応するバケットのインデックスを返す\n      if @buckets[index].key == key\n        # 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n        if first_tombstone != -1\n          @buckets[first_tombstone] = @buckets[index]\n          @buckets[index] = TOMBSTONE\n          return first_tombstone # 移動後のバケットインデックスを返す\n        end\n        return index # バケットのインデックスを返す\n      end\n      # 最初に見つかった削除マークを記録\n      first_tombstone = index if first_tombstone == -1 && @buckets[index] == TOMBSTONE\n      # バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n      index = (index + 1) % @capacity\n    end\n    # key が存在しない場合は追加位置のインデックスを返す\n    first_tombstone == -1 ? index : first_tombstone\n  end\n\n  ### 検索操作 ###\n  def get(key)\n    # key に対応するバケットインデックスを探す\n    index = find_bucket(key)\n    # キーと値の組が見つかったら、対応する val を返す\n    return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index])\n    # キーと値のペアが存在しない場合は `nil` を返す\n    nil\n  end\n\n  ### 追加操作 ###\n  def put(key, val)\n    # 負荷率がしきい値を超えたら、リサイズを実行\n    extend if load_factor > @load_thres\n    # key に対応するバケットインデックスを探す\n    index = find_bucket(key)\n    # キーと値のペアが見つかった場合は、`val` を上書きして返す\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index].val = val\n      return\n    end\n    # キーと値の組が存在しない場合は、その組を追加する\n    @buckets[index] = Pair.new(key, val)\n    @size += 1\n  end\n\n  ### 削除操作 ###\n  def remove(key)\n    # key に対応するバケットインデックスを探す\n    index = find_bucket(key)\n    # キーと値の組が見つかったら、削除マーカーで上書きする\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index] = TOMBSTONE\n      @size -= 1\n    end\n  end\n\n  ### ハッシュテーブルを拡張 ###\n  def extend\n    # 元のハッシュテーブルを一時保存\n    buckets_tmp = @buckets\n    # リサイズ後の新しいハッシュテーブルを初期化\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity)\n    @size = 0\n    # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for pair in buckets_tmp\n      put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair)\n    end\n  end\n\n  ### ハッシュテーブルを出力 ###\n  def print\n    for pair in @buckets\n      if pair.nil?\n        puts \"Nil\"\n      elsif pair == TOMBSTONE\n        puts \"TOMBSTONE\"\n      else\n        puts \"#{pair.key} -> #{pair.val}\"\n      end\n    end\n  end\nend\n
","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#2","level":3,"title":"2.   二次探索","text":"

二次探索は線形探索に似ており、オープンアドレッシングの一般的な戦略の 1 つです。衝突が発生したとき、二次探索では単純に固定歩数を飛ばすのではなく、「探索回数の二乗」に相当する歩数、すなわち \\(1, 4, 9, \\dots\\) 歩を飛ばします。

二次探索には主に次の利点があります。

  • 二次探索は、探索回数の二乗の距離を飛ばすことで、線形探索のクラスタリング効果を緩和しようとします。
  • 二次探索はより大きな距離を飛ばして空き位置を探すため、データ分布がより均一になるのに役立ちます。

しかし、二次探索は完璧ではありません。

  • 依然としてクラスタリング現象は存在し、ある位置が他の位置より占有されやすいことがあります。
  • 二乗の増加により、二次探索はハッシュテーブル全体を探索できない可能性があります。これは、ハッシュテーブルに空バケットがあっても、二次探索ではそこに到達できないことがあることを意味します。
","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#3","level":3,"title":"3.   多重ハッシュ","text":"

その名のとおり、多重ハッシュ法では複数のハッシュ関数 \\(f_1(x)\\)、\\(f_2(x)\\)、\\(f_3(x)\\)、\\(\\dots\\) を使って探索を行います。

  • 要素の挿入:ハッシュ関数 \\(f_1(x)\\) で衝突が発生した場合は、\\(f_2(x)\\) を試し、以下同様に、空き位置が見つかるまで続けてから要素を挿入します。
  • 要素の検索:同じハッシュ関数の順序で探索し、対象要素が見つかった時点で返します。空き位置に遭遇するか、すべてのハッシュ関数を試しても見つからない場合は、ハッシュテーブル内にその要素は存在しないため、 None を返します。

線形探索と比べると、多重ハッシュ法はクラスタリングを起こしにくい一方で、複数のハッシュ関数により追加の計算量が発生します。

Tip

注意してください。オープンアドレッシング(線形探索、二次探索、多重ハッシュ)のハッシュテーブルには、いずれも「要素を直接削除できない」という問題があります。

","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#623","level":2,"title":"6.2.3   プログラミング言語の選択","text":"

各種プログラミング言語は異なるハッシュテーブル実装戦略を採用しています。以下にいくつか例を挙げます。

  • Python はオープンアドレッシングを採用しています。辞書 dict は疑似乱数を用いて探索します。
  • Java はチェイン法を採用しています。JDK 1.8 以降、HashMap 内の配列長が 64 に達し、かつ連結リスト長が 8 に達すると、連結リストは検索性能を高めるため赤黒木に変換されます。
  • Go はチェイン法を採用しています。Go では各バケットに最大 8 個のキーと値のペアを格納でき、容量を超えるとオーバーフローバケットを連結します。オーバーフローバケットが多すぎる場合は、性能を確保するために特殊な等量拡張操作を実行します。
","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   ハッシュテーブル","text":"

ハッシュテーブル(hash table)は、散列表とも呼ばれ、キー key と値 value の対応関係を構築することで、高効率な要素検索を実現します。具体的には、ハッシュテーブルにキー key を入力すると、対応する値 value を \\(O(1)\\) 時間で取得できます。

以下の図に示すように、\\(n\\) 人の学生がいるとし、各学生は「名前」と「学籍番号」の 2 つの情報を持っています。もし「学籍番号を入力すると対応する名前を返す」という検索機能を実現したいなら、下図のようなハッシュテーブルを用いることができます。

図 6-1   ハッシュテーブルの抽象表現

ハッシュテーブルのほかに、配列や連結リストでも検索機能を実現できます。それらの効率比較を次の表に示します。

  • 要素の追加:要素を配列(連結リスト)の末尾に追加するだけでよく、\\(O(1)\\) 時間です。
  • 要素の検索:配列(連結リスト)は無秩序なので、すべての要素を走査する必要があり、\\(O(n)\\) 時間かかります。
  • 要素の削除:先に要素を検索してから配列(連結リスト)から削除する必要があり、\\(O(n)\\) 時間かかります。

表 6-1   要素検索効率の比較

配列 連結リスト ハッシュテーブル 要素の検索 \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) 要素の追加 \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) 要素の削除 \\(O(n)\\) \\(O(n)\\) \\(O(1)\\)

以上から分かるように、ハッシュテーブルにおける追加・削除・検索・更新の時間計算量はいずれも \\(O(1)\\) であり、非常に高効率です。

","path":["第 6 章   ハッシュテーブル","6.1   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/hash_map/#611","level":2,"title":"6.1.1   ハッシュテーブルの基本操作","text":"

ハッシュテーブルの一般的な操作には、初期化、検索、キーと値のペアの追加、キーと値のペアの削除などがあります。コード例は以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
# ハッシュテーブルを初期化\nhmap: dict = {}\n\n# 追加操作\n# ハッシュテーブルにキーと値のペア (key, value) を追加\nhmap[12836] = \"シャオハ\"\nhmap[15937] = \"シャオルオ\"\nhmap[16750] = \"シャオスワン\"\nhmap[13276] = \"シャオファ\"\nhmap[10583] = \"シャオヤー\"\n\n# 検索操作\n# ハッシュテーブルにキー key を入力し、値 value を取得\nname: str = hmap[15937]\n\n# 削除操作\n# ハッシュテーブルからキーと値のペア (key, value) を削除\nhmap.pop(10583)\n
hash_map.cpp
/* ハッシュテーブルを初期化 */\nunordered_map<int, string> map;\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap[12836] = \"シャオハ\";\nmap[15937] = \"シャオルオ\";\nmap[16750] = \"シャオスワン\";\nmap[13276] = \"シャオファ\";\nmap[10583] = \"シャオヤー\";\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nstring name = map[15937];\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.erase(10583);\n
hash_map.java
/* ハッシュテーブルを初期化 */\nMap<Integer, String> map = new HashMap<>();\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap.put(12836, \"シャオハ\");\nmap.put(15937, \"シャオルオ\");\nmap.put(16750, \"シャオスワン\");\nmap.put(13276, \"シャオファ\");\nmap.put(10583, \"シャオヤー\");\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nString name = map.get(15937);\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.remove(10583);\n
hash_map.cs
/* ハッシュテーブルを初期化 */\nDictionary<int, string> map = new() {\n    /* 追加操作 */\n    // ハッシュテーブルにキーと値のペア (key, value) を追加\n    { 12836, \"シャオハ\" },\n    { 15937, \"シャオルオ\" },\n    { 16750, \"シャオスワン\" },\n    { 13276, \"シャオファ\" },\n    { 10583, \"シャオヤー\" }\n};\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nstring name = map[15937];\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.Remove(10583);\n
hash_map_test.go
/* ハッシュテーブルを初期化 */\nhmap := make(map[int]string)\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nhmap[12836] = \"シャオハ\"\nhmap[15937] = \"シャオルオ\"\nhmap[16750] = \"シャオスワン\"\nhmap[13276] = \"シャオファ\"\nhmap[10583] = \"シャオヤー\"\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nname := hmap[15937]\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\ndelete(hmap, 10583)\n
hash_map.swift
/* ハッシュテーブルを初期化 */\nvar map: [Int: String] = [:]\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap[12836] = \"シャオハ\"\nmap[15937] = \"シャオルオ\"\nmap[16750] = \"シャオスワン\"\nmap[13276] = \"シャオファ\"\nmap[10583] = \"シャオヤー\"\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nlet name = map[15937]!\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.removeValue(forKey: 10583)\n
hash_map.js
/* ハッシュテーブルを初期化 */\nconst map = new Map();\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap.set(12836, 'シャオハ');\nmap.set(15937, 'シャオルオ');\nmap.set(16750, 'シャオスワン');\nmap.set(13276, 'シャオファ');\nmap.set(10583, 'シャオヤー');\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nlet name = map.get(15937);\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.delete(10583);\n
hash_map.ts
/* ハッシュテーブルを初期化 */\nconst map = new Map<number, string>();\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap.set(12836, 'シャオハ');\nmap.set(15937, 'シャオルオ');\nmap.set(16750, 'シャオスワン');\nmap.set(13276, 'シャオファ');\nmap.set(10583, 'シャオヤー');\nconsole.info('\\n追加後のハッシュテーブルは次のとおりです\\nKey -> Value');\nconsole.info(map);\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nlet name = map.get(15937);\nconsole.info('\\n学籍番号 15937 を入力し、名前を検索: ' + name);\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.delete(10583);\nconsole.info('\\n10583 を削除した後のハッシュテーブル\\nKey -> Value');\nconsole.info(map);\n
hash_map.dart
/* ハッシュテーブルを初期化 */\nMap<int, String> map = {};\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap[12836] = \"シャオハ\";\nmap[15937] = \"シャオルオ\";\nmap[16750] = \"シャオスワン\";\nmap[13276] = \"シャオファ\";\nmap[10583] = \"シャオヤー\";\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nString name = map[15937];\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.remove(10583);\n
hash_map.rs
use std::collections::HashMap;\n\n/* ハッシュテーブルを初期化 */\nlet mut map: HashMap<i32, String> = HashMap::new();\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap.insert(12836, \"シャオハ\".to_string());\nmap.insert(15937, \"シャオルオ\".to_string());\nmap.insert(16750, \"シャオスワン\".to_string());\nmap.insert(13279, \"シャオファ\".to_string());\nmap.insert(10583, \"シャオヤー\".to_string());\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nlet _name: Option<&String> = map.get(&15937);\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nlet _removed_value: Option<String> = map.remove(&10583);\n
hash_map.c
// C には組み込みのハッシュテーブルはありません\n
hash_map.kt
/* ハッシュテーブルを初期化 */\nval map = HashMap<Int,String>()\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap[12836] = \"シャオハ\"\nmap[15937] = \"シャオルオ\"\nmap[16750] = \"シャオスワン\"\nmap[13276] = \"シャオファ\"\nmap[10583] = \"シャオヤー\"\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nval name = map[15937]\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.remove(10583)\n
hash_map.rb
# ハッシュテーブルを初期化\nhmap = {}\n\n# 追加操作\n# ハッシュテーブルにキーと値のペア (key, value) を追加\nhmap[12836] = \"シャオハ\"\nhmap[15937] = \"シャオルオ\"\nhmap[16750] = \"シャオスワン\"\nhmap[13276] = \"シャオファ\"\nhmap[10583] = \"シャオヤー\"\n\n# 検索操作\n# ハッシュテーブルにキー key を入力し、値 value を取得\nname = hmap[15937]\n\n# 削除操作\n# ハッシュテーブルからキーと値のペア (key, value) を削除\nhmap.delete(10583)\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

ハッシュテーブルには、キーと値のペア、キー、値を走査する 3 つの一般的な方法があります。コード例は以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
# ハッシュテーブルを走査\n# キーと値のペア key->value を走査\nfor key, value in hmap.items():\n    print(key, \"->\", value)\n# キー key のみを走査\nfor key in hmap.keys():\n    print(key)\n# 値 value のみを走査\nfor value in hmap.values():\n    print(value)\n
hash_map.cpp
/* ハッシュテーブルを走査 */\n// キーと値のペア key->value を走査\nfor (auto kv: map) {\n    cout << kv.first << \" -> \" << kv.second << endl;\n}\n// イテレータを使って key->value を走査\nfor (auto iter = map.begin(); iter != map.end(); iter++) {\n    cout << iter->first << \"->\" << iter->second << endl;\n}\n
hash_map.java
/* ハッシュテーブルを走査 */\n// キーと値のペア key->value を走査\nfor (Map.Entry <Integer, String> kv: map.entrySet()) {\n    System.out.println(kv.getKey() + \" -> \" + kv.getValue());\n}\n// キー key のみを走査\nfor (int key: map.keySet()) {\n    System.out.println(key);\n}\n// 値 value のみを走査\nfor (String val: map.values()) {\n    System.out.println(val);\n}\n
hash_map.cs
/* ハッシュテーブルを走査 */\n// キーと値のペア Key->Value を走査\nforeach (var kv in map) {\n    Console.WriteLine(kv.Key + \" -> \" + kv.Value);\n}\n// キー key のみを走査\nforeach (int key in map.Keys) {\n    Console.WriteLine(key);\n}\n// 値 value のみを走査\nforeach (string val in map.Values) {\n    Console.WriteLine(val);\n}\n
hash_map_test.go
/* ハッシュテーブルを走査 */\n// キーと値のペア key->value を走査\nfor key, value := range hmap {\n    fmt.Println(key, \"->\", value)\n}\n// キー key のみを走査\nfor key := range hmap {\n    fmt.Println(key)\n}\n// 値 value のみを走査\nfor _, value := range hmap {\n    fmt.Println(value)\n}\n
hash_map.swift
/* ハッシュテーブルを走査 */\n// キーと値のペア Key->Value を走査\nfor (key, value) in map {\n    print(\"\\(key) -> \\(value)\")\n}\n// キー Key のみを走査\nfor key in map.keys {\n    print(key)\n}\n// 値 Value のみを走査\nfor value in map.values {\n    print(value)\n}\n
hash_map.js
/* ハッシュテーブルを走査 */\nconsole.info('\\nキーと値のペア Key->Value を走査');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nキー Key のみを走査');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\n値 Value のみを走査');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
hash_map.ts
/* ハッシュテーブルを走査 */\nconsole.info('\\nキーと値のペア Key->Value を走査');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nキー Key のみを走査');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\n値 Value のみを走査');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
hash_map.dart
/* ハッシュテーブルを走査 */\n// キーと値のペア Key->Value を走査\nmap.forEach((key, value) {\n  print('$key -> $value');\n});\n\n// キー Key のみを走査\nmap.keys.forEach((key) {\n  print(key);\n});\n\n// 値 Value のみを走査\nmap.values.forEach((value) {\n  print(value);\n});\n
hash_map.rs
/* ハッシュテーブルを走査 */\n// キーと値のペア Key->Value を走査\nfor (key, value) in &map {\n    println!(\"{key} -> {value}\");\n}\n\n// キー Key のみを走査\nfor key in map.keys() {\n    println!(\"{key}\");\n}\n\n// 値 Value のみを走査\nfor value in map.values() {\n    println!(\"{value}\");\n}\n
hash_map.c
// C には組み込みのハッシュテーブルはありません\n
hash_map.kt
/* ハッシュテーブルを走査 */\n// キーと値のペア key->value を走査\nfor ((key, value) in map) {\n    println(\"$key -> $value\")\n}\n// キー key のみを走査\nfor (key in map.keys) {\n    println(key)\n}\n// 値 value のみを走査\nfor (_val in map.values) {\n    println(_val)\n}\n
hash_map.rb
# ハッシュテーブルを走査\n# キーと値のペア key->value を走査\nhmap.entries.each { |key, value| puts \"#{key} -> #{value}\" }\n\n# キー key のみを走査\nhmap.keys.each { |key| puts key }\n\n# 値 value のみを走査\nhmap.values.each { |val| puts val }\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 6 章   ハッシュテーブル","6.1   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/hash_map/#612","level":2,"title":"6.1.2   ハッシュテーブルの簡単な実装","text":"

まずは最も単純なケースとして、**1 つの配列だけでハッシュテーブルを実装する**ことを考えます。ハッシュテーブルでは、配列中の各空き位置をバケット(bucket)と呼び、各バケットには 1 つのキーと値のペアを格納できます。したがって、検索操作とは key に対応するバケットを見つけ、そのバケットから value を取得することです。

では、key に基づいて対応するバケットをどのように特定するのでしょうか。これはハッシュ関数(hash function)によって実現されます。ハッシュ関数の役割は、大きな入力空間をより小さな出力空間に写像することです。ハッシュテーブルでは、入力空間はすべての key 、出力空間はすべてのバケット(配列インデックス)です。言い換えると、key を入力すると、ハッシュ関数によってその key に対応するキーと値のペアの配列内での格納位置を求められます。

key を入力したとき、ハッシュ関数の計算過程は次の 2 段階に分かれます。

  1. あるハッシュアルゴリズム hash() を用いてハッシュ値を計算します。
  2. ハッシュ値をバケット数(配列長)capacity で剰余し、その key に対応するバケット(配列インデックス)index を求めます。
index = hash(key) % capacity\n

その後、index を使ってハッシュテーブル内の対応するバケットにアクセスし、value を取得できます。

配列長を capacity = 100 、ハッシュアルゴリズムを hash(key) = key とすると、ハッシュ関数は key % 100 となります。次の図では、key を学籍番号、value を名前の例として、ハッシュ関数の動作原理を示します。

図 6-2   ハッシュ関数の動作原理

以下のコードは、単純なハッシュテーブルを実装したものです。ここでは、キーと値のペアを表すために keyvalue をクラス Pair にまとめています。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_hash_map.py
class Pair:\n    \"\"\"キーと値の組\"\"\"\n\n    def __init__(self, key: int, val: str):\n        self.key = key\n        self.val = val\n\nclass ArrayHashMap:\n    \"\"\"配列ベースのハッシュテーブル\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        # 100 個のバケットを含む配列を初期化\n        self.buckets: list[Pair | None] = [None] * 100\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"ハッシュ関数\"\"\"\n        index = key % 100\n        return index\n\n    def get(self, key: int) -> str | None:\n        \"\"\"検索操作\"\"\"\n        index: int = self.hash_func(key)\n        pair: Pair = self.buckets[index]\n        if pair is None:\n            return None\n        return pair.val\n\n    def put(self, key: int, val: str):\n        \"\"\"追加と更新の操作\"\"\"\n        pair = Pair(key, val)\n        index: int = self.hash_func(key)\n        self.buckets[index] = pair\n\n    def remove(self, key: int):\n        \"\"\"削除操作\"\"\"\n        index: int = self.hash_func(key)\n        # None に設定し、削除を表す\n        self.buckets[index] = None\n\n    def entry_set(self) -> list[Pair]:\n        \"\"\"すべてのキーと値のペアを取得\"\"\"\n        result: list[Pair] = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair)\n        return result\n\n    def key_set(self) -> list[int]:\n        \"\"\"すべてのキーを取得\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.key)\n        return result\n\n    def value_set(self) -> list[str]:\n        \"\"\"すべての値を取得\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.val)\n        return result\n\n    def print(self):\n        \"\"\"ハッシュテーブルを出力\"\"\"\n        for pair in self.buckets:\n            if pair is not None:\n                print(pair.key, \"->\", pair.val)\n
array_hash_map.cpp
/* キーと値の組 */\nstruct Pair {\n  public:\n    int key;\n    string val;\n    Pair(int key, string val) {\n        this->key = key;\n        this->val = val;\n    }\n};\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n  private:\n    vector<Pair *> buckets;\n\n  public:\n    ArrayHashMap() {\n        // 100 個のバケットを含む配列を初期化\n        buckets = vector<Pair *>(100);\n    }\n\n    ~ArrayHashMap() {\n        // メモリを解放する\n        for (const auto &bucket : buckets) {\n            delete bucket;\n        }\n        buckets.clear();\n    }\n\n    /* ハッシュ関数 */\n    int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* 検索操作 */\n    string get(int key) {\n        int index = hashFunc(key);\n        Pair *pair = buckets[index];\n        if (pair == nullptr)\n            return \"\";\n        return pair->val;\n    }\n\n    /* 追加操作 */\n    void put(int key, string val) {\n        Pair *pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* 削除操作 */\n    void remove(int key) {\n        int index = hashFunc(key);\n        // メモリを解放して nullptr に設定する\n        delete buckets[index];\n        buckets[index] = nullptr;\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    vector<Pair *> pairSet() {\n        vector<Pair *> pairSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                pairSet.push_back(pair);\n            }\n        }\n        return pairSet;\n    }\n\n    /* すべてのキーを取得 */\n    vector<int> keySet() {\n        vector<int> keySet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                keySet.push_back(pair->key);\n            }\n        }\n        return keySet;\n    }\n\n    /* すべての値を取得 */\n    vector<string> valueSet() {\n        vector<string> valueSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                valueSet.push_back(pair->val);\n            }\n        }\n        return valueSet;\n    }\n\n    /* ハッシュテーブルを出力 */\n    void print() {\n        for (Pair *kv : pairSet()) {\n            cout << kv->key << \" -> \" << kv->val << endl;\n        }\n    }\n};\n
array_hash_map.java
/* キーと値の組 */\nclass Pair {\n    public int key;\n    public String val;\n\n    public Pair(int key, String val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    private List<Pair> buckets;\n\n    public ArrayHashMap() {\n        // 100 個のバケットを含む配列を初期化\n        buckets = new ArrayList<>();\n        for (int i = 0; i < 100; i++) {\n            buckets.add(null);\n        }\n    }\n\n    /* ハッシュ関数 */\n    private int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* 検索操作 */\n    public String get(int key) {\n        int index = hashFunc(key);\n        Pair pair = buckets.get(index);\n        if (pair == null)\n            return null;\n        return pair.val;\n    }\n\n    /* 追加操作 */\n    public void put(int key, String val) {\n        Pair pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets.set(index, pair);\n    }\n\n    /* 削除操作 */\n    public void remove(int key) {\n        int index = hashFunc(key);\n        // null に設定し、削除を表す\n        buckets.set(index, null);\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    public List<Pair> pairSet() {\n        List<Pair> pairSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                pairSet.add(pair);\n        }\n        return pairSet;\n    }\n\n    /* すべてのキーを取得 */\n    public List<Integer> keySet() {\n        List<Integer> keySet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                keySet.add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* すべての値を取得 */\n    public List<String> valueSet() {\n        List<String> valueSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                valueSet.add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* ハッシュテーブルを出力 */\n    public void print() {\n        for (Pair kv : pairSet()) {\n            System.out.println(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
array_hash_map.cs
/* キーと値の組 int->string */\nclass Pair(int key, string val) {\n    public int key = key;\n    public string val = val;\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    List<Pair?> buckets;\n    public ArrayHashMap() {\n        // 100 個のバケットを含む配列を初期化\n        buckets = [];\n        for (int i = 0; i < 100; i++) {\n            buckets.Add(null);\n        }\n    }\n\n    /* ハッシュ関数 */\n    int HashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* 検索操作 */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        Pair? pair = buckets[index];\n        if (pair == null) return null;\n        return pair.val;\n    }\n\n    /* 追加操作 */\n    public void Put(int key, string val) {\n        Pair pair = new(key, val);\n        int index = HashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* 削除操作 */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // null に設定し、削除を表す\n        buckets[index] = null;\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    public List<Pair> PairSet() {\n        List<Pair> pairSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                pairSet.Add(pair);\n        }\n        return pairSet;\n    }\n\n    /* すべてのキーを取得 */\n    public List<int> KeySet() {\n        List<int> keySet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                keySet.Add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* すべての値を取得 */\n    public List<string> ValueSet() {\n        List<string> valueSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                valueSet.Add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* ハッシュテーブルを出力 */\n    public void Print() {\n        foreach (Pair kv in PairSet()) {\n            Console.WriteLine(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
array_hash_map.go
/* キーと値の組 */\ntype pair struct {\n    key int\n    val string\n}\n\n/* 配列ベースのハッシュテーブル */\ntype arrayHashMap struct {\n    buckets []*pair\n}\n\n/* ハッシュテーブルを初期化 */\nfunc newArrayHashMap() *arrayHashMap {\n    // 100 個のバケットを含む配列を初期化\n    buckets := make([]*pair, 100)\n    return &arrayHashMap{buckets: buckets}\n}\n\n/* ハッシュ関数 */\nfunc (a *arrayHashMap) hashFunc(key int) int {\n    index := key % 100\n    return index\n}\n\n/* 検索操作 */\nfunc (a *arrayHashMap) get(key int) string {\n    index := a.hashFunc(key)\n    pair := a.buckets[index]\n    if pair == nil {\n        return \"Not Found\"\n    }\n    return pair.val\n}\n\n/* 追加操作 */\nfunc (a *arrayHashMap) put(key int, val string) {\n    pair := &pair{key: key, val: val}\n    index := a.hashFunc(key)\n    a.buckets[index] = pair\n}\n\n/* 削除操作 */\nfunc (a *arrayHashMap) remove(key int) {\n    index := a.hashFunc(key)\n    // nil に設定し、削除を表す\n    a.buckets[index] = nil\n}\n\n/* すべてのキーのペアを取得する */\nfunc (a *arrayHashMap) pairSet() []*pair {\n    var pairs []*pair\n    for _, pair := range a.buckets {\n        if pair != nil {\n            pairs = append(pairs, pair)\n        }\n    }\n    return pairs\n}\n\n/* すべてのキーを取得 */\nfunc (a *arrayHashMap) keySet() []int {\n    var keys []int\n    for _, pair := range a.buckets {\n        if pair != nil {\n            keys = append(keys, pair.key)\n        }\n    }\n    return keys\n}\n\n/* すべての値を取得 */\nfunc (a *arrayHashMap) valueSet() []string {\n    var values []string\n    for _, pair := range a.buckets {\n        if pair != nil {\n            values = append(values, pair.val)\n        }\n    }\n    return values\n}\n\n/* ハッシュテーブルを出力 */\nfunc (a *arrayHashMap) print() {\n    for _, pair := range a.buckets {\n        if pair != nil {\n            fmt.Println(pair.key, \"->\", pair.val)\n        }\n    }\n}\n
array_hash_map.swift
/* キーと値の組 */\nclass Pair: Equatable {\n    public var key: Int\n    public var val: String\n\n    public init(key: Int, val: String) {\n        self.key = key\n        self.val = val\n    }\n\n    public static func == (lhs: Pair, rhs: Pair) -> Bool {\n        lhs.key == rhs.key && lhs.val == rhs.val\n    }\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    private var buckets: [Pair?]\n\n    init() {\n        // 100 個のバケットを含む配列を初期化\n        buckets = Array(repeating: nil, count: 100)\n    }\n\n    /* ハッシュ関数 */\n    private func hashFunc(key: Int) -> Int {\n        let index = key % 100\n        return index\n    }\n\n    /* 検索操作 */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let pair = buckets[index]\n        return pair?.val\n    }\n\n    /* 追加操作 */\n    func put(key: Int, val: String) {\n        let pair = Pair(key: key, val: val)\n        let index = hashFunc(key: key)\n        buckets[index] = pair\n    }\n\n    /* 削除操作 */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        // nil に設定し、削除を表す\n        buckets[index] = nil\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    func pairSet() -> [Pair] {\n        buckets.compactMap { $0 }\n    }\n\n    /* すべてのキーを取得 */\n    func keySet() -> [Int] {\n        buckets.compactMap { $0?.key }\n    }\n\n    /* すべての値を取得 */\n    func valueSet() -> [String] {\n        buckets.compactMap { $0?.val }\n    }\n\n    /* ハッシュテーブルを出力 */\n    func print() {\n        for pair in pairSet() {\n            Swift.print(\"\\(pair.key) -> \\(pair.val)\")\n        }\n    }\n}\n
array_hash_map.js
/* キーと値の組 Number -> String */\nclass Pair {\n    constructor(key, val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    #buckets;\n    constructor() {\n        // 100 個のバケットを含む配列を初期化\n        this.#buckets = new Array(100).fill(null);\n    }\n\n    /* ハッシュ関数 */\n    #hashFunc(key) {\n        return key % 100;\n    }\n\n    /* 検索操作 */\n    get(key) {\n        let index = this.#hashFunc(key);\n        let pair = this.#buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* 追加操作 */\n    set(key, val) {\n        let index = this.#hashFunc(key);\n        this.#buckets[index] = new Pair(key, val);\n    }\n\n    /* 削除操作 */\n    delete(key) {\n        let index = this.#hashFunc(key);\n        // null に設定し、削除を表す\n        this.#buckets[index] = null;\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    entries() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* すべてのキーを取得 */\n    keys() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* すべての値を取得 */\n    values() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* ハッシュテーブルを出力 */\n    print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
array_hash_map.ts
/* キーと値の組 Number -> String */\nclass Pair {\n    public key: number;\n    public val: string;\n\n    constructor(key: number, val: string) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    private readonly buckets: (Pair | null)[];\n\n    constructor() {\n        // 100 個のバケットを含む配列を初期化\n        this.buckets = new Array(100).fill(null);\n    }\n\n    /* ハッシュ関数 */\n    private hashFunc(key: number): number {\n        return key % 100;\n    }\n\n    /* 検索操作 */\n    public get(key: number): string | null {\n        let index = this.hashFunc(key);\n        let pair = this.buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* 追加操作 */\n    public set(key: number, val: string) {\n        let index = this.hashFunc(key);\n        this.buckets[index] = new Pair(key, val);\n    }\n\n    /* 削除操作 */\n    public delete(key: number) {\n        let index = this.hashFunc(key);\n        // null に設定し、削除を表す\n        this.buckets[index] = null;\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    public entries(): (Pair | null)[] {\n        let arr: (Pair | null)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* すべてのキーを取得 */\n    public keys(): (number | undefined)[] {\n        let arr: (number | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* すべての値を取得 */\n    public values(): (string | undefined)[] {\n        let arr: (string | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* ハッシュテーブルを出力 */\n    public print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
array_hash_map.dart
/* キーと値の組 */\nclass Pair {\n  int key;\n  String val;\n  Pair(this.key, this.val);\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n  late List<Pair?> _buckets;\n\n  ArrayHashMap() {\n    // 100 個のバケットを含む配列を初期化\n    _buckets = List.filled(100, null);\n  }\n\n  /* ハッシュ関数 */\n  int _hashFunc(int key) {\n    final int index = key % 100;\n    return index;\n  }\n\n  /* 検索操作 */\n  String? get(int key) {\n    final int index = _hashFunc(key);\n    final Pair? pair = _buckets[index];\n    if (pair == null) {\n      return null;\n    }\n    return pair.val;\n  }\n\n  /* 追加操作 */\n  void put(int key, String val) {\n    final Pair pair = Pair(key, val);\n    final int index = _hashFunc(key);\n    _buckets[index] = pair;\n  }\n\n  /* 削除操作 */\n  void remove(int key) {\n    final int index = _hashFunc(key);\n    _buckets[index] = null;\n  }\n\n  /* すべてのキーと値のペアを取得 */\n  List<Pair> pairSet() {\n    List<Pair> pairSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        pairSet.add(pair);\n      }\n    }\n    return pairSet;\n  }\n\n  /* すべてのキーを取得 */\n  List<int> keySet() {\n    List<int> keySet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        keySet.add(pair.key);\n      }\n    }\n    return keySet;\n  }\n\n  /* すべての値を取得 */\n  List<String> values() {\n    List<String> valueSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        valueSet.add(pair.val);\n      }\n    }\n    return valueSet;\n  }\n\n  /* ハッシュテーブルを出力 */\n  void printHashMap() {\n    for (final Pair kv in pairSet()) {\n      print(\"${kv.key} -> ${kv.val}\");\n    }\n  }\n}\n
array_hash_map.rs
/* キーと値の組 */\n#[derive(Debug, Clone, PartialEq)]\npub struct Pair {\n    pub key: i32,\n    pub val: String,\n}\n\n/* 配列ベースのハッシュテーブル */\npub struct ArrayHashMap {\n    buckets: Vec<Option<Pair>>,\n}\n\nimpl ArrayHashMap {\n    pub fn new() -> ArrayHashMap {\n        // 100 個のバケットを含む配列を初期化\n        Self {\n            buckets: vec![None; 100],\n        }\n    }\n\n    /* ハッシュ関数 */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % 100\n    }\n\n    /* 検索操作 */\n    pub fn get(&self, key: i32) -> Option<&String> {\n        let index = self.hash_func(key);\n        self.buckets[index].as_ref().map(|pair| &pair.val)\n    }\n\n    /* 追加操作 */\n    pub fn put(&mut self, key: i32, val: &str) {\n        let index = self.hash_func(key);\n        self.buckets[index] = Some(Pair {\n            key,\n            val: val.to_string(),\n        });\n    }\n\n    /* 削除操作 */\n    pub fn remove(&mut self, key: i32) {\n        let index = self.hash_func(key);\n        // None に設定し、削除を表す\n        self.buckets[index] = None;\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    pub fn entry_set(&self) -> Vec<&Pair> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref())\n            .collect()\n    }\n\n    /* すべてのキーを取得 */\n    pub fn key_set(&self) -> Vec<&i32> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.key))\n            .collect()\n    }\n\n    /* すべての値を取得 */\n    pub fn value_set(&self) -> Vec<&String> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.val))\n            .collect()\n    }\n\n    /* ハッシュテーブルを出力 */\n    pub fn print(&self) {\n        for pair in self.entry_set() {\n            println!(\"{} -> {}\", pair.key, pair.val);\n        }\n    }\n}\n
array_hash_map.c
/* キーと値の組 int->string */\ntypedef struct {\n    int key;\n    char *val;\n} Pair;\n\n/* 配列ベースのハッシュテーブル */\ntypedef struct {\n    Pair *buckets[MAX_SIZE];\n} ArrayHashMap;\n\n/* コンストラクタ */\nArrayHashMap *newArrayHashMap() {\n    ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap));\n    for (int i=0; i < MAX_SIZE; i++) {\n        hmap->buckets[i] = NULL;\n    }\n    return hmap;\n}\n\n/* デストラクタ */\nvoid delArrayHashMap(ArrayHashMap *hmap) {\n    for (int i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            free(hmap->buckets[i]->val);\n            free(hmap->buckets[i]);\n        }\n    }\n    free(hmap);\n}\n\n/* 追加操作 */\nvoid put(ArrayHashMap *hmap, const int key, const char *val) {\n    Pair *Pair = malloc(sizeof(Pair));\n    Pair->key = key;\n    Pair->val = malloc(strlen(val) + 1);\n    strcpy(Pair->val, val);\n\n    int index = hashFunc(key);\n    hmap->buckets[index] = Pair;\n}\n\n/* 削除操作 */\nvoid removeItem(ArrayHashMap *hmap, const int key) {\n    int index = hashFunc(key);\n    free(hmap->buckets[index]->val);\n    free(hmap->buckets[index]);\n    hmap->buckets[index] = NULL;\n}\n\n/* すべてのキーと値のペアを取得 */\nvoid pairSet(ArrayHashMap *hmap, MapSet *set) {\n    Pair *entries;\n    int i = 0, index = 0;\n    int total = 0;\n    /* 有効なキーと値のペア数を集計 */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    entries = malloc(sizeof(Pair) * total);\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            entries[index].key = hmap->buckets[i]->key;\n            entries[index].val = malloc(strlen(hmap->buckets[i]->val) + 1);\n            strcpy(entries[index].val, hmap->buckets[i]->val);\n            index++;\n        }\n    }\n    set->set = entries;\n    set->len = total;\n}\n\n/* すべてのキーを取得 */\nvoid keySet(ArrayHashMap *hmap, MapSet *set) {\n    int *keys;\n    int i = 0, index = 0;\n    int total = 0;\n    /* 有効なキーと値のペア数を集計 */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    keys = malloc(total * sizeof(int));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            keys[index] = hmap->buckets[i]->key;\n            index++;\n        }\n    }\n    set->set = keys;\n    set->len = total;\n}\n\n/* すべての値を取得 */\nvoid valueSet(ArrayHashMap *hmap, MapSet *set) {\n    char **vals;\n    int i = 0, index = 0;\n    int total = 0;\n    /* 有効なキーと値のペア数を集計 */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    vals = malloc(total * sizeof(char *));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            vals[index] = hmap->buckets[i]->val;\n            index++;\n        }\n    }\n    set->set = vals;\n    set->len = total;\n}\n\n/* ハッシュテーブルを出力 */\nvoid print(ArrayHashMap *hmap) {\n    int i;\n    MapSet set;\n    pairSet(hmap, &set);\n    Pair *entries = (Pair *)set.set;\n    for (i = 0; i < set.len; i++) {\n        printf(\"%d -> %s\\n\", entries[i].key, entries[i].val);\n    }\n    free(set.set);\n}\n
array_hash_map.kt
/* キーと値の組 */\nclass Pair(\n    var key: Int,\n    var _val: String\n)\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    // 100 個のバケットを含む配列を初期化\n    private val buckets = arrayOfNulls<Pair>(100)\n\n    /* ハッシュ関数 */\n    fun hashFunc(key: Int): Int {\n        val index = key % 100\n        return index\n    }\n\n    /* 検索操作 */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val pair = buckets[index] ?: return null\n        return pair._val\n    }\n\n    /* 追加操作 */\n    fun put(key: Int, _val: String) {\n        val pair = Pair(key, _val)\n        val index = hashFunc(key)\n        buckets[index] = pair\n    }\n\n    /* 削除操作 */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        // null に設定し、削除を表す\n        buckets[index] = null\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    fun pairSet(): MutableList<Pair> {\n        val pairSet = mutableListOf<Pair>()\n        for (pair in buckets) {\n            if (pair != null)\n                pairSet.add(pair)\n        }\n        return pairSet\n    }\n\n    /* すべてのキーを取得 */\n    fun keySet(): MutableList<Int> {\n        val keySet = mutableListOf<Int>()\n        for (pair in buckets) {\n            if (pair != null)\n                keySet.add(pair.key)\n        }\n        return keySet\n    }\n\n    /* すべての値を取得 */\n    fun valueSet(): MutableList<String> {\n        val valueSet = mutableListOf<String>()\n        for (pair in buckets) {\n            if (pair != null)\n                valueSet.add(pair._val)\n        }\n        return valueSet\n    }\n\n    /* ハッシュテーブルを出力 */\n    fun print() {\n        for (kv in pairSet()) {\n            val key = kv.key\n            val _val = kv._val\n            println(\"$key -> $_val\")\n        }\n    }\n}\n
array_hash_map.rb
### キーと値のペア ###\nclass Pair\n  attr_accessor :key, :val\n\n  def initialize(key, val)\n    @key = key\n    @val = val\n  end\nend\n\n### 配列で実装したハッシュテーブル ###\nclass ArrayHashMap\n  ### コンストラクタ ###\n  def initialize\n    # 100 個のバケットを含む配列を初期化\n    @buckets = Array.new(100)\n  end\n\n  ### ハッシュ関数 ###\n  def hash_func(key)\n    index = key % 100\n  end\n\n  ### 検索操作 ###\n  def get(key)\n    index = hash_func(key)\n    pair = @buckets[index]\n\n    return if pair.nil?\n    pair.val\n  end\n\n  ### 追加操作 ###\n  def put(key, val)\n    pair = Pair.new(key, val)\n    index = hash_func(key)\n    @buckets[index] = pair\n  end\n\n  ### 削除操作 ###\n  def remove(key)\n    index = hash_func(key)\n    # nil に設定し、削除を表す\n    @buckets[index] = nil\n  end\n\n  ### すべてのキーと値のペアを取得 ###\n  def entry_set\n    result = []\n    @buckets.each { |pair| result << pair unless pair.nil? }\n    result\n  end\n\n  ### すべてのキーを取得 ###\n  def key_set\n    result = []\n    @buckets.each { |pair| result << pair.key unless pair.nil? }\n    result\n  end\n\n  ### すべての値を取得 ###\n  def value_set\n    result = []\n    @buckets.each { |pair| result << pair.val unless pair.nil? }\n    result\n  end\n\n  ### ハッシュテーブルを出力 ###\n  def print\n    @buckets.each { |pair| puts \"#{pair.key} -> #{pair.val}\" unless pair.nil? }\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 6 章   ハッシュテーブル","6.1   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/hash_map/#613","level":2,"title":"6.1.3   ハッシュ衝突と拡張","text":"

本質的には、ハッシュ関数の役割は、すべての key からなる入力空間を、配列のすべてのインデックスからなる出力空間に写像することです。しかし、入力空間は多くの場合、出力空間よりはるかに大きいため、理論上は必ず「複数の入力が同じ出力に対応する」状況が存在します。

上の例のハッシュ関数では、入力 key の下 2 桁が同じであれば、出力結果も同じになります。たとえば、学籍番号 12836 と 20336 の 2 人の学生を検索すると、次の結果を得ます:

12836 % 100 = 36\n20336 % 100 = 36\n

次の図に示すように、2 つの学籍番号が同じ名前を指してしまっており、これは明らかに誤りです。このような、複数の入力が同じ出力に対応する状況をハッシュ衝突(hash collision)と呼びます。

図 6-3   ハッシュ衝突の例

容易に分かるように、ハッシュテーブルの容量 \\(n\\) が大きいほど、複数の key が同じバケットに割り当てられる確率は低くなり、衝突も少なくなります。したがって、ハッシュテーブルを拡張することでハッシュ衝突を減らせます。

次の図に示すように、拡張前はキーと値のペア (136, A)(236, D) が衝突していますが、拡張後は衝突が解消されます。

図 6-4   ハッシュテーブルの拡張

配列の拡張と同様に、ハッシュテーブルの拡張ではすべてのキーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移し替える必要があり、非常に時間がかかります。また、ハッシュテーブルの容量 capacity が変わるため、ハッシュ関数を使ってすべてのキーと値のペアの格納位置を再計算しなければならず、これによって拡張過程の計算コストがさらに増加します。そのため、プログラミング言語では通常、頻繁な拡張を防ぐために十分大きなハッシュテーブル容量をあらかじめ確保します。

負荷率(load factor)はハッシュテーブルにおける重要な概念であり、ハッシュテーブル内の要素数をバケット数で割ったものとして定義され、ハッシュ衝突の深刻さを測るために用いられます。また、ハッシュテーブル拡張の発動条件としてもよく使われます。例えば Java では、負荷率が \\(0.75\\) を超えると、システムはハッシュテーブルを元の \\(2\\) 倍に拡張します。

","path":["第 6 章   ハッシュテーブル","6.1   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/summary/","level":1,"title":"6.4   まとめ","text":"","path":["第 6 章   ハッシュテーブル","6.4   まとめ"],"tags":[]},{"location":"chapter_hashing/summary/#1","level":3,"title":"1.   重要ポイントの振り返り","text":"
  • key を入力すると、ハッシュテーブルは \\(O(1)\\) 時間で value を検索でき、非常に高効率である。
  • 一般的なハッシュテーブルの操作には、検索、キーと値のペアの追加、キーと値のペアの削除、ハッシュテーブルの走査などがある。
  • ハッシュ関数は key を配列インデックスに写像し、それによって対応するバケットにアクセスして value を取得する。
  • 異なる 2 つの key が、ハッシュ関数を通した後に同じ配列インデックスになることがあり、検索結果の誤りを引き起こす。この現象をハッシュ衝突と呼ぶ。
  • ハッシュテーブルの容量が大きいほど、ハッシュ衝突の確率は低くなる。そのため、ハッシュテーブルを拡張することでハッシュ衝突を緩和できる。配列の拡張と同様に、ハッシュテーブルの拡張操作のコストは大きい。
  • 負荷率は、ハッシュテーブル内の要素数をバケット数で割ったものと定義され、ハッシュ衝突の深刻さを反映する。ハッシュテーブル拡張を発動する条件としてよく用いられる。
  • 連鎖方式では、単一要素を連結リストに変換し、衝突したすべての要素を同じ連結リストに格納する。しかし、連結リストが長すぎると検索効率が低下するため、さらに連結リストを赤黒木に変換して効率を高めることができる。
  • オープンアドレス法は複数回の探索によってハッシュ衝突を処理する。線形探索は固定のステップ幅を用いるが、要素を削除できず、クラスタリングが発生しやすいという欠点がある。二重ハッシュは複数のハッシュ関数を用いて探索するため、線形探索に比べてクラスタリングが起きにくいが、複数のハッシュ関数によって計算量が増える。
  • プログラミング言語ごとに、異なるハッシュテーブル実装が採用されている。たとえば、Java の HashMap は連鎖方式を使用し、Python の Dict はオープンアドレス法を採用している。
  • ハッシュテーブルでは、ハッシュアルゴリズムに決定性、高効率、均一分布という特徴が求められる。暗号学では、ハッシュアルゴリズムはさらに耐衝突性とアバランシェ効果も備えるべきである。
  • ハッシュアルゴリズムは通常、大きな素数を法として用い、ハッシュ値の均一分布を最大限に保証してハッシュ衝突を減らす。
  • 一般的なハッシュアルゴリズムには MD5、SHA-1、SHA-2、SHA-3 などがある。MD5 はファイル完全性の検証によく用いられ、SHA-2 はセキュリティ用途やプロトコルでよく用いられる。
  • プログラミング言語は通常、データ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュテーブル内のバケットインデックスの計算に用いる。通常、ハッシュ可能なのは不変オブジェクトだけである。
","path":["第 6 章   ハッシュテーブル","6.4   まとめ"],"tags":[]},{"location":"chapter_hashing/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:ハッシュテーブルの時間計算量が \\(O(n)\\) になるのはどのような場合ですか?

ハッシュ衝突が深刻な場合、ハッシュテーブルの時間計算量は \\(O(n)\\) に劣化する。ハッシュ関数の設計が適切で、容量設定が合理的で、衝突が比較的均等な場合、時間計算量は \\(O(1)\\) である。プログラミング言語組み込みのハッシュテーブルを使うとき、通常は時間計算量を \\(O(1)\\) とみなす。

Q:なぜハッシュ関数 \\(f(x) = x\\) を使わないのですか? そうすれば衝突は起きません。

\\(f(x) = x\\) というハッシュ関数では、各要素は一意のバケットインデックスに対応し、これは配列と等価である。しかし、入力空間は通常、出力空間(配列長)よりはるかに大きいため、ハッシュ関数の最後のステップはたいてい配列長での剰余になる。言い換えると、ハッシュテーブルの目的は、大きな状態空間をより小さな空間に写像し、\\(O(1)\\) の検索効率を提供することである。

Q:ハッシュテーブルの基礎実装は配列、連結リスト、二分木なのに、なぜそれらより高効率になり得るのですか?

まず、ハッシュテーブルは時間効率が高くなる一方で、空間効率は低くなる。ハッシュテーブルには、かなりの部分で未使用のメモリが存在する。

次に、時間効率が高くなるのは特定の利用場面に限られる。ある機能が同じ時間計算量で配列や連結リストによって実装できるなら、通常はハッシュテーブルより速い。これは、ハッシュ関数の計算にコストがかかり、時間計算量の定数項がより大きいからである。

最後に、ハッシュテーブルの時間計算量は劣化する可能性がある。たとえば連鎖方式では、連結リストや赤黒木で検索操作を行うため、なお \\(O(n)\\) 時間に劣化するリスクがある。

Q:二重ハッシュにも要素を直接削除できない欠点がありますか? 削除済みとマークした領域は再利用できますか?

二重ハッシュはオープンアドレス法の一種であり、オープンアドレス法はいずれも要素を直接削除できないという欠点があるため、削除のマーク付けが必要になる。削除済みとマークされた領域は再利用できる。新しい要素をハッシュテーブルに挿入し、ハッシュ関数によって削除済みとマークされた位置を見つけた場合、その位置は新しい要素に使用できる。こうすることで、ハッシュテーブルの探索系列を変えずに保ちつつ、空間利用率も確保できる。

Q:なぜ線形探索では、要素を探すときにハッシュ衝突が発生するのですか?

探索時には、ハッシュ関数で対応するバケットとキーと値のペアを見つけ、key が一致しないことが分かると、それはハッシュ衝突を意味する。そのため、線形探索法では事前に設定したステップ幅に従って順に探索し、正しいキーと値のペアを見つけるか、見つからずに終了するまで続ける。

Q:なぜハッシュテーブルの拡張でハッシュ衝突を緩和できるのですか?

ハッシュ関数の最後のステップは、たいてい配列長 \\(n\\) での剰余を取り、出力値を配列インデックスの範囲内に収めることである。拡張後は配列長 \\(n\\) が変化し、key に対応するインデックスも変化する可能性がある。もともと同じバケットに入っていた複数の key は、拡張後には複数のバケットに割り当てられる可能性があり、それによってハッシュ衝突が緩和される。

Q:高効率な読み書きのためなら、配列を直接使えばよいのではないですか?

データの key が連続した小範囲の整数であれば、配列を直接使えばよく、単純で高効率である。しかし key が別の型(たとえば文字列)の場合は、ハッシュ関数を用いて key を配列インデックスに写像し、さらにバケット配列を通じて要素を格納する必要がある。このような構造がハッシュテーブルである。

","path":["第 6 章   ハッシュテーブル","6.4   まとめ"],"tags":[]},{"location":"chapter_heap/","level":1,"title":"第 8 章   ヒープ","text":"

Abstract

ヒープは連なる山々の峰のように、幾重にも重なり、さまざまな形をしている。

いくつもの山の高さはまちまちだが、最も高い峰がいつも最初に目に入る。

","path":["第 8 章   ヒープ"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"章の内容","text":"
  • 8.1   ヒープ
  • 8.2   ヒープ構築
  • 8.3   Top-k 問題
  • 8.4   まとめ
","path":["第 8 章   ヒープ"],"tags":[]},{"location":"chapter_heap/build_heap/","level":1,"title":"8.2   ヒープ構築","text":"

場合によっては、リスト内のすべての要素を使ってヒープを構築したいことがあります。この過程を「ヒープ構築」と呼びます。

","path":["第 8 章   ヒープ","8.2   ヒープ構築"],"tags":[]},{"location":"chapter_heap/build_heap/#821","level":2,"title":"8.2.1   ヒープへの挿入操作による実現","text":"

まず空のヒープを作成し、次にリストを走査して、各要素に対して順に「ヒープへの挿入操作」を実行します。つまり、要素をヒープの末尾に追加してから、その要素に対して「下から上へ」のヒープ化を行います。

要素が1つヒープに挿入されるたびに、ヒープの長さは1増加します。ノードは上から下へ順に二分木へ追加されるため、ヒープは「上から下へ」構築されます。

要素数を \\(n\\) とすると、各要素のヒープへの挿入操作には \\(O(\\log{n})\\) の時間がかかるため、このヒープ構築法の時間計算量は \\(O(n \\log n)\\) です。

","path":["第 8 章   ヒープ","8.2   ヒープ構築"],"tags":[]},{"location":"chapter_heap/build_heap/#822","level":2,"title":"8.2.2   走査によるヒープ化で実現","text":"

実際には、より効率的なヒープ構築法を実現でき、全体は2つの手順に分かれます。

  1. リストのすべての要素をそのままヒープに追加します。この時点では、ヒープの性質はまだ満たされていません。
  2. ヒープを逆順で走査し(レベル順走査の逆順)、各非葉ノードに対して順に「上から下へ」のヒープ化を実行します。

あるノードをヒープ化するたびに、そのノードを根とする部分木は合法な部分ヒープになります。また、逆順で走査するため、ヒープは「下から上へ」構築されます。

逆順走査を選ぶのは、この方法なら現在のノードの下にある部分木がすでに合法な部分ヒープであることを保証でき、そのうえで現在のノードをヒープ化してはじめて有効になるからです。

なお、葉ノードには子ノードがないため、それ自体が自然に合法な部分ヒープであり、ヒープ化は不要です。以下のコードが示すように、最後の非葉ノードは最後のノードの親ノードであり、そこから逆順に走査してヒープ化を実行します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def __init__(self, nums: list[int]):\n    \"\"\"コンストラクタ。入力リストに基づいてヒープを構築する\"\"\"\n    # リスト要素をそのままヒープに追加\n    self.max_heap = nums\n    # 葉ノード以外のすべてのノードをヒープ化\n    for i in range(self.parent(self.size() - 1), -1, -1):\n        self.sift_down(i)\n
my_heap.cpp
/* コンストラクタ。入力リストに基づいてヒープを構築する */\nMaxHeap(vector<int> nums) {\n    // リスト要素をそのままヒープに追加\n    maxHeap = nums;\n    // 葉ノード以外のすべてのノードをヒープ化\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
my_heap.java
/* コンストラクタ。入力リストに基づいてヒープを構築する */\nMaxHeap(List<Integer> nums) {\n    // リスト要素をそのままヒープに追加\n    maxHeap = new ArrayList<>(nums);\n    // 葉ノード以外のすべてのノードをヒープ化\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
my_heap.cs
/* コンストラクタ。入力リストに基づいてヒープを構築 */\nMaxHeap(IEnumerable<int> nums) {\n    // リスト要素をそのままヒープに追加\n    maxHeap = new List<int>(nums);\n    // 葉ノード以外のすべてのノードをヒープ化\n    var size = Parent(this.Size() - 1);\n    for (int i = size; i >= 0; i--) {\n        SiftDown(i);\n    }\n}\n
my_heap.go
/* コンストラクタ。スライスからヒープを構築する */\nfunc newMaxHeap(nums []any) *maxHeap {\n    // リスト要素をそのままヒープに追加\n    h := &maxHeap{data: nums}\n    for i := h.parent(len(h.data) - 1); i >= 0; i-- {\n        // 葉ノード以外のすべてのノードをヒープ化\n        h.siftDown(i)\n    }\n    return h\n}\n
my_heap.swift
/* コンストラクタ。入力リストに基づいてヒープを構築する */\ninit(nums: [Int]) {\n    // リスト要素をそのままヒープに追加\n    maxHeap = nums\n    // 葉ノード以外のすべてのノードをヒープ化\n    for i in (0 ... parent(i: size() - 1)).reversed() {\n        siftDown(i: i)\n    }\n}\n
my_heap.js
/* コンストラクタ。空のヒープを作成するか、入力リストからヒープを構築する */\nconstructor(nums) {\n    // リスト要素をそのままヒープに追加\n    this.#maxHeap = nums === undefined ? [] : [...nums];\n    // 葉ノード以外のすべてのノードをヒープ化\n    for (let i = this.#parent(this.size() - 1); i >= 0; i--) {\n        this.#siftDown(i);\n    }\n}\n
my_heap.ts
/* コンストラクタ。空のヒープを作成するか、入力リストからヒープを構築する */\nconstructor(nums?: number[]) {\n    // リスト要素をそのままヒープに追加\n    this.maxHeap = nums === undefined ? [] : [...nums];\n    // 葉ノード以外のすべてのノードをヒープ化\n    for (let i = this.parent(this.size() - 1); i >= 0; i--) {\n        this.siftDown(i);\n    }\n}\n
my_heap.dart
/* コンストラクタ。入力リストに基づいてヒープを構築する */\nMaxHeap(List<int> nums) {\n  // リスト要素をそのままヒープに追加\n  _maxHeap = nums;\n  // 葉ノード以外のすべてのノードをヒープ化\n  for (int i = _parent(size() - 1); i >= 0; i--) {\n    siftDown(i);\n  }\n}\n
my_heap.rs
/* コンストラクタ。入力リストに基づいてヒープを構築する */\nfn new(nums: Vec<i32>) -> Self {\n    // リスト要素をそのままヒープに追加\n    let mut heap = MaxHeap { max_heap: nums };\n    // 葉ノード以外のすべてのノードをヒープ化\n    for i in (0..=Self::parent(heap.size() - 1)).rev() {\n        heap.sift_down(i);\n    }\n    heap\n}\n
my_heap.c
/* コンストラクタ。スライスからヒープを構築する */\nMaxHeap *newMaxHeap(int nums[], int size) {\n    // すべての要素をヒープに入れる\n    MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap));\n    maxHeap->size = size;\n    memcpy(maxHeap->data, nums, size * sizeof(int));\n    for (int i = parent(maxHeap, size - 1); i >= 0; i--) {\n        // 葉ノード以外のすべてのノードをヒープ化\n        siftDown(maxHeap, i);\n    }\n    return maxHeap;\n}\n
my_heap.kt
/* 最大ヒープ */\nclass MaxHeap(nums: MutableList<Int>?) {\n    // 配列ではなくリストを使うことで、拡張を考慮する必要がない\n    private val maxHeap = mutableListOf<Int>()\n\n    /* コンストラクタ。入力リストに基づいてヒープを構築する */\n    init {\n        // リスト要素をそのままヒープに追加\n        maxHeap.addAll(nums!!)\n        // 葉ノード以外のすべてのノードをヒープ化\n        for (i in parent(size() - 1) downTo 0) {\n            siftDown(i)\n        }\n    }\n\n    /* 左子ノードのインデックスを取得 */\n    private fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* 右子ノードのインデックスを取得 */\n    private fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* 親ノードのインデックスを取得 */\n    private fun parent(i: Int): Int {\n        return (i - 1) / 2 // 切り捨て除算\n    }\n\n    /* 要素を交換 */\n    private fun swap(i: Int, j: Int) {\n        val temp = maxHeap[i]\n        maxHeap[i] = maxHeap[j]\n        maxHeap[j] = temp\n    }\n\n    /* ヒープのサイズを取得 */\n    fun size(): Int {\n        return maxHeap.size\n    }\n\n    /* ヒープが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        /* ヒープが空かどうかを判定 */\n        return size() == 0\n    }\n\n    /* ヒープ先頭要素にアクセス */\n    fun peek(): Int {\n        return maxHeap[0]\n    }\n\n    /* 要素をヒープに追加 */\n    fun push(_val: Int) {\n        // ノードを追加\n        maxHeap.add(_val)\n        // 下から上へヒープ化\n        siftUp(size() - 1)\n    }\n\n    /* ノード i から始めて、下から上へヒープ化 */\n    private fun siftUp(it: Int) {\n        // Kotlin の関数引数は不変のため、一時変数を作成する\n        var i = it\n        while (true) {\n            // ノード i の親ノードを取得\n            val p = parent(i)\n            // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n            if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n            // 2 つのノードを交換\n            swap(i, p)\n            // ループで下から上へヒープ化\n            i = p\n        }\n    }\n\n    /* 要素をヒープから取り出す */\n    fun pop(): Int {\n        // 空判定の処理\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        swap(0, size() - 1)\n        // ノードを削除\n        val _val = maxHeap.removeAt(size() - 1)\n        // 上から下へヒープ化\n        siftDown(0)\n        // ヒープ先頭要素を返す\n        return _val\n    }\n\n    /* ノード i から始めて、上から下へヒープ化 */\n    private fun siftDown(it: Int) {\n        // Kotlin の関数引数は不変のため、一時変数を作成する\n        var i = it\n        while (true) {\n            // ノード i, l, r のうち値が最大のノードを ma とする\n            val l = left(i)\n            val r = right(i)\n            var ma = i\n            if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n            if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n            // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n            if (ma == i) break\n            // 2 つのノードを交換\n            swap(i, ma)\n            // ループで上から下へヒープ化\n            i = ma\n        }\n    }\n\n    /* ヒープ(二分木)を出力 */\n    fun print() {\n        val queue = PriorityQueue { a: Int, b: Int -> b - a }\n        queue.addAll(maxHeap)\n        printHeap(queue)\n    }\n}\n
my_heap.rb
### コンストラクタ。入力リストに基づいてヒープを構築 ###\ndef initialize(nums)\n  # リスト要素をそのままヒープに追加\n  @max_heap = nums\n  # 葉ノード以外のすべてのノードをヒープ化\n  parent(size - 1).downto(0) do |i|\n    sift_down(i)\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 8 章   ヒープ","8.2   ヒープ構築"],"tags":[]},{"location":"chapter_heap/build_heap/#823","level":2,"title":"8.2.3   計算量の分析","text":"

以下では、2つ目のヒープ構築法の時間計算量を求めてみましょう。

  • 完全二分木のノード数を \\(n\\) とすると、葉ノード数は \\((n + 1) / 2\\) です。ここで \\(/\\) は切り捨て除算を表します。したがって、ヒープ化が必要なノード数は \\((n - 1) / 2\\) です。
  • 上から下へのヒープ化の過程では、各ノードは最大で葉ノードまでヒープ化されるため、最大反復回数は二分木の高さ \\(\\log n\\) です。

上の2つを掛け合わせると、ヒープ構築過程の時間計算量は \\(O(n \\log n)\\) となります。しかし、この見積もりは正確ではありません。二分木では下層のノード数が上層よりはるかに多いという性質を考慮していないためです。

次に、より正確な計算を行います。計算を簡単にするため、ノード数が \\(n\\) 、高さが \\(h\\) の「満二分木」を仮定します。この仮定は計算結果の正しさに影響しません。

図 8-5   満二分木の各層のノード数

上図に示すように、ノードを「上から下へヒープ化」する最大反復回数は、そのノードから葉ノードまでの距離に等しく、この距離こそが「ノードの高さ」です。したがって、各層の「ノード数 \\(\\times\\) ノードの高さ」を合計すれば、**すべてのノードのヒープ化反復回数の総和**が得られます。

\\[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{(h-1)}\\times1 \\]

上式を簡約するには中学の数列の知識を用います。まず \\(T(h)\\) に \\(2\\) を掛けると、次のようになります。

\\[ \\begin{aligned} T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{h-1}\\times1 \\newline 2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \\dots + 2^{h}\\times1 \\newline \\end{aligned} \\]

ずらして引く方法を用い、下式の \\(2 T(h)\\) から上式の \\(T(h)\\) を引くと、次が得られます。

\\[ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \\dots + 2^{h-1} + 2^h \\]

上式を見ると、\\(T(h)\\) は等比数列であることがわかるため、和の公式を直接用いて、時間計算量は次のように求められます。

\\[ \\begin{aligned} T(h) & = 2 \\frac{1 - 2^h}{1 - 2} - h \\newline & = 2^{h+1} - h - 2 \\newline & = O(2^h) \\end{aligned} \\]

さらに、高さ \\(h\\) の満二分木のノード数は \\(n = 2^{h+1} - 1\\) であるため、計算量は容易に \\(O(2^h) = O(n)\\) とわかります。以上の導出は、**入力リストからヒープを構築する時間計算量が \\(O(n)\\) であり、非常に効率的である**ことを示しています。

","path":["第 8 章   ヒープ","8.2   ヒープ構築"],"tags":[]},{"location":"chapter_heap/heap/","level":1,"title":"8.1   ヒープ","text":"

ヒープ(heap)は、特定の条件を満たす完全二分木であり、主に次の 2 種類に分けられます。

  • 最小ヒープ(min heap):任意のノードの値 \\(\\leq\\) その子ノードの値。
  • 最大ヒープ(max heap):任意のノードの値 \\(\\geq\\) その子ノードの値。

図 8-1   最小ヒープと最大ヒープ

ヒープは完全二分木の特殊な例であり、次の性質を持ちます。

  • 最下層のノードは左から順に埋められ、ほかの層のノードはすべて埋まっています。
  • 二分木の根ノードを「ヒープ頂点」、最下層で最も右にあるノードを「ヒープ底」と呼びます。
  • 最大ヒープ(最小ヒープ)では、ヒープ頂点の要素(根ノード)の値が最大(最小)です。
","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#811","level":2,"title":"8.1.1   ヒープの基本操作","text":"

ここで注意したいのは、多くのプログラミング言語が提供しているのは優先度付きキュー(priority queue)であり、これは優先度順に並ぶキューとして定義される抽象データ構造だということです。

実際には、ヒープは通常、優先度付きキューの実装に用いられ、最大ヒープは要素が大きい順に取り出される優先度付きキューに相当します。利用の観点では、「優先度付きキュー」と「ヒープ」は等価なデータ構造とみなせます。そのため、本書では両者を特に区別せず、まとめて「ヒープ」と呼びます。

ヒープの基本操作を以下の表に示します。メソッド名はプログラミング言語によって異なります。

表 8-1   ヒープの操作効率

メソッド名 説明 時間計算量 push() 要素をヒープに追加 \\(O(\\log n)\\) pop() ヒープ頂点の要素を取り出す \\(O(\\log n)\\) peek() ヒープ頂点の要素にアクセス(最大 / 最小ヒープではそれぞれ最大 / 最小値) \\(O(1)\\) size() ヒープ内の要素数を取得 \\(O(1)\\) isEmpty() ヒープが空かどうかを判定 \\(O(1)\\)

実際の応用では、プログラミング言語が提供するヒープクラス(または優先度付きキュークラス)をそのまま使えます。

ソートアルゴリズムにおける「昇順」と「降順」と同様に、flag を設定したり Comparator を変更したりすることで、「最小ヒープ」と「最大ヒープ」を切り替えられます。コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap.py
# 最小ヒープを初期化\nmin_heap, flag = [], 1\n# 最大ヒープを初期化\nmax_heap, flag = [], -1\n\n# Python の heapq モジュールはデフォルトで最小ヒープを実装している\n# 「要素を負にして」からヒープに追加すると、大小関係を反転させて最大ヒープを実現できる\n# この例では、flag = 1 のときは最小ヒープ、flag = -1 のときは最大ヒープに対応する\n\n# 要素をヒープに追加\nheapq.heappush(max_heap, flag * 1)\nheapq.heappush(max_heap, flag * 3)\nheapq.heappush(max_heap, flag * 2)\nheapq.heappush(max_heap, flag * 5)\nheapq.heappush(max_heap, flag * 4)\n\n# ヒープ頂点の要素を取得\npeek: int = flag * max_heap[0] # 5\n\n# ヒープ頂点の要素を取り出す\n# 取り出された要素は大きい順の列になる\nval = flag * heapq.heappop(max_heap) # 5\nval = flag * heapq.heappop(max_heap) # 4\nval = flag * heapq.heappop(max_heap) # 3\nval = flag * heapq.heappop(max_heap) # 2\nval = flag * heapq.heappop(max_heap) # 1\n\n# ヒープのサイズを取得\nsize: int = len(max_heap)\n\n# ヒープが空かどうかを判定\nis_empty: bool = not max_heap\n\n# 入力リストからヒープを構築\nmin_heap: list[int] = [1, 3, 2, 5, 4]\nheapq.heapify(min_heap)\n
heap.cpp
/* ヒープを初期化 */\n// 最小ヒープを初期化\npriority_queue<int, vector<int>, greater<int>> minHeap;\n// 最大ヒープを初期化\npriority_queue<int, vector<int>, less<int>> maxHeap;\n\n/* 要素をヒープに追加 */\nmaxHeap.push(1);\nmaxHeap.push(3);\nmaxHeap.push(2);\nmaxHeap.push(5);\nmaxHeap.push(4);\n\n/* ヒープ頂点の要素を取得 */\nint peek = maxHeap.top(); // 5\n\n/* ヒープ頂点の要素を取り出す */\n// 取り出された要素は大きい順の列になる\nmaxHeap.pop(); // 5\nmaxHeap.pop(); // 4\nmaxHeap.pop(); // 3\nmaxHeap.pop(); // 2\nmaxHeap.pop(); // 1\n\n/* ヒープのサイズを取得 */\nint size = maxHeap.size();\n\n/* ヒープが空かどうかを判定 */\nbool isEmpty = maxHeap.empty();\n\n/* 入力リストからヒープを構築 */\nvector<int> input{1, 3, 2, 5, 4};\npriority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());\n
heap.java
/* ヒープを初期化 */\n// 最小ヒープを初期化\nQueue<Integer> minHeap = new PriorityQueue<>();\n// 最大ヒープを初期化(lambda 式で Comparator を変更すればよい)\nQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);\n\n/* 要素をヒープに追加 */\nmaxHeap.offer(1);\nmaxHeap.offer(3);\nmaxHeap.offer(2);\nmaxHeap.offer(5);\nmaxHeap.offer(4);\n\n/* ヒープ頂点の要素を取得 */\nint peek = maxHeap.peek(); // 5\n\n/* ヒープ頂点の要素を取り出す */\n// 取り出された要素は大きい順の列になる\npeek = maxHeap.poll(); // 5\npeek = maxHeap.poll(); // 4\npeek = maxHeap.poll(); // 3\npeek = maxHeap.poll(); // 2\npeek = maxHeap.poll(); // 1\n\n/* ヒープのサイズを取得 */\nint size = maxHeap.size();\n\n/* ヒープが空かどうかを判定 */\nboolean isEmpty = maxHeap.isEmpty();\n\n/* 入力リストからヒープを構築 */\nminHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));\n
heap.cs
/* ヒープを初期化 */\n// 最小ヒープを初期化\nPriorityQueue<int, int> minHeap = new();\n// 最大ヒープを初期化(lambda 式で Comparer を変更すればよい)\nPriorityQueue<int, int> maxHeap = new(Comparer<int>.Create((x, y) => y.CompareTo(x)));\n\n/* 要素をヒープに追加 */\nmaxHeap.Enqueue(1, 1);\nmaxHeap.Enqueue(3, 3);\nmaxHeap.Enqueue(2, 2);\nmaxHeap.Enqueue(5, 5);\nmaxHeap.Enqueue(4, 4);\n\n/* ヒープ頂点の要素を取得 */\nint peek = maxHeap.Peek();//5\n\n/* ヒープ頂点の要素を取り出す */\n// 取り出された要素は大きい順の列になる\npeek = maxHeap.Dequeue();  // 5\npeek = maxHeap.Dequeue();  // 4\npeek = maxHeap.Dequeue();  // 3\npeek = maxHeap.Dequeue();  // 2\npeek = maxHeap.Dequeue();  // 1\n\n/* ヒープのサイズを取得 */\nint size = maxHeap.Count;\n\n/* ヒープが空かどうかを判定 */\nbool isEmpty = maxHeap.Count == 0;\n\n/* 入力リストからヒープを構築 */\nminHeap = new PriorityQueue<int, int>([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);\n
heap.go
// Go では、heap.Interface を実装することで整数の最大ヒープを構築できる\n// heap.Interface を実装するには、同時に sort.Interface も実装する必要がある\ntype intHeap []any\n\n// Push は heap.Interface のメソッドで、要素をヒープに追加する\nfunc (h *intHeap) Push(x any) {\n    // Push と Pop は pointer receiver を引数に取る\n    // スライスの内容を調整するだけでなく、スライスの長さも変更するため。\n    *h = append(*h, x.(int))\n}\n\n// Pop は heap.Interface のメソッドで、ヒープ頂点の要素を取り出す\nfunc (h *intHeap) Pop() any {\n    // 取り出す要素は末尾に格納されている\n    last := (*h)[len(*h)-1]\n    *h = (*h)[:len(*h)-1]\n    return last\n}\n\n// Len は sort.Interface のメソッド\nfunc (h *intHeap) Len() int {\n    return len(*h)\n}\n\n// Less は sort.Interface のメソッド\nfunc (h *intHeap) Less(i, j int) bool {\n    // 最小ヒープを実装する場合は、不等号を小なりに変更する\n    return (*h)[i].(int) > (*h)[j].(int)\n}\n\n// Swap は sort.Interface のメソッド\nfunc (h *intHeap) Swap(i, j int) {\n    (*h)[i], (*h)[j] = (*h)[j], (*h)[i]\n}\n\n// Top はヒープ頂点の要素を取得\nfunc (h *intHeap) Top() any {\n    return (*h)[0]\n}\n\n/* Driver Code */\nfunc TestHeap(t *testing.T) {\n    /* ヒープを初期化 */\n    // 最大ヒープを初期化\n    maxHeap := &intHeap{}\n    heap.Init(maxHeap)\n    /* 要素をヒープに追加 */\n    // heap.Interface のメソッドを呼び出して要素を追加する\n    heap.Push(maxHeap, 1)\n    heap.Push(maxHeap, 3)\n    heap.Push(maxHeap, 2)\n    heap.Push(maxHeap, 4)\n    heap.Push(maxHeap, 5)\n\n    /* ヒープ頂点の要素を取得 */\n    top := maxHeap.Top()\n    fmt.Printf(\"ヒープ頂点の要素は %d\\n\", top)\n\n    /* ヒープ頂点の要素を取り出す */\n    // heap.Interface のメソッドを呼び出して要素を削除する\n    heap.Pop(maxHeap) // 5\n    heap.Pop(maxHeap) // 4\n    heap.Pop(maxHeap) // 3\n    heap.Pop(maxHeap) // 2\n    heap.Pop(maxHeap) // 1\n\n    /* ヒープのサイズを取得 */\n    size := len(*maxHeap)\n    fmt.Printf(\"ヒープ内の要素数は %d\\n\", size)\n\n    /* ヒープが空かどうかを判定 */\n    isEmpty := len(*maxHeap) == 0\n    fmt.Printf(\"ヒープは空か %t\\n\", isEmpty)\n}\n
heap.swift
/* ヒープを初期化 */\n// Swift の Heap 型は最大ヒープと最小ヒープの両方をサポートしており、swift-collections の導入が必要\nvar heap = Heap<Int>()\n\n/* 要素をヒープに追加 */\nheap.insert(1)\nheap.insert(3)\nheap.insert(2)\nheap.insert(5)\nheap.insert(4)\n\n/* ヒープ頂点の要素を取得 */\nvar peek = heap.max()!\n\n/* ヒープ頂点の要素を取り出す */\npeek = heap.removeMax() // 5\npeek = heap.removeMax() // 4\npeek = heap.removeMax() // 3\npeek = heap.removeMax() // 2\npeek = heap.removeMax() // 1\n\n/* ヒープのサイズを取得 */\nlet size = heap.count\n\n/* ヒープが空かどうかを判定 */\nlet isEmpty = heap.isEmpty\n\n/* 入力リストからヒープを構築 */\nlet heap2 = Heap([1, 3, 2, 5, 4])\n
heap.js
// JavaScript には組み込みの Heap クラスがない\n
heap.ts
// TypeScript には組み込みの Heap クラスがない\n
heap.dart
// Dart には組み込みの Heap クラスがない\n
heap.rs
use std::collections::BinaryHeap;\nuse std::cmp::Reverse;\n\n/* ヒープを初期化 */\n// 最小ヒープを初期化\nlet mut min_heap = BinaryHeap::<Reverse<i32>>::new();\n// 最大ヒープを初期化\nlet mut max_heap = BinaryHeap::new();\n\n/* 要素をヒープに追加 */\nmax_heap.push(1);\nmax_heap.push(3);\nmax_heap.push(2);\nmax_heap.push(5);\nmax_heap.push(4);\n\n/* ヒープ頂点の要素を取得 */\nlet peek = max_heap.peek().unwrap();  // 5\n\n/* ヒープ頂点の要素を取り出す */\n// 取り出された要素は大きい順の列になる\nlet peek = max_heap.pop().unwrap();   // 5\nlet peek = max_heap.pop().unwrap();   // 4\nlet peek = max_heap.pop().unwrap();   // 3\nlet peek = max_heap.pop().unwrap();   // 2\nlet peek = max_heap.pop().unwrap();   // 1\n\n/* ヒープのサイズを取得 */\nlet size = max_heap.len();\n\n/* ヒープが空かどうかを判定 */\nlet is_empty = max_heap.is_empty();\n\n/* 入力リストからヒープを構築 */\nlet min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);\n
heap.c
// C には組み込みの Heap クラスがない\n
heap.kt
/* ヒープを初期化 */\n// 最小ヒープを初期化\nvar minHeap = PriorityQueue<Int>()\n// 最大ヒープを初期化(lambda 式で Comparator を変更すればよい)\nval maxHeap = PriorityQueue { a: Int, b: Int -> b - a }\n\n/* 要素をヒープに追加 */\nmaxHeap.offer(1)\nmaxHeap.offer(3)\nmaxHeap.offer(2)\nmaxHeap.offer(5)\nmaxHeap.offer(4)\n\n/* ヒープ頂点の要素を取得 */\nvar peek = maxHeap.peek() // 5\n\n/* ヒープ頂点の要素を取り出す */\n// 取り出された要素は大きい順の列になる\npeek = maxHeap.poll() // 5\npeek = maxHeap.poll() // 4\npeek = maxHeap.poll() // 3\npeek = maxHeap.poll() // 2\npeek = maxHeap.poll() // 1\n\n/* ヒープのサイズを取得 */\nval size = maxHeap.size\n\n/* ヒープが空かどうかを判定 */\nval isEmpty = maxHeap.isEmpty()\n\n/* 入力リストからヒープを構築 */\nminHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))\n
heap.rb
# Ruby には組み込みの Heap クラスがない\n
実行を可視化

https://pythontutor.com/render.html#code=import%20heapq%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20min_heap,%20flag%20%3D%20%5B%5D,%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20max_heap,%20flag%20%3D%20%5B%5D,%20-1%0A%20%20%20%20%0A%20%20%20%20%23%20Python%20%E7%9A%84%20heapq%20%E6%A8%A1%E5%9D%97%E9%BB%98%E8%AE%A4%E5%AE%9E%E7%8E%B0%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E8%80%83%E8%99%91%E5%B0%86%E2%80%9C%E5%85%83%E7%B4%A0%E5%8F%96%E8%B4%9F%E2%80%9D%E5%90%8E%E5%86%8D%E5%85%A5%E5%A0%86%EF%BC%8C%E8%BF%99%E6%A0%B7%E5%B0%B1%E5%8F%AF%E4%BB%A5%E5%B0%86%E5%A4%A7%E5%B0%8F%E5%85%B3%E7%B3%BB%E9%A2%A0%E5%80%92%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%AE%9E%E7%8E%B0%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E5%9C%A8%E6%9C%AC%E7%A4%BA%E4%BE%8B%E4%B8%AD%EF%BC%8Cflag%20%3D%201%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%B0%8F%E9%A1%B6%E5%A0%86%EF%BC%8Cflag%20%3D%20-1%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%201%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%203%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%202%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%205%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%204%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20flag%20*%20max_heap%5B0%5D%20%23%205%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%E5%87%BA%E5%A0%86%0A%20%20%20%20%23%20%E5%87%BA%E5%A0%86%E5%85%83%E7%B4%A0%E4%BC%9A%E5%BD%A2%E6%88%90%E4%B8%80%E4%B8%AA%E4%BB%8E%E5%A4%A7%E5%88%B0%E5%B0%8F%E7%9A%84%E5%BA%8F%E5%88%97%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%205%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%204%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%203%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%202%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%201%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E5%A4%A7%E5%B0%8F%0A%20%20%20%20size%20%3D%20len%28max_heap%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%A0%86%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20not%20max_heap%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%BE%93%E5%85%A5%E5%88%97%E8%A1%A8%E5%B9%B6%E5%BB%BA%E5%A0%86%0A%20%20%20%20min_heap%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20heapq.heapify%28min_heap%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#812","level":2,"title":"8.1.2   ヒープの実装","text":"

以下では最大ヒープを実装します。最小ヒープに変換したい場合は、すべての大小比較ロジックを反転させるだけです(たとえば、\\(\\geq\\) を \\(\\leq\\) に置き換えます)。興味のある読者は自分で実装してみてください。

","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#1","level":3,"title":"1.   ヒープの格納と表現","text":"

「二分木」の章で述べたように、完全二分木は配列で表現するのに非常に適しています。ヒープはまさに完全二分木の一種なので、ここでは配列を使ってヒープを格納します。

配列で二分木を表す場合、要素はノードの値を表し、インデックスは二分木におけるノードの位置を表します。ノード間の参照関係はインデックスの対応式によって実現できます。

次の図に示すように、インデックス \\(i\\) に対して、左子ノードのインデックスは \\(2i + 1\\) 、右子ノードのインデックスは \\(2i + 2\\) 、親ノードのインデックスは \\((i - 1) / 2\\)(切り捨て除算)です。インデックスが範囲外であれば、空ノードまたはノードが存在しないことを表します。

図 8-2   ヒープの表現と格納

インデックスの対応式は関数にまとめておくと、後続で使いやすくなります:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def left(self, i: int) -> int:\n    \"\"\"左子ノードのインデックスを取得\"\"\"\n    return 2 * i + 1\n\ndef right(self, i: int) -> int:\n    \"\"\"右子ノードのインデックスを取得\"\"\"\n    return 2 * i + 2\n\ndef parent(self, i: int) -> int:\n    \"\"\"親ノードのインデックスを取得\"\"\"\n    return (i - 1) // 2  # 切り捨て除算\n
my_heap.cpp
/* 左子ノードのインデックスを取得 */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint parent(int i) {\n    return (i - 1) / 2; // 切り捨て除算\n}\n
my_heap.java
/* 左子ノードのインデックスを取得 */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint parent(int i) {\n    return (i - 1) / 2; // 切り捨て除算\n}\n
my_heap.cs
/* 左子ノードのインデックスを取得 */\nint Left(int i) {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nint Right(int i) {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint Parent(int i) {\n    return (i - 1) / 2; // 切り捨て除算\n}\n
my_heap.go
/* 左子ノードのインデックスを取得 */\nfunc (h *maxHeap) left(i int) int {\n    return 2*i + 1\n}\n\n/* 右子ノードのインデックスを取得 */\nfunc (h *maxHeap) right(i int) int {\n    return 2*i + 2\n}\n\n/* 親ノードのインデックスを取得 */\nfunc (h *maxHeap) parent(i int) int {\n    // 切り捨て除算\n    return (i - 1) / 2\n}\n
my_heap.swift
/* 左子ノードのインデックスを取得 */\nfunc left(i: Int) -> Int {\n    2 * i + 1\n}\n\n/* 右子ノードのインデックスを取得 */\nfunc right(i: Int) -> Int {\n    2 * i + 2\n}\n\n/* 親ノードのインデックスを取得 */\nfunc parent(i: Int) -> Int {\n    (i - 1) / 2 // 切り捨て除算\n}\n
my_heap.js
/* 左子ノードのインデックスを取得 */\n#left(i) {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\n#right(i) {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\n#parent(i) {\n    return Math.floor((i - 1) / 2); // 切り捨て除算\n}\n
my_heap.ts
/* 左子ノードのインデックスを取得 */\nleft(i: number): number {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nright(i: number): number {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nparent(i: number): number {\n    return Math.floor((i - 1) / 2); // 切り捨て除算\n}\n
my_heap.dart
/* 左子ノードのインデックスを取得 */\nint _left(int i) {\n  return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nint _right(int i) {\n  return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint _parent(int i) {\n  return (i - 1) ~/ 2; // 切り捨て除算\n}\n
my_heap.rs
/* 左子ノードのインデックスを取得 */\nfn left(i: usize) -> usize {\n    2 * i + 1\n}\n\n/* 右子ノードのインデックスを取得 */\nfn right(i: usize) -> usize {\n    2 * i + 2\n}\n\n/* 親ノードのインデックスを取得 */\nfn parent(i: usize) -> usize {\n    (i - 1) / 2 // 切り捨て除算\n}\n
my_heap.c
/* 左子ノードのインデックスを取得 */\nint left(MaxHeap *maxHeap, int i) {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nint right(MaxHeap *maxHeap, int i) {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint parent(MaxHeap *maxHeap, int i) {\n    return (i - 1) / 2; // 切り捨て\n}\n
my_heap.kt
/* 左子ノードのインデックスを取得 */\nfun left(i: Int): Int {\n    return 2 * i + 1\n}\n\n/* 右子ノードのインデックスを取得 */\nfun right(i: Int): Int {\n    return 2 * i + 2\n}\n\n/* 親ノードのインデックスを取得 */\nfun parent(i: Int): Int {\n    return (i - 1) / 2 // 切り捨て除算\n}\n
my_heap.rb
### 左子ノードのインデックスを取得 ###\ndef left(i)\n  2 * i + 1\nend\n\n### 右子ノードのインデックスを取得 ###\ndef right(i)\n  2 * i + 2\nend\n\n### 親ノードのインデックスを取得 ###\ndef parent(i)\n  (i - 1) / 2     # 切り捨て除算\nend\n
","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#2","level":3,"title":"2.   ヒープ頂点の要素にアクセス","text":"

ヒープ頂点の要素は二分木の根ノード、すなわちリストの先頭要素です:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def peek(self) -> int:\n    \"\"\"ヒープ先頭要素にアクセス\"\"\"\n    return self.max_heap[0]\n
my_heap.cpp
/* ヒープ先頭要素にアクセス */\nint peek() {\n    return maxHeap[0];\n}\n
my_heap.java
/* ヒープ先頭要素にアクセス */\nint peek() {\n    return maxHeap.get(0);\n}\n
my_heap.cs
/* ヒープ先頭要素にアクセス */\nint Peek() {\n    return maxHeap[0];\n}\n
my_heap.go
/* ヒープ先頭要素にアクセス */\nfunc (h *maxHeap) peek() any {\n    return h.data[0]\n}\n
my_heap.swift
/* ヒープ先頭要素にアクセス */\nfunc peek() -> Int {\n    maxHeap[0]\n}\n
my_heap.js
/* ヒープ先頭要素にアクセス */\npeek() {\n    return this.#maxHeap[0];\n}\n
my_heap.ts
/* ヒープ先頭要素にアクセス */\npeek(): number {\n    return this.maxHeap[0];\n}\n
my_heap.dart
/* ヒープ先頭要素にアクセス */\nint peek() {\n  return _maxHeap[0];\n}\n
my_heap.rs
/* ヒープ先頭要素にアクセス */\nfn peek(&self) -> Option<i32> {\n    self.max_heap.first().copied()\n}\n
my_heap.c
/* ヒープ先頭要素にアクセス */\nint peek(MaxHeap *maxHeap) {\n    return maxHeap->data[0];\n}\n
my_heap.kt
/* ヒープ先頭要素にアクセス */\nfun peek(): Int {\n    return maxHeap[0]\n}\n
my_heap.rb
### ヒープ先頭要素を参照 ###\ndef peek\n  @max_heap[0]\nend\n
コードの可視化

全画面で見る >

","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#3","level":3,"title":"3.   要素をヒープに追加","text":"

与えられた要素 val を、まずヒープの底に追加します。追加後、val がヒープ内のほかの要素より大きい可能性があるため、ヒープ条件が崩れているかもしれません。そのため、挿入ノードから根ノードまでの経路上にある各ノードを修復する必要があります。この操作をヒープ化(heapify)と呼びます。

ヒープへ追加したノードから始めて、**下から上へヒープ化**を行います。次の図のように、挿入ノードとその親ノードの値を比較し、挿入ノードのほうが大きければそれらを交換します。その後もこの操作を繰り返し、下から上へ各ノードを修復して、根ノードを越えるか交換不要のノードに達した時点で終了します。

<1><2><3><4><5><6><7><8><9>

図 8-3   要素をヒープに追加する手順

ノード総数を \\(n\\) とすると、木の高さは \\(O(\\log n)\\) です。したがって、ヒープ化操作のループ回数は高々 \\(O(\\log n)\\) であり、要素をヒープに追加する操作の時間計算量は \\(O(\\log n)\\) です。コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def push(self, val: int):\n    \"\"\"要素をヒープに追加\"\"\"\n    # ノードを追加\n    self.max_heap.append(val)\n    # 下から上へヒープ化\n    self.sift_up(self.size() - 1)\n\ndef sift_up(self, i: int):\n    \"\"\"ノード i から始めて、下から上へヒープ化\"\"\"\n    while True:\n        # ノード i の親ノードを取得\n        p = self.parent(i)\n        # 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n            break\n        # 2 つのノードを交換\n        self.swap(i, p)\n        # ループで下から上へヒープ化\n        i = p\n
my_heap.cpp
/* 要素をヒープに追加 */\nvoid push(int val) {\n    // ノードを追加\n    maxHeap.push_back(val);\n    // 下から上へヒープ化\n    siftUp(size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nvoid siftUp(int i) {\n    while (true) {\n        // ノード i の親ノードを取得\n        int p = parent(i);\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // 2 つのノードを交換\n        swap(maxHeap[i], maxHeap[p]);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.java
/* 要素をヒープに追加 */\nvoid push(int val) {\n    // ノードを追加\n    maxHeap.add(val);\n    // 下から上へヒープ化\n    siftUp(size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nvoid siftUp(int i) {\n    while (true) {\n        // ノード i の親ノードを取得\n        int p = parent(i);\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n            break;\n        // 2 つのノードを交換\n        swap(i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.cs
/* 要素をヒープに追加 */\nvoid Push(int val) {\n    // ノードを追加\n    maxHeap.Add(val);\n    // 下から上へヒープ化\n    SiftUp(Size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nvoid SiftUp(int i) {\n    while (true) {\n        // ノード i の親ノードを取得\n        int p = Parent(i);\n        // 「根ノードを越えた」または「ノードの修復が不要」な場合は、ヒープ化を終了する\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // 2 つのノードを交換\n        Swap(i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.go
/* 要素をヒープに追加 */\nfunc (h *maxHeap) push(val any) {\n    // ノードを追加\n    h.data = append(h.data, val)\n    // 下から上へヒープ化\n    h.siftUp(len(h.data) - 1)\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nfunc (h *maxHeap) siftUp(i int) {\n    for true {\n        // ノード i の親ノードを取得\n        p := h.parent(i)\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 || h.data[i].(int) <= h.data[p].(int) {\n            break\n        }\n        // 2 つのノードを交換\n        h.swap(i, p)\n        // ループで下から上へヒープ化\n        i = p\n    }\n}\n
my_heap.swift
/* 要素をヒープに追加 */\nfunc push(val: Int) {\n    // ノードを追加\n    maxHeap.append(val)\n    // 下から上へヒープ化\n    siftUp(i: size() - 1)\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nfunc siftUp(i: Int) {\n    var i = i\n    while true {\n        // ノード i の親ノードを取得\n        let p = parent(i: i)\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 || maxHeap[i] <= maxHeap[p] {\n            break\n        }\n        // 2 つのノードを交換\n        swap(i: i, j: p)\n        // ループで下から上へヒープ化\n        i = p\n    }\n}\n
my_heap.js
/* 要素をヒープに追加 */\npush(val) {\n    // ノードを追加\n    this.#maxHeap.push(val);\n    // 下から上へヒープ化\n    this.#siftUp(this.size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\n#siftUp(i) {\n    while (true) {\n        // ノード i の親ノードを取得\n        const p = this.#parent(i);\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break;\n        // 2 つのノードを交換\n        this.#swap(i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.ts
/* 要素をヒープに追加 */\npush(val: number): void {\n    // ノードを追加\n    this.maxHeap.push(val);\n    // 下から上へヒープ化\n    this.siftUp(this.size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nsiftUp(i: number): void {\n    while (true) {\n        // ノード i の親ノードを取得\n        const p = this.parent(i);\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break;\n        // 2 つのノードを交換\n        this.swap(i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.dart
/* 要素をヒープに追加 */\nvoid push(int val) {\n  // ノードを追加\n  _maxHeap.add(val);\n  // 下から上へヒープ化\n  siftUp(size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nvoid siftUp(int i) {\n  while (true) {\n    // ノード i の親ノードを取得\n    int p = _parent(i);\n    // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n    if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {\n      break;\n    }\n    // 2 つのノードを交換\n    _swap(i, p);\n    // ループで下から上へヒープ化\n    i = p;\n  }\n}\n
my_heap.rs
/* 要素をヒープに追加 */\nfn push(&mut self, val: i32) {\n    // ノードを追加\n    self.max_heap.push(val);\n    // 下から上へヒープ化\n    self.sift_up(self.size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nfn sift_up(&mut self, mut i: usize) {\n    loop {\n        // ノード i はすでにヒープの先頭ノードなので、ヒープ化を終了する\n        if i == 0 {\n            break;\n        }\n        // ノード i の親ノードを取得\n        let p = Self::parent(i);\n        // 「ノードの修復が不要」になったら、ヒープ化を終了\n        if self.max_heap[i] <= self.max_heap[p] {\n            break;\n        }\n        // 2 つのノードを交換\n        self.swap(i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.c
/* 要素をヒープに追加 */\nvoid push(MaxHeap *maxHeap, int val) {\n    // 通常は、これほど多くのノードを追加すべきではない\n    if (maxHeap->size == MAX_SIZE) {\n        printf(\"heap is full!\");\n        return;\n    }\n    // ノードを追加\n    maxHeap->data[maxHeap->size] = val;\n    maxHeap->size++;\n\n    // 下から上へヒープ化\n    siftUp(maxHeap, maxHeap->size - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nvoid siftUp(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // ノード i の親ノードを取得\n        int p = parent(maxHeap, i);\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {\n            break;\n        }\n        // 2 つのノードを交換\n        swap(maxHeap, i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.kt
/* 要素をヒープに追加 */\nfun push(_val: Int) {\n    // ノードを追加\n    maxHeap.add(_val)\n    // 下から上へヒープ化\n    siftUp(size() - 1)\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nfun siftUp(it: Int) {\n    // Kotlin の関数引数は不変のため、一時変数を作成する\n    var i = it\n    while (true) {\n        // ノード i の親ノードを取得\n        val p = parent(i)\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n        // 2 つのノードを交換\n        swap(i, p)\n        // ループで下から上へヒープ化\n        i = p\n    }\n}\n
my_heap.rb
### 要素をヒープに挿入 ###\ndef push(val)\n  # ノードを追加\n  @max_heap << val\n  # 下から上へヒープ化\n  sift_up(size - 1)\nend\n\n### ノード i から下から上へヒープ化 ###\ndef sift_up(i)\n  loop do\n    # ノード i の親ノードを取得\n    p = parent(i)\n    # 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n    break if p < 0 || @max_heap[i] <= @max_heap[p]\n    # 2 つのノードを交換\n    swap(i, p)\n    # ループで下から上へヒープ化\n    i = p\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#4","level":3,"title":"4.   ヒープ頂点の要素を取り出す","text":"

ヒープ頂点の要素は二分木の根ノード、すなわちリストの先頭要素です。もし先頭要素をそのまま削除すると、二分木内のすべてのノードのインデックスが変化してしまい、その後のヒープ化による修復が困難になります。要素インデックスの変動をできるだけ小さくするため、次の手順を取ります。

  1. ヒープ頂点の要素とヒープ底の要素を交換する(根ノードと最も右の葉ノードを交換する)。
  2. 交換後、ヒープ底をリストから削除する(すでに交換済みであるため、実際に削除されるのは元のヒープ頂点の要素であることに注意)。
  3. 根ノードから開始し、**上から下へヒープ化**を行う。

次の図のように、**「上から下へのヒープ化」の方向は「下から上へのヒープ化」と逆**です。根ノードの値を 2 つの子ノードと比較し、最大の子ノードと根ノードを交換します。その後、この操作を繰り返し、葉ノードを越えるか交換不要のノードに達した時点で終了します。

<1><2><3><4><5><6><7><8><9><10>

図 8-4   ヒープ頂点の要素を取り出す手順

要素をヒープに追加する操作と同様に、ヒープ頂点の要素を取り出す操作の時間計算量も \\(O(\\log n)\\) です。コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def pop(self) -> int:\n    \"\"\"要素をヒープから取り出す\"\"\"\n    # 空判定の処理\n    if self.is_empty():\n        raise IndexError(\"ヒープが空です\")\n    # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    self.swap(0, self.size() - 1)\n    # ノードを削除\n    val = self.max_heap.pop()\n    # 上から下へヒープ化\n    self.sift_down(0)\n    # ヒープ先頭要素を返す\n    return val\n\ndef sift_down(self, i: int):\n    \"\"\"ノード i から始めて、上から下へヒープ化\"\"\"\n    while True:\n        # ノード i, l, r のうち値が最大のノードを ma とする\n        l, r, ma = self.left(i), self.right(i), i\n        if l < self.size() and self.max_heap[l] > self.max_heap[ma]:\n            ma = l\n        if r < self.size() and self.max_heap[r] > self.max_heap[ma]:\n            ma = r\n        # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i:\n            break\n        # 2 つのノードを交換\n        self.swap(i, ma)\n        # ループで上から下へヒープ化\n        i = ma\n
my_heap.cpp
/* 要素をヒープから取り出す */\nvoid pop() {\n    // 空判定の処理\n    if (isEmpty()) {\n        throw out_of_range(\"ヒープが空です\");\n    }\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    swap(maxHeap[0], maxHeap[size() - 1]);\n    // ノードを削除\n    maxHeap.pop_back();\n    // 上から下へヒープ化\n    siftDown(0);\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nvoid siftDown(int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i)\n            break;\n        swap(maxHeap[i], maxHeap[ma]);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.java
/* 要素をヒープから取り出す */\nint pop() {\n    // 空判定の処理\n    if (isEmpty())\n        throw new IndexOutOfBoundsException();\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    swap(0, size() - 1);\n    // ノードを削除\n    int val = maxHeap.remove(size() - 1);\n    // 上から下へヒープ化\n    siftDown(0);\n    // ヒープ先頭要素を返す\n    return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nvoid siftDown(int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap.get(l) > maxHeap.get(ma))\n            ma = l;\n        if (r < size() && maxHeap.get(r) > maxHeap.get(ma))\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i)\n            break;\n        // 2 つのノードを交換\n        swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.cs
/* 要素をヒープから取り出す */\nint Pop() {\n    // 空判定の処理\n    if (IsEmpty())\n        throw new IndexOutOfRangeException();\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    Swap(0, Size() - 1);\n    // ノードを削除\n    int val = maxHeap.Last();\n    maxHeap.RemoveAt(Size() - 1);\n    // 上から下へヒープ化\n    SiftDown(0);\n    // ヒープ先頭要素を返す\n    return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nvoid SiftDown(int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = Left(i), r = Right(i), ma = i;\n        if (l < Size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < Size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // 「ノード i が最大」または「葉ノードを越えた」場合は、ヒープ化を終了する\n        if (ma == i) break;\n        // 2 つのノードを交換\n        Swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.go
/* 要素をヒープから取り出す */\nfunc (h *maxHeap) pop() any {\n    // 空判定の処理\n    if h.isEmpty() {\n        fmt.Println(\"error\")\n        return nil\n    }\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    h.swap(0, h.size()-1)\n    // ノードを削除\n    val := h.data[len(h.data)-1]\n    h.data = h.data[:len(h.data)-1]\n    // 上から下へヒープ化\n    h.siftDown(0)\n\n    // ヒープ先頭要素を返す\n    return val\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nfunc (h *maxHeap) siftDown(i int) {\n    for true {\n        // ノード i, l, r のうち値が最大のノードを max とする\n        l, r, max := h.left(i), h.right(i), i\n        if l < h.size() && h.data[l].(int) > h.data[max].(int) {\n            max = l\n        }\n        if r < h.size() && h.data[r].(int) > h.data[max].(int) {\n            max = r\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if max == i {\n            break\n        }\n        // 2 つのノードを交換\n        h.swap(i, max)\n        // ループで上から下へヒープ化\n        i = max\n    }\n}\n
my_heap.swift
/* 要素をヒープから取り出す */\nfunc pop() -> Int {\n    // 空判定の処理\n    if isEmpty() {\n        fatalError(\"ヒープが空です\")\n    }\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    swap(i: 0, j: size() - 1)\n    // ノードを削除\n    let val = maxHeap.remove(at: size() - 1)\n    // 上から下へヒープ化\n    siftDown(i: 0)\n    // ヒープ先頭要素を返す\n    return val\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nfunc siftDown(i: Int) {\n    var i = i\n    while true {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let l = left(i: i)\n        let r = right(i: i)\n        var ma = i\n        if l < size(), maxHeap[l] > maxHeap[ma] {\n            ma = l\n        }\n        if r < size(), maxHeap[r] > maxHeap[ma] {\n            ma = r\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i {\n            break\n        }\n        // 2 つのノードを交換\n        swap(i: i, j: ma)\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n
my_heap.js
/* 要素をヒープから取り出す */\npop() {\n    // 空判定の処理\n    if (this.isEmpty()) throw new Error('ヒープが空です');\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    this.#swap(0, this.size() - 1);\n    // ノードを削除\n    const val = this.#maxHeap.pop();\n    // 上から下へヒープ化\n    this.#siftDown(0);\n    // ヒープ先頭要素を返す\n    return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\n#siftDown(i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        const l = this.#left(i),\n            r = this.#right(i);\n        let ma = i;\n        if (l < this.size() && this.#maxHeap[l] > this.#maxHeap[ma]) ma = l;\n        if (r < this.size() && this.#maxHeap[r] > this.#maxHeap[ma]) ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma === i) break;\n        // 2 つのノードを交換\n        this.#swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.ts
/* 要素をヒープから取り出す */\npop(): number {\n    // 空判定の処理\n    if (this.isEmpty()) throw new RangeError('Heap is empty.');\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    this.swap(0, this.size() - 1);\n    // ノードを削除\n    const val = this.maxHeap.pop();\n    // 上から下へヒープ化\n    this.siftDown(0);\n    // ヒープ先頭要素を返す\n    return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nsiftDown(i: number): void {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        const l = this.left(i),\n            r = this.right(i);\n        let ma = i;\n        if (l < this.size() && this.maxHeap[l] > this.maxHeap[ma]) ma = l;\n        if (r < this.size() && this.maxHeap[r] > this.maxHeap[ma]) ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma === i) break;\n        // 2 つのノードを交換\n        this.swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.dart
/* 要素をヒープから取り出す */\nint pop() {\n  // 空判定の処理\n  if (isEmpty()) throw Exception('ヒープが空です');\n  // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n  _swap(0, size() - 1);\n  // ノードを削除\n  int val = _maxHeap.removeLast();\n  // 上から下へヒープ化\n  siftDown(0);\n  // ヒープ先頭要素を返す\n  return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nvoid siftDown(int i) {\n  while (true) {\n    // ノード i, l, r のうち値が最大のノードを ma とする\n    int l = _left(i);\n    int r = _right(i);\n    int ma = i;\n    if (l < size() && _maxHeap[l] > _maxHeap[ma]) ma = l;\n    if (r < size() && _maxHeap[r] > _maxHeap[ma]) ma = r;\n    // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n    if (ma == i) break;\n    // 2 つのノードを交換\n    _swap(i, ma);\n    // ループで上から下へヒープ化\n    i = ma;\n  }\n}\n
my_heap.rs
/* 要素をヒープから取り出す */\nfn pop(&mut self) -> i32 {\n    // 空判定の処理\n    if self.is_empty() {\n        panic!(\"index out of bounds\");\n    }\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    self.swap(0, self.size() - 1);\n    // ノードを削除\n    let val = self.max_heap.pop().unwrap();\n    // 上から下へヒープ化\n    self.sift_down(0);\n    // ヒープ先頭要素を返す\n    val\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nfn sift_down(&mut self, mut i: usize) {\n    loop {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let (l, r, mut ma) = (Self::left(i), Self::right(i), i);\n        if l < self.size() && self.max_heap[l] > self.max_heap[ma] {\n            ma = l;\n        }\n        if r < self.size() && self.max_heap[r] > self.max_heap[ma] {\n            ma = r;\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i {\n            break;\n        }\n        // 2 つのノードを交換\n        self.swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.c
/* 要素をヒープから取り出す */\nint pop(MaxHeap *maxHeap) {\n    // 空判定の処理\n    if (isEmpty(maxHeap)) {\n        printf(\"heap is empty!\");\n        return INT_MAX;\n    }\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    swap(maxHeap, 0, size(maxHeap) - 1);\n    // ノードを削除\n    int val = maxHeap->data[maxHeap->size - 1];\n    maxHeap->size--;\n    // 上から下へヒープ化\n    siftDown(maxHeap, 0);\n\n    // ヒープ先頭要素を返す\n    return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nvoid siftDown(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを max とする\n        int l = left(maxHeap, i);\n        int r = right(maxHeap, i);\n        int max = i;\n        if (l < size(maxHeap) && maxHeap->data[l] > maxHeap->data[max]) {\n            max = l;\n        }\n        if (r < size(maxHeap) && maxHeap->data[r] > maxHeap->data[max]) {\n            max = r;\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (max == i) {\n            break;\n        }\n        // 2 つのノードを交換\n        swap(maxHeap, i, max);\n        // ループで上から下へヒープ化\n        i = max;\n    }\n}\n
my_heap.kt
/* 要素をヒープから取り出す */\nfun pop(): Int {\n    // 空判定の処理\n    if (isEmpty()) throw IndexOutOfBoundsException()\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    swap(0, size() - 1)\n    // ノードを削除\n    val _val = maxHeap.removeAt(size() - 1)\n    // 上から下へヒープ化\n    siftDown(0)\n    // ヒープ先頭要素を返す\n    return _val\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nfun siftDown(it: Int) {\n    // Kotlin の関数引数は不変のため、一時変数を作成する\n    var i = it\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        val l = left(i)\n        val r = right(i)\n        var ma = i\n        if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n        if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i) break\n        // 2 つのノードを交換\n        swap(i, ma)\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n
my_heap.rb
### 要素をヒープから取り出す ###\ndef pop\n  # 空判定の処理\n  raise IndexError, \"ヒープが空です\" if is_empty?\n  # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n  swap(0, size - 1)\n  # ノードを削除\n  val = @max_heap.pop\n  # 上から下へヒープ化\n  sift_down(0)\n  # ヒープ先頭要素を返す\n  val\nend\n\n### ノード i から上から下へヒープ化 ###\ndef sift_down(i)\n  loop do\n    # ノード i, l, r のうち値が最大のノードを ma とする\n    l, r, ma = left(i), right(i), i\n    ma = l if l < size && @max_heap[l] > @max_heap[ma]\n    ma = r if r < size && @max_heap[r] > @max_heap[ma]\n\n    # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n    break if ma == i\n\n    # 2 つのノードを交換\n    swap(i, ma)\n    # ループで上から下へヒープ化\n    i = ma\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#813","level":2,"title":"8.1.3   ヒープの代表的な応用","text":"
  • 優先度付きキュー:ヒープは、優先度付きキューを実装するための代表的なデータ構造です。キューへの追加と取り出しの時間計算量はいずれも \\(O(\\log n)\\) で、ヒープ構築は \\(O(n)\\) であり、これらの操作はいずれも非常に効率的です。
  • ヒープソート:与えられたデータ群からヒープを構築し、要素の取り出しを繰り返すことで整列済みデータを得られます。ただし、通常はより洗練された方法でヒープソートを実装します。詳しくは「ヒープソート」の章を参照してください。
  • 最大の \\(k\\) 個の要素を取得:これは古典的なアルゴリズム問題であると同時に、典型的な応用でもあります。たとえば、人気上位 10 件のニュースをホットトピックとして選んだり、売上上位 10 件の商品を選んだりする場面です。
","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/summary/","level":1,"title":"8.4   まとめ","text":"","path":["第 8 章   ヒープ","8.4   まとめ"],"tags":[]},{"location":"chapter_heap/summary/#1","level":3,"title":"1.   重要なポイントの振り返り","text":"
  • ヒープは完全二分木であり、条件の違いによって最大ヒープと最小ヒープに分けられる。最大(最小)ヒープの根の要素は最大値(最小値)である。
  • 優先度付きキューとは、取り出し時に優先度が考慮されるキューであり、通常はヒープを用いて実装される。
  • ヒープの代表的な操作とそれに対応する時間計算量には、要素の挿入 \\(O(\\log n)\\)、根の要素の削除 \\(O(\\log n)\\)、根の要素へのアクセス \\(O(1)\\) などがある。
  • 完全二分木は配列で表現するのに非常に適しているため、通常は配列を使ってヒープを格納する。
  • ヒープ化操作はヒープの性質を保つために用いられ、挿入操作と削除操作の両方で使用される。
  • \\(n\\) 個の要素を入力してヒープを構築する時間計算量は \\(O(n)\\) まで最適化でき、非常に効率的である。
  • Top-k は古典的なアルゴリズム問題であり、ヒープ構造を用いることで効率的に解くことができ、時間計算量は \\(O(n \\log k)\\) である。
","path":["第 8 章   ヒープ","8.4   まとめ"],"tags":[]},{"location":"chapter_heap/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:データ構造の「ヒープ」とメモリ管理の「ヒープ」は同じ概念ですか?

両者は同じ概念ではなく、たまたまどちらも「ヒープ」と呼ばれているだけである。コンピュータシステムのメモリにおけるヒープは動的メモリ割り当ての一部であり、プログラムは実行時にこれを使ってデータを格納できる。プログラムは一定量のヒープメモリを要求し、オブジェクトや配列などの複雑な構造を保存できる。これらのデータが不要になったときは、メモリリークを防ぐためにそのメモリを解放する必要がある。スタックメモリと比べると、ヒープメモリの管理と使用にはより慎重さが求められ、不適切に扱うとメモリリークやダングリングポインタなどの問題を引き起こす可能性がある。

","path":["第 8 章   ヒープ","8.4   まとめ"],"tags":[]},{"location":"chapter_heap/top_k/","level":1,"title":"8.3   Top-k 問題","text":"

Question

長さ \\(n\\) の未整列配列 nums が与えられたとき、配列内で最大の \\(k\\) 個の要素を返してください。

この問題について、まずは発想が比較的直接的な 2 つの解法を紹介し、その後でより効率の高いヒープ解法を紹介します。

","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#831","level":2,"title":"8.3.1   方法一:走査による選択","text":"

以下の図に示すように \\(k\\) 回の走査を行い、各ラウンドでそれぞれ第 \\(1\\)、\\(2\\)、\\(\\dots\\)、\\(k\\) 位の要素を取り出すことができます。時間計算量は \\(O(nk)\\) です。

この方法は \\(k \\ll n\\) の場合にしか適していません。\\(k\\) が \\(n\\) にかなり近いと、時間計算量は \\(O(n^2)\\) に近づき、非常に時間がかかるためです。

図 8-6   走査によって最大の k 個の要素を探す

Tip

\\(k = n\\) のとき、完全な昇順列を得ることができ、この場合は「選択ソート」アルゴリズムと等価になります。

","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#832","level":2,"title":"8.3.2   方法二:ソート","text":"

以下の図に示すように、まず配列 nums をソートし、その後で右端の \\(k\\) 個の要素を返すことができます。時間計算量は \\(O(n \\log n)\\) です。

明らかに、この方法は必要以上の処理を行っています。なぜなら、必要なのは最大の \\(k\\) 個の要素を見つけることだけであり、他の要素をソートする必要はないからです。

図 8-7   ソートによって最大の k 個の要素を探す

","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#833","level":2,"title":"8.3.3   方法三:ヒープ","text":"

ヒープを用いることで、Top-k 問題をより効率的に解くことができます。手順は以下の図のとおりです。

  1. 最小ヒープを初期化し、そのヒープ頂点の要素が最小となるようにします。
  2. まず配列の先頭 \\(k\\) 個の要素を順にヒープへ挿入します。
  3. \\(k + 1\\) 番目の要素から開始し、現在の要素がヒープ頂点の要素より大きければ、ヒープ頂点の要素を取り出し、現在の要素をヒープへ挿入します。
  4. 走査が完了した後、ヒープに保持されているのが最大の \\(k\\) 個の要素です。
<1><2><3><4><5><6><7><8><9>

図 8-8   ヒープに基づいて最大の k 個の要素を探す

サンプルコードは以下のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby top_k.py
def top_k_heap(nums: list[int], k: int) -> list[int]:\n    \"\"\"ヒープに基づいて配列中の最大の k 個の要素を探す\"\"\"\n    # 最小ヒープを初期化\n    heap = []\n    # 配列の先頭 k 個の要素をヒープに追加\n    for i in range(k):\n        heapq.heappush(heap, nums[i])\n    # k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for i in range(k, len(nums)):\n        # 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if nums[i] > heap[0]:\n            heapq.heappop(heap)\n            heapq.heappush(heap, nums[i])\n    return heap\n
top_k.cpp
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\npriority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {\n    // 最小ヒープを初期化\n    priority_queue<int, vector<int>, greater<int>> heap;\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (int i = 0; i < k; i++) {\n        heap.push(nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (int i = k; i < nums.size(); i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > heap.top()) {\n            heap.pop();\n            heap.push(nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.java
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nQueue<Integer> topKHeap(int[] nums, int k) {\n    // 最小ヒープを初期化\n    Queue<Integer> heap = new PriorityQueue<Integer>();\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (int i = 0; i < k; i++) {\n        heap.offer(nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (int i = k; i < nums.length; i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > heap.peek()) {\n            heap.poll();\n            heap.offer(nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.cs
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nPriorityQueue<int, int> TopKHeap(int[] nums, int k) {\n    // 最小ヒープを初期化\n    PriorityQueue<int, int> heap = new();\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (int i = 0; i < k; i++) {\n        heap.Enqueue(nums[i], nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (int i = k; i < nums.Length; i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > heap.Peek()) {\n            heap.Dequeue();\n            heap.Enqueue(nums[i], nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.go
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfunc topKHeap(nums []int, k int) *minHeap {\n    // 最小ヒープを初期化\n    h := &minHeap{}\n    heap.Init(h)\n    // 配列の先頭 k 個の要素をヒープに追加\n    for i := 0; i < k; i++ {\n        heap.Push(h, nums[i])\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for i := k; i < len(nums); i++ {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if nums[i] > h.Top().(int) {\n            heap.Pop(h)\n            heap.Push(h, nums[i])\n        }\n    }\n    return h\n}\n
top_k.swift
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfunc topKHeap(nums: [Int], k: Int) -> [Int] {\n    // 最小ヒープを初期化し、先頭 k 個の要素でヒープを構築する\n    var heap = Heap(nums.prefix(k))\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for i in nums.indices.dropFirst(k) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if nums[i] > heap.min()! {\n            _ = heap.removeMin()\n            heap.insert(nums[i])\n        }\n    }\n    return heap.unordered\n}\n
top_k.js
/* 要素をヒープに追加 */\nfunction pushMinHeap(maxHeap, val) {\n    // 要素を反転する\n    maxHeap.push(-val);\n}\n\n/* 要素をヒープから取り出す */\nfunction popMinHeap(maxHeap) {\n    // 要素を反転する\n    return -maxHeap.pop();\n}\n\n/* ヒープ先頭要素にアクセス */\nfunction peekMinHeap(maxHeap) {\n    // 要素を反転する\n    return -maxHeap.peek();\n}\n\n/* ヒープから要素を取り出す */\nfunction getMinHeap(maxHeap) {\n    // 要素を反転する\n    return maxHeap.getMaxHeap().map((num) => -num);\n}\n\n/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfunction topKHeap(nums, k) {\n    // 最小ヒープを初期化する\n    // 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする\n    const maxHeap = new MaxHeap([]);\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (let i = k; i < nums.length; i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // ヒープ内の要素を返す\n    return getMinHeap(maxHeap);\n}\n
top_k.ts
/* 要素をヒープに追加 */\nfunction pushMinHeap(maxHeap: MaxHeap, val: number): void {\n    // 要素を反転する\n    maxHeap.push(-val);\n}\n\n/* 要素をヒープから取り出す */\nfunction popMinHeap(maxHeap: MaxHeap): number {\n    // 要素を反転する\n    return -maxHeap.pop();\n}\n\n/* ヒープ先頭要素にアクセス */\nfunction peekMinHeap(maxHeap: MaxHeap): number {\n    // 要素を反転する\n    return -maxHeap.peek();\n}\n\n/* ヒープから要素を取り出す */\nfunction getMinHeap(maxHeap: MaxHeap): number[] {\n    // 要素を反転する\n    return maxHeap.getMaxHeap().map((num: number) => -num);\n}\n\n/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfunction topKHeap(nums: number[], k: number): number[] {\n    // 最小ヒープを初期化する\n    // 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする\n    const maxHeap = new MaxHeap([]);\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (let i = k; i < nums.length; i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // ヒープ内の要素を返す\n    return getMinHeap(maxHeap);\n}\n
top_k.dart
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nMinHeap topKHeap(List<int> nums, int k) {\n  // 最小ヒープを初期化し、配列の先頭 k 個の要素をヒープに入れる\n  MinHeap heap = MinHeap(nums.sublist(0, k));\n  // k+1 番目の要素から開始し、ヒープ長を k に保つ\n  for (int i = k; i < nums.length; i++) {\n    // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n    if (nums[i] > heap.peek()) {\n      heap.pop();\n      heap.push(nums[i]);\n    }\n  }\n  return heap;\n}\n
top_k.rs
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {\n    // BinaryHeap は最大ヒープであり、Reverse で要素の順序を反転することで最小ヒープを実現する\n    let mut heap = BinaryHeap::<Reverse<i32>>::new();\n    // 配列の先頭 k 個の要素をヒープに追加\n    for &num in nums.iter().take(k) {\n        heap.push(Reverse(num));\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for &num in nums.iter().skip(k) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if num > heap.peek().unwrap().0 {\n            heap.pop();\n            heap.push(Reverse(num));\n        }\n    }\n    heap\n}\n
top_k.c
/* 要素をヒープに追加 */\nvoid pushMinHeap(MaxHeap *maxHeap, int val) {\n    // 要素を反転する\n    push(maxHeap, -val);\n}\n\n/* 要素をヒープから取り出す */\nint popMinHeap(MaxHeap *maxHeap) {\n    // 要素を反転する\n    return -pop(maxHeap);\n}\n\n/* ヒープ先頭要素にアクセス */\nint peekMinHeap(MaxHeap *maxHeap) {\n    // 要素を反転する\n    return -peek(maxHeap);\n}\n\n/* ヒープから要素を取り出す */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // ヒープ内のすべての要素を反転して res 配列に格納\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n/* ヒープから要素を取り出す */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // ヒープ内のすべての要素を反転して res 配列に格納\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n// ヒープに基づいて配列中の最大の k 個の要素を求める関数\nint *topKHeap(int *nums, int sizeNums, int k) {\n    // 最小ヒープを初期化する\n    // 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする\n    int *empty = (int *)malloc(0);\n    MaxHeap *maxHeap = newMaxHeap(empty, 0);\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (int i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (int i = k; i < sizeNums; i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    int *res = getMinHeap(maxHeap);\n    // メモリを解放する\n    delMaxHeap(maxHeap);\n    return res;\n}\n
top_k.kt
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfun topKHeap(nums: IntArray, k: Int): Queue<Int> {\n    // 最小ヒープを初期化\n    val heap = PriorityQueue<Int>()\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (i in 0..<k) {\n        heap.offer(nums[i])\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (i in k..<nums.size) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > heap.peek()) {\n            heap.poll()\n            heap.offer(nums[i])\n        }\n    }\n    return heap\n}\n
top_k.rb
### ヒープに基づいて配列中の最大 k 個の要素を探す ###\ndef top_k_heap(nums, k)\n  # 最小ヒープを初期化する\n  # 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする\n  max_heap = MaxHeap.new([])\n\n  # 配列の先頭 k 個の要素をヒープに追加\n  for i in 0...k\n    push_min_heap(max_heap, nums[i])\n  end\n\n  # k+1 番目の要素から開始し、ヒープ長を k に保つ\n  for i in k...nums.length\n    # 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n    if nums[i] > peek_min_heap(max_heap)\n      pop_min_heap(max_heap)\n      push_min_heap(max_heap, nums[i])\n    end\n  end\n\n  get_min_heap(max_heap)\nend\n
コードの可視化

全画面で見る >

合計で \\(n\\) 回のヒープ挿入と取り出しを行い、ヒープの最大長は \\(k\\) であるため、時間計算量は \\(O(n \\log k)\\) です。この方法は非常に効率が高く、\\(k\\) が小さいときは時間計算量が \\(O(n)\\) に近づき、\\(k\\) が大きいときでも \\(O(n \\log n)\\) を超えることはありません。

さらに、この方法は動的データストリームの利用シーンにも適しています。データが継続的に追加される場合でも、ヒープ内の要素を保ち続けることで、最大の \\(k\\) 個の要素を動的に更新できます。

","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_hello_algo/","level":1,"title":"はじめに","text":"

数年前、私は LeetCode 中国版で「剣指 Offer」シリーズの解説を共有し、多くの読者から励ましと支持をいただきました。読者との交流の中で、最もよく聞かれた質問の一つが「アルゴリズムをどう学び始めればよいか」でした。次第に、私はこの問題に強い関心を抱くようになりました。

手探りでひたすら問題を解くことは、最も人気のある方法のようです。単純で、直接的で、しかも効果的です。しかし問題演習は「マインスイーパー」を遊ぶことに似ており、独学力の高い人は地雷を一つずつうまく取り除ける一方で、基礎が十分でない人は大きな痛手を受け、挫折の中で少しずつ後退してしまいがちです。教材を通読するのもよくある方法ですが、就職を目指す人にとっては、卒業論文、履歴書の提出、筆記試験や面接の準備ですでに大半の力を使い果たしており、分厚い本を読み込むことはしばしば困難な挑戦になってしまいます。

もしあなたも同じような悩みを抱えているなら、この本があなたのもとに“たどり着いた”のは幸運なことです。本書は、この問いに対して私が示す答えです。たとえ最良の解ではなくても、少なくとも前向きな試みではあります。本書だけで直接内定を得られるわけではありませんが、データ構造とアルゴリズムの「知識地図」を探る手助けをし、さまざまな「地雷」の形や大きさ、分布を理解し、いろいろな「地雷除去の方法」を身につけられるよう導きます。こうした力があれば、問題演習や文献読解をより自在に進め、やがて完整な知識体系を築いていけると信じています。

私はファインマン教授の言葉に深く賛同しています。「Knowledge isn't free. You have to pay attention.」この意味において、本書は完全に「無料」ではありません。本書のためにあなたが払ってくれる貴重な「注意」に応えるため、私はできる限りの力を尽くし、最大限の「注意」を注いで本書を書き上げます。

私は自らの学識と力量の浅さをよく承知しています。本書の内容はある程度磨きをかけてきたものの、なお多くの誤りが残っているはずです。先生方、そして学習者の皆さまからのご批判とご指摘を心よりお願いいたします。

Hello、アルゴリズム!

コンピュータの登場は世界に大きな変革をもたらしました。高速な計算能力と優れたプログラム可能性によって、アルゴリズムの実行とデータ処理の理想的な媒体となったのです。ビデオゲームのリアルな映像、自動運転の知的な意思決定、AlphaGo の見事な対局、ChatGPT の自然な対話に至るまで、これらの応用はいずれもコンピュータ上でアルゴリズムが巧みに表現されたものです。

実際には、コンピュータが誕生する以前から、アルゴリズムとデータ構造は世界のいたるところに存在していました。初期のアルゴリズムは比較的単純で、たとえば古代の数え方や道具作りの手順などがそれに当たります。文明の進歩とともに、アルゴリズムは次第により精緻で複雑なものになっていきました。匠の巧みな技から、生産力を解放する工業製品、さらには宇宙の運行を支配する科学法則に至るまで、ほとんどあらゆる平凡なもの、あるいは驚嘆すべきものの背後には、精妙なアルゴリズムの思想が潜んでいます。

同様に、データ構造も至るところに存在します。社会ネットワークのような大きなものから地下鉄路線のような小さなものまで、多くのシステムは「グラフ」としてモデル化できます。国家のような大きな単位から家庭のような小さな単位まで、社会の主な組織形態には「木」の特徴があります。冬服は「スタック」のように、最初に着たものが最後に脱がれます。バドミントンシャトルの筒は「キュー」のように、一方から入れて他方から取り出します。辞書は「ハッシュテーブル」のようなもので、目的の見出し語を素早く探せます。

本書は、わかりやすいアニメーション図解と実行可能なコード例を通じて、読者がアルゴリズムとデータ構造の核心概念を理解し、さらにプログラミングによってそれらを実装できるようになることを目指しています。そのうえで、本書は複雑な世界の中にあるアルゴリズムの生き生きとした現れを明らかにし、アルゴリズムの美しさを示そうとしています。本書があなたの助けになれば幸いです。

","path":["はじめに"],"tags":[]},{"location":"chapter_introduction/","level":1,"title":"第 1 章   アルゴリズム入門","text":"

Abstract

一人の少女が軽やかに舞い、データと織り重なり合いながら、スカートの裾にはアルゴリズムの旋律がたなびいています。

彼女はあなたをこの舞いへと誘います。その足取りに続いて、論理と美しさに満ちたアルゴリズムの世界へ踏み入りましょう。

","path":["第 1 章   アルゴリズムを知る","第 1 章   アルゴリズム入門"],"tags":[]},{"location":"chapter_introduction/#_1","level":2,"title":"章の内容","text":"
  • 1.1   アルゴリズムは至るところにある
  • 1.2   アルゴリズムとは
  • 1.3   まとめ
","path":["第 1 章   アルゴリズムを知る","第 1 章   アルゴリズム入門"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   アルゴリズムは至るところにある","text":"

「アルゴリズム」という言葉を聞くと、自然に数学を思い浮かべます。しかし実際には、多くのアルゴリズムは複雑な数学を必要とせず、むしろ基本的な論理に依存しており、その論理は私たちの日常生活のいたるところで見られます。

アルゴリズムを本格的に議論する前に、ひとつ面白い事実を共有しておきます。あなたはすでに知らず知らずのうちに多くのアルゴリズムを身につけ、それらを日常生活に応用することに慣れているのです。以下では、いくつかの具体例を挙げてこれを示します。

例1:辞書を引く。辞書では、各漢字に対応するピンインがあり、辞書はピンインのアルファベット順に並んでいます。ピンインの先頭文字が \\(r\\) の字を探すと仮定すると、通常は次の図のような方法で行います。

  1. 辞書をおよそ半分のところまで開き、そのページの先頭文字を確認し、先頭文字が \\(m\\) だとします。
  2. ピンインのアルファベット表では \\(r\\) は \\(m\\) の後にあるため、辞書の前半を除外し、探索範囲を後半に絞ります。
  3. ピンインの先頭文字が \\(r\\) のページを見つけるまで、手順 1. と手順 2. を繰り返します。
<1><2><3><4><5>

図 1-1   辞書を引く手順

辞書を引くという小学生の必須スキルは、実は有名な「二分探索」アルゴリズムそのものです。データ構造の観点では、辞書を整列済みの「配列」とみなせます。アルゴリズムの観点では、上記の一連の辞書引きの操作を「二分探索」とみなせます。

例2:トランプを整理する。カードゲームをするとき、毎回手札のトランプを小さい順に並べ替える必要があります。その流れは次の図のとおりです。

  1. トランプを「整列済み」と「未整列」の2つの部分に分け、初期状態では一番左の1枚がすでに整列済みだとします。
  2. 未整列部分から1枚のトランプを取り出し、整列済み部分の正しい位置に挿入します。完了すると、左端の2枚は整列済みになります。
  3. 手順 2. を繰り返し、各ラウンドで未整列部分から1枚を整列済み部分へ挿入し、すべてのトランプが整列済みになるまで続けます。

図 1-2   トランプを並べ替える手順

上記のトランプ整理の方法は、本質的には「挿入ソート」アルゴリズムです。これは小規模なデータ集合を処理する際に非常に効率的で、多くのプログラミング言語のソートライブラリ関数にも挿入ソートが使われています。

例3:お釣りを出す。スーパーで \\(69\\) 元の商品を購入し、店員に \\(100\\) 元渡したとすると、店員は \\(31\\) 元のお釣りを返す必要があります。店員は自然に次の図のような考え方をします。

  1. 選択肢は \\(31\\) 元より小さい額面の貨幣で、\\(1\\) 元、\\(5\\) 元、\\(10\\) 元、\\(20\\) 元があります。
  2. 選択肢の中から最大の \\(20\\) 元を取り出すと、残りは \\(31 - 20 = 11\\) 元です。
  3. 残りの選択肢の中から最大の \\(10\\) 元を取り出すと、残りは \\(11 - 10 = 1\\) 元です。
  4. 残りの選択肢の中から最大の \\(1\\) 元を取り出すと、残りは \\(1 - 1 = 0\\) 元です。
  5. お釣りは完了し、内訳は \\(20 + 10 + 1 = 31\\) 元です。

図 1-3   お釣りの過程

以上の手順では、各ステップでその時点で最善と思われる選択肢を取っています。つまり、できるだけ額面の大きい貨幣を使い、最終的に実行可能なお釣りの方案を得ています。データ構造とアルゴリズムの観点から見ると、この方法は本質的に「貪欲法」です。

料理を一品作ることから星間航行に至るまで、ほとんどあらゆる問題の解決にアルゴリズムは欠かせません。コンピュータの登場によって、プログラミングを通じてデータ構造をメモリに格納し、さらにコードを書いて CPU や GPU にアルゴリズムを実行させることが可能になりました。こうして、生活の中の問題をコンピュータに移し、より効率的な方法でさまざまな複雑な問題を解決できるのです。

Tip

データ構造、アルゴリズム、配列、二分探索といった概念がまだ少し曖昧でも、そのまま読み進めてください。本書がデータ構造とアルゴリズムの知識の世界へと案内します。

","path":["第 1 章   アルゴリズムを知る","1.1   アルゴリズムは至るところにある"],"tags":[]},{"location":"chapter_introduction/summary/","level":1,"title":"1.3   まとめ","text":"","path":["第 1 章   アルゴリズムを知る","1.3   まとめ"],"tags":[]},{"location":"chapter_introduction/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • アルゴリズムは日常生活の至る所にあり、決して手の届かない難解な知識ではありません。実際、私たちは気づかないうちに多くのアルゴリズムを身につけ、生活のさまざまな問題を解決しています。
  • 辞書を引く原理は二分探索アルゴリズムと一致しています。二分探索アルゴリズムは分割統治という重要なアルゴリズム思想を体現しています。
  • トランプを整理する過程は挿入ソートアルゴリズムと非常によく似ています。挿入ソートアルゴリズムは小規模なデータ集合のソートに適しています。
  • 貨幣の釣り銭を求める手順の本質は貪欲アルゴリズムであり、各ステップでその時点で最善と思われる選択を取ります。
  • アルゴリズムとは、限られた時間内に特定の問題を解決するための一連の命令または操作手順であり、データ構造とは、コンピュータ内でデータを組織し保存する方法です。
  • データ構造とアルゴリズムは密接に結びついています。データ構造はアルゴリズムの土台であり、アルゴリズムはデータ構造に生命を吹き込みます。
  • データ構造とアルゴリズムは積み木の組み立てにたとえることができます。積み木はデータを表し、積み木の形や接続方法などはデータ構造を表し、積み木を組み立てる手順がアルゴリズムに対応します。
","path":["第 1 章   アルゴリズムを知る","1.3   まとめ"],"tags":[]},{"location":"chapter_introduction/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:プログラマーとして、私は日常業務でアルゴリズムを使って問題を解決したことがありません。よく使うアルゴリズムはプログラミング言語にすべてカプセル化されており、そのまま使えばよいです。これは、仕事上の問題がまだアルゴリズムを必要とする段階に達していないことを意味するのでしょうか?

具体的な仕事のスキルを武術の「型」にたとえるなら、基礎科目はむしろ「内功」に近いものです。

私は、アルゴリズム(およびその他の基礎科目)を学ぶ意義は、仕事でそれをゼロから実装することではなく、学んだ知識に基づいて問題解決の際に専門的な反応や判断を下せるようになり、その結果として仕事全体の品質を高めることにあると考えています。簡単な例を挙げると、どのプログラミング言語にもソート関数が組み込まれています。

  • もしデータ構造とアルゴリズムを学んでいなければ、どんなデータが与えられても、そのソート関数に任せてしまうかもしれません。問題なく動き、性能も悪くなく、一見すると特に問題はありません。
  • しかしアルゴリズムを学んでいれば、組み込みのソート関数の時間計算量が \\(O(n \\log n)\\) であることを知っています。さらに、与えられたデータが固定桁数の整数(例えば学籍番号)であれば、より効率の高い「基数ソート」を使って、時間計算量を \\(O(nk)\\) に下げることができます。ここで \\(k\\) は桁数です。データ量が非常に大きい場合、節約できた実行時間は大きな価値を生みます(コスト削減、体験向上など)。

工学分野では、多くの問題で最適解に到達することは難しく、少なくない問題は「だいたい」解決されているにすぎません。問題の難しさは、一方では問題そのものの性質に依存し、他方ではそれを観測する人の知識の蓄積にも依存します。知識が充実し、経験が豊富であるほど、問題分析はより深くなり、問題はより洗練された形で解決できるようになります。

","path":["第 1 章   アルゴリズムを知る","1.3   まとめ"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/","level":1,"title":"1.2   アルゴリズムとは","text":"","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#121","level":2,"title":"1.2.1   アルゴリズムの定義","text":"

アルゴリズム(algorithm)とは、限られた時間内に特定の問題を解決するための一連の命令または操作手順であり、次のような特徴を持ちます。

  • 問題が明確であり、入力と出力の定義がはっきりしています。
  • 実行可能であり、有限の手順、時間、メモリ空間で完了できます。
  • 各手順の意味が確定しており、同じ入力と実行条件では常に同じ出力になります。
","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#122","level":2,"title":"1.2.2   データ構造の定義","text":"

データ構造(data structure)とは、データを整理して保存する方式であり、データの内容、データ間の関係、データの操作方法を含み、次のような設計目標があります。

  • 使用する空間をできるだけ少なくし、コンピュータのメモリを節約します。
  • データの操作をできるだけ高速にし、アクセス、追加、削除、更新などを含みます。
  • 簡潔なデータ表現と論理情報を提供し、アルゴリズムが効率よく動作できるようにします。

データ構造の設計はトレードオフに満ちた過程です。ある面を改善したい場合、別の面で妥協が必要になることがよくあります。以下に 2 つの例を示します。

  • 連結リストは配列に比べてデータの追加や削除がしやすい一方で、データアクセス速度を犠牲にしています。
  • グラフは連結リストに比べてより豊富な論理情報を提供しますが、より大きなメモリ空間を必要とします。
","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123","level":2,"title":"1.2.3   データ構造とアルゴリズムの関係","text":"

以下の図のように、データ構造とアルゴリズムは高度に関連し、密接に結び付いており、具体的には次の 3 つの点に表れます。

  • データ構造はアルゴリズムの土台です。データ構造はアルゴリズムに対して、構造化して格納されたデータと、そのデータを操作する方法を提供します。
  • アルゴリズムはデータ構造に命を吹き込みます。データ構造そのものはデータ情報を保存するだけであり、アルゴリズムと組み合わせて初めて特定の問題を解決できます。
  • アルゴリズムは通常、異なるデータ構造に基づいて実装できますが、実行効率が大きく異なる場合があり、適切なデータ構造を選ぶことが重要です。

図 1-4   データ構造とアルゴリズムの関係

データ構造とアルゴリズムは、以下の図に示す組み立てブロックのようなものです。1 セットのブロックには多くの部品が含まれるだけでなく、詳しい組み立て説明書も付いています。説明書に従って一歩ずつ操作すれば、精巧なブロック模型を組み立てられます。

図 1-5   組み立てブロック

両者の詳細な対応関係を次の表に示します。

表 1-1   データ構造とアルゴリズムを組み立てブロックにたとえる

データ構造とアルゴリズム 組み立てブロック 入力データ まだ組み立てていないブロック データ構造 ブロックの構成形式。形状、大きさ、接続方法などを含む アルゴリズム ブロックを目標の形に組み上げる一連の操作手順 出力データ ブロック模型

特筆すべき点として、データ構造とアルゴリズムはプログラミング言語から独立しています。だからこそ、本書では複数のプログラミング言語に基づく実装を提供できます。

慣習的な略称

実際の議論では、私たちは通常「データ構造とアルゴリズム」を略して「アルゴリズム」と呼びます。たとえば広く知られている LeetCode のアルゴリズム問題は、実際にはデータ構造とアルゴリズムの両方の知識を同時に問うています。

","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_paperbook/","level":1,"title":"紙の書籍","text":"

長い時間をかけて磨き上げた『Hello アルゴリズム』の紙の書籍が、ついに発売されました!今の気持ちは、次の一節で表せます:

風を追い月を追って立ち止まるな、草原の果てには春の山がある。

以下の動画では紙の書籍を紹介しており、私の考えもいくつか含まれています:

  • データ構造とアルゴリズムを学ぶ重要性。
  • なぜ紙の書籍で Python を選んだのか。
  • 知識共有に対する理解。

新人 UP 主ですので、ぜひ応援と高評価・チャンネル登録をお願いします~ありがとうございます!

紙の書籍のスナップショット:

","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_2","level":2,"title":"長所と短所","text":"

紙の書籍ならではの魅力を、簡単にまとめると次のとおりです:

  • フルカラー印刷を採用し、本書の「アニメーション図解」の強みをそのまま活かせます。
  • 紙の素材にもこだわり、色彩を高い精度で再現しつつ、紙の書籍ならではの質感も残しています。
  • 紙の書籍版は Web 版よりも書式が整っており、たとえば図中の数式には斜体を用いています。
  • 価格を上げずに、マインドマップの折り込みページやしおりも付属します。
  • 紙の書籍、Web 版、PDF 版で内容は同期しており、自由に切り替えて読めます。

Tip

紙の書籍と Web 版を同期させるのは難しいため、細かな違いが生じる場合があります。ご了承ください!

もちろん、購入前に検討しておくべき点もいくつかあります:

  • Python 言語を使用しているため、あなたの主言語と合わない可能性があります(Python は疑似コードと捉え、考え方の理解を重視してください)。
  • フルカラー印刷は図解やコードの読みやすさを大きく高める一方で、白黒印刷より価格はやや高くなります。

Tip

「印刷品質」と「価格」は、アルゴリズムにおける「時間効率」と「空間効率」のようなもので、両立は容易ではありません。そして私は、「印刷品質」は「時間効率」に当たるため、より重視すべきだと考えています。

","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_3","level":2,"title":"購入リンク","text":"

紙の書籍に興味があれば、ぜひ一冊ご検討ください。新刊の 5 割引を用意していただきましたので、こちらのリンクをご覧いただくか、以下の QR コードをスキャンしてください:

","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_4","level":2,"title":"あとがき","text":"

当初、私は紙の書籍出版に必要な作業量を甘く見ていて、オープンソースプロジェクトをきちんと保守していれば、紙の書籍版も何らかの自動化手段で生成できると思っていました。実際には、紙の書籍の制作フローとオープンソースプロジェクトの更新の仕組みには大きな違いがあり、その間をつなぐには多くの追加作業が必要でした。

一冊の本の初稿と出版基準を満たす完成稿との間には、なお大きな隔たりがあります。出版社(企画、編集、デザイン、マーケティングなど)と著者が力を合わせ、長い時間をかけて磨き上げていく必要があります。ここで、図霊の企画編集者である王軍花さん、そして人民郵電出版社と図霊コミュニティで本書の出版工程に携わってくださったすべての皆さまに感謝いたします!

この本があなたの助けになれば幸いです!

","path":["紙の書籍"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"第 0 章   はじめに","text":"

Abstract

アルゴリズムは美しい交響曲のようであり、コードの一行一行が旋律のように流れていきます。

この本があなたの心の中でそっと響き、独自で深い旋律を残してくれることを願っています。

","path":["第 0 章   前書き","第 0 章   はじめに"],"tags":[]},{"location":"chapter_preface/#_1","level":2,"title":"章の内容","text":"
  • 0.1   本書について
  • 0.2   本書の使い方
  • 0.3   まとめ
","path":["第 0 章   前書き","第 0 章   はじめに"],"tags":[]},{"location":"chapter_preface/about_the_book/","level":1,"title":"0.1   本書について","text":"

本プロジェクトは、オープンソースで無料、かつ初心者にやさしいデータ構造とアルゴリズムの入門書を作ることを目的としています。

  • 全編でアニメーション付きの図解を採用し、内容は明快で理解しやすく、学習曲線もなだらかで、初心者がデータ構造とアルゴリズムの知識地図を探求できるよう導きます。
  • ソースコードはワンクリックで実行でき、読者が演習を通じてプログラミング能力を高め、アルゴリズムの動作原理とデータ構造の内部実装を理解する助けとなります。
  • 読者どうしの助け合いによる学習を推奨しており、コメント欄で質問や見解を共有し、対話と議論を通じてともに成長していくことを歓迎します。
","path":["第 0 章   前書き","0.1   本書について"],"tags":[]},{"location":"chapter_preface/about_the_book/#011","level":2,"title":"0.1.1   対象読者","text":"

もしあなたがアルゴリズム初心者で、これまでアルゴリズムに触れたことがない、あるいはすでに多少の問題演習の経験はあるものの、データ構造とアルゴリズムについてはまだ曖昧な理解にとどまり、できるかできないかの間を行き来しているなら、本書はまさにあなたのために作られています!

もしすでに一定量の問題演習を積み、ほとんどの問題パターンに慣れているなら、本書はアルゴリズム知識体系の復習と整理に役立ちます。リポジトリのソースコードは「問題演習ツール集」や「アルゴリズム辞典」として活用できます。

もしあなたがアルゴリズムの「達人」なら、貴重なご提案をいただけることを楽しみにしています。あるいは一緒に執筆に参加してください。

前提条件

少なくともいずれか一つの言語でのプログラミング基礎があり、簡単なコードを読んだり書いたりできる必要があります。

","path":["第 0 章   前書き","0.1   本書について"],"tags":[]},{"location":"chapter_preface/about_the_book/#012","level":2,"title":"0.1.2   内容構成","text":"

本書の主な内容は以下の図のとおりです。

  • 計算量解析:データ構造とアルゴリズムを評価する観点と方法。時間計算量と空間計算量の求め方、代表的な種類、例など。
  • データ構造:基本データ型とデータ構造の分類方法。配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなどのデータ構造の定義、長所と短所、基本操作、代表的な種類、典型的な応用、実装方法など。
  • アルゴリズム:探索、ソート、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムの定義、長所と短所、効率、適用場面、問題を解く手順、例題など。

図 0-1   本書の主な内容

","path":["第 0 章   前書き","0.1   本書について"],"tags":[]},{"location":"chapter_preface/about_the_book/#013","level":2,"title":"0.1.3   謝辞","text":"

本書は、オープンソースコミュニティの多くの貢献者による共同の努力のもとで、継続的に改善されています。時間と労力を注いでくださったすべての執筆者の皆さんに感謝します。お名前は次のとおりです(GitHub により自動生成された順序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、IsChristina、xBLACKICEx、guowei-gong、Cathay-Chen、pengchzn、QiLOL、magentaqin、hello-ikun、JoseHung、qualifier1024、thomasq0、sunshinesDL、L-Super、Guanngxu、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、Shyam-Chen、theNefelibatas、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、MolDuM、Nigh、Dr-XYZ、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、beatrix-chan、DullSword、xjr7670、jiaxianhua、qq909244296、iStig、boloboloda、hts0000、gledfish、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、llql1211、fbigm、echo1937、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、GaochaoZhu、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Allen-Scai、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、zhongfq、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Keynman、KeiichiKasai と KawaiiAsh。

本書のコードレビューは coderonion、curtishd、Gonglja、gvenusleo、hpstory、justin-tse、khoaxuantu、krahets、night-cruise、nuomi1、Reanon と rongyi によって行われました(アルファベット順)。彼らが費やしてくれた時間と労力に感謝します。各言語のコードの規範性と統一性が保たれているのは、まさに彼らのおかげです。

本書の繁体字中国語版は Shyam-Chen と Dr-XYZ がレビューし、英語版は yuelinxin、K3v123、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn、thomasq0 と magentaqin がレビューし、日本語版は eltociear がレビューしました。彼らの継続的な貢献があってこそ、本書はより幅広い読者に届けられています。感謝いたします。

本書の ePub 電子書籍生成ツールは zhongfq によって開発されました。彼の貢献に感謝します。読者により自由な読書方法を提供してくれました。

本書の執筆過程で、私は多くの方々の助けを得ました。

  • 会社での私の指導教員である李汐博士に感謝します。ある対話の中で「すぐに行動しよう」と励ましてくださり、この本を書く決意を固めることができました;
  • 私の恋人であり、本書の最初の読者でもある泡泡に感謝します。アルゴリズム初心者の視点から多くの貴重な提案をしてくれたおかげで、本書はより初心者に適したものになりました;
  • 腾宝、琦宝、飞宝が本書に創造性あふれる名前を付けてくれたことに感謝します。みんなが最初のコード行「Hello World!」を書いた美しい記憶を呼び起こしてくれました;
  • 校铨が知的財産の面で専門的な支援をしてくれたことに感謝します。これは本オープンソース書籍の改善に重要な役割を果たしました;
  • 苏潼が本書の美しい表紙と logo をデザインし、私の完璧主義につき合って何度も辛抱強く修正してくれたことに感謝します;
  • @squidfunk が組版に関する助言を提供してくれたこと、そして彼が開発したオープンソースのドキュメントテーマ Material-for-MkDocs に感謝します。

執筆の過程で、私はデータ構造とアルゴリズムに関する多くの教材や記事を読みました。これらの作品は本書に優れた手本を与え、本書の内容の正確性と品質を支えてくれました。ここに、すべての先生方と先人たちの卓越した貢献に感謝します!

本書は手と頭を同時に使う学習方法を提唱しています。この点で私は『手を動かして学ぶ深層学習』から大きな啓発を受けました。ここで読者の皆さんにこの優れた著作を強くお勧めします。

心から両親に感謝します。いつも支え励ましてくれたからこそ、私はこの興味深いことに取り組む機会を得ることができました。

","path":["第 0 章   前書き","0.1   本書について"],"tags":[]},{"location":"chapter_preface/suggestions/","level":1,"title":"0.2   本書の使い方","text":"

Tip

最適な読書体験を得るために、本節の内容を一通り読むことをおすすめします。

","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#021","level":2,"title":"0.2.1   文章スタイルの約束","text":"
  • 見出しの後に * が付いているのは選読章で、内容は比較的難しめです。時間が限られている場合は、先に読み飛ばしてもかまいません。
  • 専門用語は太字(紙書籍版と PDF 版)または下線付き(Web 版)で示します。たとえば配列(array)のようなものです。文献を読む際に役立つため、覚えておくことをおすすめします。
  • 重要な内容やまとめの文は 太字 で示します。これらの文章には特に注意してください。
  • 特定の意味を持つ語句には“引用符”を付け、曖昧さを避けます。
  • プログラミング言語ごとに用語が一致しない場合、本書では Python を基準とします。たとえば、“空”を表すのに None を使います。
  • 本書では、よりコンパクトなレイアウトのために、言語ごとのコメント規約を一部省略しています。コメントは主に3種類あります。タイトルコメント、内容コメント、複数行コメントです。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
\"\"\"タイトルコメント。関数、クラス、テストケースなどを示すために使います\"\"\"\n\n# 内容コメント。コードを詳しく説明するために使います\n\n\"\"\"\n複数行\nコメント\n\"\"\"\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n// 複数行\n// コメント\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
### タイトルコメント。関数、クラス、テストケースなどを示すために使います ###\n\n# 内容コメント。コードを詳しく説明するために使います\n\n# 複数行\n# コメント\n
","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#022","level":2,"title":"0.2.2   アニメーション図解で効率よく学ぶ","text":"

文字と比べて、動画や画像は情報密度と構造化の度合いが高く、理解しやすいものです。本書では、重要かつ難解な知識は主にアニメーションによる図解で示し、文章は説明と補足を担います。

本書を読んでいて、ある内容に以下の図のようなアニメーション図解がある場合は、図を主、文章を従として、両方を合わせて理解してください。

図 0-2   アニメーション図解の例

","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#023","level":2,"title":"0.2.3   コード実践で理解を深める","text":"

本書のサンプルコードは GitHub リポジトリ で管理されています。以下の図のように、ソースコードにはテストケースが付いており、ワンクリックで実行できます。

時間に余裕があれば、コードを見ながら自分で一度書いてみることをおすすめします。学習時間が限られている場合でも、少なくともすべてのコードに目を通し、実行してください。

コードを読むのに比べて、書く過程のほうが得られるものは多いものです。手を動かしてこそ、本当に学んだことになります。

図 0-3   コード実行例

コードを実行する前準備は主に3ステップです。

第1ステップ:ローカルのプログラミング環境をインストールする。付録のチュートリアルを参照してインストールしてください。すでにインストール済みであれば、この手順は省略できます。

第2ステップ:コードリポジトリをクローンまたはダウンロードする。 GitHub リポジトリ にアクセスしてください。すでに Git をインストールしている場合は、次のコマンドでこのリポジトリをクローンできます:

git clone https://github.com/krahets/hello-algo.git\n

もちろん、以下の図に示す場所で“Download ZIP”ボタンをクリックし、コードの圧縮ファイルを直接ダウンロードしてローカルで展開することもできます。

図 0-4   リポジトリのクローンとコードのダウンロード

第3ステップ:ソースコードを実行する。以下の図のように、上部にファイル名が表示されているコードブロックについては、リポジトリの codes フォルダ内に対応するソースコードファイルがあります。ソースコードファイルはワンクリックで実行できるため、不要なデバッグ時間を減らし、学習内容に集中できます。

図 0-5   コードブロックと対応するソースコードファイル

ローカルでコードを実行するだけでなく、Web 版では Python コードの可視化実行にも対応しています(pythontutor を利用)。以下の図のように、コードブロックの下にある“可視化実行”をクリックすると表示を展開し、アルゴリズムコードの実行過程を観察できます。また、“全画面表示”をクリックすると、より見やすい閲覧体験が得られます。

図 0-6   Python コードの可視化実行

","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#024","level":2,"title":"0.2.4   質問と議論を通じてともに成長する","text":"

本書を読んでいて、理解できていない知識点を安易に読み飛ばさないでください。コメント欄で気軽に質問してください。私と仲間たちが誠意をもって回答し、通常は 2 日以内に返信します。

以下の図のように、Web 版では各章の下部にコメント欄があります。ぜひコメント欄の内容にも目を通してください。一方では、みんなが直面した問題を知ることで知識の抜けを補い、より深い思考を促せます。もう一方では、ほかの仲間の質問にも積極的に答え、見解を共有し、互いの成長を助けてほしいと思います。

図 0-7   コメント欄の例

","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#025","level":2,"title":"0.2.5   アルゴリズム学習ロードマップ","text":"

全体として見ると、データ構造とアルゴリズムの学習過程は 3 つの段階に分けられます。

  1. 第 1 段階:アルゴリズム入門。さまざまなデータ構造の特徴と使い方に慣れ、異なるアルゴリズムの原理、流れ、用途、効率などを学ぶ必要があります。
  2. 第 2 段階:アルゴリズム問題を解く。まずは人気の高い問題から取り組み、少なくとも 100 問は蓄積して、主流のアルゴリズム問題に慣れることをおすすめします。最初のうちは、“知識の忘却”が課題になるかもしれませんが、心配はいりません。これはごく自然なことです。“エビングハウスの忘却曲線”に沿って問題を復習すれば、通常は 3~5 回繰り返すことでしっかり記憶に定着します。おすすめの問題リストと学習計画は、この GitHub リポジトリ を参照してください。
  3. 第 3 段階:知識体系を構築する。学習面では、アルゴリズムの連載記事、解法フレームワーク、教材などを読むことで、知識体系を継続的に充実させられます。問題演習の面では、トピック別分類、1 問多解、1 解多題といった発展的な戦略も試せます。関連する学習ノウハウは各コミュニティで見つけられます。

以下の図のように、本書の内容は主に“第 1 段階”を扱っており、第 2 段階と第 3 段階の学習をより効率的に進める助けとなることを目的としています。

図 0-8   アルゴリズム学習ロードマップ

","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/summary/","level":1,"title":"0.3   まとめ","text":"","path":["第 0 章   前書き","0.3   まとめ"],"tags":[]},{"location":"chapter_preface/summary/#1","level":3,"title":"1.   重要ポイントの振り返り","text":"
  • 本書の主な対象読者はアルゴリズム初学者です。すでにある程度の基礎がある場合でも、本書はアルゴリズム知識を体系的に振り返る助けとなり、書中のソースコードは「問題演習用ツール集」としても利用できます。
  • 本書の内容は主に計算量解析、データ構造、アルゴリズムの三部からなり、この分野の大部分のテーマを網羅しています。
  • アルゴリズム初心者にとって、学習初期の段階で入門書を読むことは非常に重要であり、多くの遠回りを避けられます。
  • 本書のアニメーション図解は通常、重要な知識や難しい知識を紹介するために用いられます。本書を読む際は、これらの内容により多く注意を払うべきです。
  • 実践はプログラミングを学ぶ最良の方法です。ソースコードを実行し、実際に自分でコードを書くことを強く勧めます。
  • 本書のWeb版の各章にはコメント欄が設けられており、疑問や見解をいつでも共有することを歓迎します。
","path":["第 0 章   前書き","0.3   まとめ"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"参考文献","text":"

[1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition).

[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

[3] Robert Sedgewick, et al. Algorithms (4th Edition).

[4] 严蔚敏. データ構造(C 言語版).

[5] 邓俊辉. データ構造(C++ 言語版、第3版).

[6] マーク・アレン・ワイス著,陈越訳. データ構造とアルゴリズム解析:Java言語による記述(第3版).

[7] 程杰. データ構造の話.

[8] 王争. データ構造とアルゴリズムの美.

[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

[10] Aston Zhang, et al. Dive into Deep Learning.

","path":["参考文献"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"第 10 章   探索","text":"

Abstract

探索は未知の冒険であり、私たちは神秘的な空間の隅々まで歩き回る必要があるかもしれず、あるいは素早く目標を特定できるかもしれません。

この探索の旅において、すべての探求が思いもよらなかった答えをもたらすかもしれません。

","path":["第 10 章   探索"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"章の内容","text":"
  • 10.1   二分探索
  • 10.2   二分探索の挿入位置
  • 10.3   二分探索の境界
  • 10.4   ハッシュによる最適化戦略
  • 10.5   探索アルゴリズム再考
  • 10.6   まとめ
","path":["第 10 章   探索"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   二分探索","text":"

二分探索(binary search)は分割統治法に基づく効率的な探索アルゴリズムです。データが整列済みである性質を利用し、各ラウンドで探索範囲を半分に縮小し、目標要素を見つけるか探索区間が空になるまで続けます。

Question

長さ \\(n\\) の配列 nums が与えられます。要素は小さい順に並んでおり、重複しません。要素 target がこの配列内にある場合はそのインデックスを返し、含まれない場合は \\(-1\\) を返してください。例を次の図に示します。

図 10-1   二分探索の例

次の図に示すように、まずポインタ \\(i = 0\\) と \\(j = n - 1\\) を初期化し、それぞれ配列の先頭要素と末尾要素を指すようにして、探索区間 \\([0, n - 1]\\) を表します。角括弧は閉区間を表し、境界値自体を含むことに注意してください。

次に、以下の 2 つの手順を繰り返します。

  1. 中央のインデックス \\(m = \\lfloor {(i + j) / 2} \\rfloor\\) を計算します。ここで \\(\\lfloor \\: \\rfloor\\) は切り捨てを表します。
  2. nums[m]target の大小関係を判定し、次の 3 つの場合に分かれます。
    1. nums[m] < target のとき、target は区間 \\([m + 1, j]\\) にあるため、\\(i = m + 1\\) を実行します。
    2. nums[m] > target のとき、target は区間 \\([i, m - 1]\\) にあるため、\\(j = m - 1\\) を実行します。
    3. nums[m] = target のとき、target が見つかったので、インデックス \\(m\\) を返します。

配列に目標要素が含まれない場合、探索区間は最終的に空まで縮小されます。このとき \\(-1\\) を返します。

<1><2><3><4><5><6><7>

図 10-2   二分探索の流れ

注意すべき点として、\\(i\\) と \\(j\\) はどちらも int 型であるため、\\(i + j\\) が int 型の範囲を超える可能性があります。大きな数によるオーバーフローを避けるため、通常は式 \\(m = \\lfloor {i + (j - i) / 2} \\rfloor\\) を用いて中点を計算します。

コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
def binary_search(nums: list[int], target: int) -> int:\n    \"\"\"二分探索(両閉区間)\"\"\"\n    # 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    i, j = 0, len(nums) - 1\n    # ループし、探索区間が空になったら終了する(i > j で空)\n    while i <= j:\n        # 理論上、Python の数値は無限に大きくできるため(メモリ容量に依存)、大きな数のオーバーフローを考慮する必要はない\n        m = (i + j) // 2  # 中点インデックス m を計算\n        if nums[m] < target:\n            i = m + 1  # この場合、target は区間 [m+1, j] にある\n        elif nums[m] > target:\n            j = m - 1  # この場合、target は区間 [i, m-1] にある\n        else:\n            return m  # 目標要素が見つかったらそのインデックスを返す\n    return -1  # 目標要素が見つからなければ -1 を返す\n
binary_search.cpp
/* 二分探索(両閉区間) */\nint binarySearch(vector<int> &nums, int target) {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    int i = 0, j = nums.size() - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target)    // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.java
/* 二分探索(両閉区間) */\nint binarySearch(int[] nums, int target) {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    int i = 0, j = nums.length - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.cs
/* 二分探索(両閉区間) */\nint BinarySearch(int[] nums, int target) {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    int i = 0, j = nums.Length - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        int m = i + (j - i) / 2;   // 中点インデックス m を計算\n        if (nums[m] < target)      // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        else                       // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.go
/* 二分探索(両閉区間) */\nfunc binarySearch(nums []int, target int) int {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    i, j := 0, len(nums)-1\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    for i <= j {\n        m := i + (j-i)/2      // 中点インデックス m を計算\n        if nums[m] < target { // この場合、target は区間 [m+1, j] にある\n            i = m + 1\n        } else if nums[m] > target { // この場合、target は区間 [i, m-1] にある\n            j = m - 1\n        } else { // 目標要素が見つかったらそのインデックスを返す\n            return m\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.swift
/* 二分探索(両閉区間) */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while i <= j {\n        let m = i + (j - i) / 2 // 中点インデックス m を計算\n        if nums[m] < target { // この場合、target は区間 [m+1, j] にある\n            i = m + 1\n        } else if nums[m] > target { // この場合、target は区間 [i, m-1] にある\n            j = m - 1\n        } else { // 目標要素が見つかったらそのインデックスを返す\n            return m\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.js
/* 二分探索(両閉区間) */\nfunction binarySearch(nums, target) {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    let i = 0,\n        j = nums.length - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        // 中点インデックス `m` を計算し、`parseInt()` で切り捨てる\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        else if (nums[m] > target)\n            // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        else return m; // 目標要素が見つかったらそのインデックスを返す\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.ts
/* 二分探索(両閉区間) */\nfunction binarySearch(nums: number[], target: number): number {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    let i = 0,\n        j = nums.length - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        // 中点インデックス m を計算\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        } else {\n            // 目標要素が見つかったらそのインデックスを返す\n            return m;\n        }\n    }\n    return -1; // 目標要素が見つからなければ -1 を返す\n}\n
binary_search.dart
/* 二分探索(両閉区間) */\nint binarySearch(List<int> nums, int target) {\n  // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n  int i = 0, j = nums.length - 1;\n  // ループし、探索区間が空になったら終了する(i > j で空)\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // 中点インデックス m を計算\n    if (nums[m] < target) {\n      // この場合、target は区間 [m+1, j] にある\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // この場合、target は区間 [i, m-1] にある\n      j = m - 1;\n    } else {\n      // 目標要素が見つかったらそのインデックスを返す\n      return m;\n    }\n  }\n  // 目標要素が見つからなければ -1 を返す\n  return -1;\n}\n
binary_search.rs
/* 二分探索(両閉区間) */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    let mut i = 0;\n    let mut j = nums.len() as i32 - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while i <= j {\n        let m = i + (j - i) / 2; // 中点インデックス m を計算\n        if nums[m as usize] < target {\n            // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        } else {\n            // 目標要素が見つかったらそのインデックスを返す\n            return m;\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.c
/* 二分探索(両閉区間) */\nint binarySearch(int *nums, int len, int target) {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    int i = 0, j = len - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target)    // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.kt
/* 二分探索(両閉区間) */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    var i = 0\n    var j = nums.size - 1\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        val m = i + (j - i) / 2 // 中点インデックス m を計算\n        if (nums[m] < target) // この場合、target は区間 [m+1, j] にある\n            i = m + 1\n        else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある\n            j = m - 1\n        else  // 目標要素が見つかったらそのインデックスを返す\n            return m\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.rb
### 二分探索(両閉区間) ###\ndef binary_search(nums, target)\n  # 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n  i, j = 0, nums.length - 1\n\n  # ループし、探索区間が空になったら終了する(i > j で空)\n  while i <= j\n    # 理論上、Ruby の数値は無限に大きくできるため(メモリ容量に依存)、大きな数のオーバーフローを考慮する必要はない\n    m = (i + j) / 2   # 中点インデックス m を計算\n\n    if nums[m] < target\n      i = m + 1 # この場合、target は区間 [m+1, j] にある\n    elsif nums[m] > target\n      j = m - 1 # この場合、target は区間 [i, m-1] にある\n    else\n      return m  # 目標要素が見つかったらそのインデックスを返す\n    end\n  end\n\n  -1  # 目標要素が見つからなければ -1 を返す\nend\n
コードの可視化

全画面で見る >

時間計算量は \\(O(\\log n)\\) :二分探索のループでは各ラウンドで区間が半分になるため、ループ回数は \\(\\log_2 n\\) です。

空間計算量は \\(O(1)\\) :ポインタ \\(i\\) と \\(j\\) に必要なのは定数サイズの空間だけです。

","path":["第 10 章   探索","10.1   二分探索"],"tags":[]},{"location":"chapter_searching/binary_search/#1011","level":2,"title":"10.1.1   区間の表し方","text":"

上記の両閉区間のほかに、一般的な区間表現として「左閉右開」区間があり、\\([0, n)\\) と定義されます。つまり左端は含み、右端は含みません。この表現では、区間 \\([i, j)\\) は \\(i = j\\) のとき空です。

この表現に基づいて、同じ機能を持つ二分探索アルゴリズムを実装できます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
def binary_search_lcro(nums: list[int], target: int) -> int:\n    \"\"\"二分探索(左閉右開区間)\"\"\"\n    # 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    i, j = 0, len(nums)\n    # ループし、探索区間が空になったら終了する(i = j で空)\n    while i < j:\n        m = (i + j) // 2  # 中点インデックス m を計算\n        if nums[m] < target:\n            i = m + 1  # この場合、target は区間 [m+1, j) にある\n        elif nums[m] > target:\n            j = m  # この場合、target は区間 [i, m) にある\n        else:\n            return m  # 目標要素が見つかったらそのインデックスを返す\n    return -1  # 目標要素が見つからなければ -1 を返す\n
binary_search.cpp
/* 二分探索(左閉右開区間) */\nint binarySearchLCRO(vector<int> &nums, int target) {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    int i = 0, j = nums.size();\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target)    // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m) にある\n            j = m;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.java
/* 二分探索(左閉右開区間) */\nint binarySearchLCRO(int[] nums, int target) {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    int i = 0, j = nums.length;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m) にある\n            j = m;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.cs
/* 二分探索(左閉右開区間) */\nint BinarySearchLCRO(int[] nums, int target) {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    int i = 0, j = nums.Length;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        int m = i + (j - i) / 2;   // 中点インデックス m を計算\n        if (nums[m] < target)      // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m) にある\n            j = m;\n        else                       // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.go
/* 二分探索(左閉右開区間) */\nfunc binarySearchLCRO(nums []int, target int) int {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    i, j := 0, len(nums)\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    for i < j {\n        m := i + (j-i)/2      // 中点インデックス m を計算\n        if nums[m] < target { // この場合、target は区間 [m+1, j) にある\n            i = m + 1\n        } else if nums[m] > target { // この場合、target は区間 [i, m) にある\n            j = m\n        } else { // 目標要素が見つかったらそのインデックスを返す\n            return m\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.swift
/* 二分探索(左閉右開区間) */\nfunc binarySearchLCRO(nums: [Int], target: Int) -> Int {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    var i = nums.startIndex\n    var j = nums.endIndex\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while i < j {\n        let m = i + (j - i) / 2 // 中点インデックス m を計算\n        if nums[m] < target { // この場合、target は区間 [m+1, j) にある\n            i = m + 1\n        } else if nums[m] > target { // この場合、target は区間 [i, m) にある\n            j = m\n        } else { // 目標要素が見つかったらそのインデックスを返す\n            return m\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.js
/* 二分探索(左閉右開区間) */\nfunction binarySearchLCRO(nums, target) {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    let i = 0,\n        j = nums.length;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        // 中点インデックス `m` を計算し、`parseInt()` で切り捨てる\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        else if (nums[m] > target)\n            // この場合、target は区間 [i, m) にある\n            j = m;\n        // 目標要素が見つかったらそのインデックスを返す\n        else return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.ts
/* 二分探索(左閉右開区間) */\nfunction binarySearchLCRO(nums: number[], target: number): number {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    let i = 0,\n        j = nums.length;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        // 中点インデックス m を計算\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // この場合、target は区間 [i, m) にある\n            j = m;\n        } else {\n            // 目標要素が見つかったらそのインデックスを返す\n            return m;\n        }\n    }\n    return -1; // 目標要素が見つからなければ -1 を返す\n}\n
binary_search.dart
/* 二分探索(左閉右開区間) */\nint binarySearchLCRO(List<int> nums, int target) {\n  // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n  int i = 0, j = nums.length;\n  // ループし、探索区間が空になったら終了する(i = j で空)\n  while (i < j) {\n    int m = i + (j - i) ~/ 2; // 中点インデックス m を計算\n    if (nums[m] < target) {\n      // この場合、target は区間 [m+1, j) にある\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // この場合、target は区間 [i, m) にある\n      j = m;\n    } else {\n      // 目標要素が見つかったらそのインデックスを返す\n      return m;\n    }\n  }\n  // 目標要素が見つからなければ -1 を返す\n  return -1;\n}\n
binary_search.rs
/* 二分探索(左閉右開区間) */\nfn binary_search_lcro(nums: &[i32], target: i32) -> i32 {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    let mut i = 0;\n    let mut j = nums.len() as i32;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while i < j {\n        let m = i + (j - i) / 2; // 中点インデックス m を計算\n        if nums[m as usize] < target {\n            // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // この場合、target は区間 [i, m) にある\n            j = m;\n        } else {\n            // 目標要素が見つかったらそのインデックスを返す\n            return m;\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.c
/* 二分探索(左閉右開区間) */\nint binarySearchLCRO(int *nums, int len, int target) {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    int i = 0, j = len;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target)    // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m) にある\n            j = m;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.kt
/* 二分探索(左閉右開区間) */\nfun binarySearchLCRO(nums: IntArray, target: Int): Int {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    var i = 0\n    var j = nums.size\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        val m = i + (j - i) / 2 // 中点インデックス m を計算\n        if (nums[m] < target) // この場合、target は区間 [m+1, j) にある\n            i = m + 1\n        else if (nums[m] > target) // この場合、target は区間 [i, m) にある\n            j = m\n        else  // 目標要素が見つかったらそのインデックスを返す\n            return m\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.rb
### 二分探索(左閉右開区間) ###\ndef binary_search_lcro(nums, target)\n  # 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n  i, j = 0, nums.length\n\n  # ループし、探索区間が空になったら終了する(i = j で空)\n  while i < j\n    # 中点インデックス m を計算\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # この場合、target は区間 [m+1, j) にある\n    elsif nums[m] > target\n      j = m - 1 # この場合、target は区間 [i, m) にある\n    else\n      return m  # 目標要素が見つかったらそのインデックスを返す\n    end\n  end\n\n  -1  # 目標要素が見つからなければ -1 を返す\nend\n
コードの可視化

全画面で見る >

次の図に示すように、2 種類の区間表現では、二分探索アルゴリズムの初期化、ループ条件、区間の縮小操作がそれぞれ異なります。

「両閉区間」の表現では左右の境界がどちらも閉区間として定義されるため、ポインタ \\(i\\) とポインタ \\(j\\) による区間縮小の操作も対称になります。このほうがミスをしにくいため、一般には「両閉区間」の書き方を推奨します。

図 10-3   2 種類の区間定義

","path":["第 10 章   探索","10.1   二分探索"],"tags":[]},{"location":"chapter_searching/binary_search/#1012","level":2,"title":"10.1.2   利点と限界","text":"

二分探索は時間と空間の両面で優れた性能を持ちます。

  • 二分探索は時間効率が高いです。データ量が大きい場合、対数時間計算量は大きな優位性を持ちます。たとえば、データサイズ \\(n = 2^{20}\\) のとき、線形探索では \\(2^{20} = 1048576\\) 回のループが必要ですが、二分探索では \\(\\log_2 2^{20} = 20\\) 回で済みます。
  • 二分探索は追加の空間を必要としません。追加領域を要する探索アルゴリズム(たとえばハッシュ探索)と比べて、二分探索はより省メモリです。

しかし、二分探索があらゆる状況に適しているわけではなく、主な理由は次のとおりです。

  • 二分探索は整列済みデータにしか適用できません。入力データが無秩序な場合、二分探索を使うためだけにソートするのは割に合いません。ソートアルゴリズムの時間計算量は通常 \\(O(n \\log n)\\) であり、線形探索や二分探索よりも高いからです。要素を頻繁に挿入する場面では、配列の整列性を保つために特定位置へ挿入する必要があり、その時間計算量は \\(O(n)\\) と高コストです。
  • 二分探索は配列にしか適していません。二分探索では要素へ飛び飛びにアクセスする必要がありますが、連結リストでそのようなアクセスを行う効率は低いため、連結リストやそれを基に実装されたデータ構造には向きません。
  • データ量が小さい場合は線形探索のほうが高性能です。線形探索では各ラウンドで 1 回の比較だけで済みますが、二分探索では 1 回の加算、1 回の除算、1 ~ 3 回の比較、1 回の加算(減算)が必要で、合計 4 ~ 6 個の基本操作になります。したがって、データ量 \\(n\\) が小さいときは、線形探索のほうがかえって速くなります。
","path":["第 10 章   探索","10.1   二分探索"],"tags":[]},{"location":"chapter_searching/binary_search_edge/","level":1,"title":"10.3   二分探索の境界","text":"","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1031","level":2,"title":"10.3.1   左端境界を探す","text":"

Question

長さ \\(n\\) のソート済み配列 nums が与えられ、その中には重複要素が含まれる可能性があります。配列内で最も左にある要素 target のインデックスを返してください。配列にこの要素が含まれない場合は、\\(-1\\) を返します。

二分探索で挿入位置を求める方法を思い出すと、探索完了後に \\(i\\) は最も左にある target を指します。したがって、挿入位置を探すことの本質は、最も左にある target のインデックスを探すことです。

挿入位置を探す関数を使って左端境界を求めることを考えます。なお、配列に target が含まれない場合があり、そのときは次の 2 つの結果が起こりえます。

  • 挿入位置のインデックス \\(i\\) が範囲外になる。
  • 要素 nums[i]target と等しくない。

上の 2 つの状況に当てはまる場合は、直接 \\(-1\\) を返せば十分です。コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
def binary_search_left_edge(nums: list[int], target: int) -> int:\n    \"\"\"最も左の target を二分探索\"\"\"\n    # target の挿入位置を探すのと等価\n    i = binary_search_insertion(nums, target)\n    # target が見つからなければ、-1 を返す\n    if i == len(nums) or nums[i] != target:\n        return -1\n    # target が見つかったら、インデックス i を返す\n    return i\n
binary_search_edge.cpp
/* 最も左の target を二分探索 */\nint binarySearchLeftEdge(vector<int> &nums, int target) {\n    // target の挿入位置を探すのと等価\n    int i = binarySearchInsertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if (i == nums.size() || nums[i] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.java
/* 最も左の target を二分探索 */\nint binarySearchLeftEdge(int[] nums, int target) {\n    // target の挿入位置を探すのと等価\n    int i = binary_search_insertion.binarySearchInsertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if (i == nums.length || nums[i] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.cs
/* 最も左の target を二分探索 */\nint BinarySearchLeftEdge(int[] nums, int target) {\n    // target の挿入位置を探すのと等価\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if (i == nums.Length || nums[i] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.go
/* 最も左の target を二分探索 */\nfunc binarySearchLeftEdge(nums []int, target int) int {\n    // target の挿入位置を探すのと等価\n    i := binarySearchInsertion(nums, target)\n    // target が見つからなければ、-1 を返す\n    if i == len(nums) || nums[i] != target {\n        return -1\n    }\n    // target が見つかったら、インデックス i を返す\n    return i\n}\n
binary_search_edge.swift
/* 最も左の target を二分探索 */\nfunc binarySearchLeftEdge(nums: [Int], target: Int) -> Int {\n    // target の挿入位置を探すのと等価\n    let i = binarySearchInsertion(nums: nums, target: target)\n    // target が見つからなければ、-1 を返す\n    if i == nums.endIndex || nums[i] != target {\n        return -1\n    }\n    // target が見つかったら、インデックス i を返す\n    return i\n}\n
binary_search_edge.js
/* 最も左の target を二分探索 */\nfunction binarySearchLeftEdge(nums, target) {\n    // target の挿入位置を探すのと等価\n    const i = binarySearchInsertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.ts
/* 最も左の target を二分探索 */\nfunction binarySearchLeftEdge(nums: Array<number>, target: number): number {\n    // target の挿入位置を探すのと等価\n    const i = binarySearchInsertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.dart
/* 最も左の target を二分探索 */\nint binarySearchLeftEdge(List<int> nums, int target) {\n  // target の挿入位置を探すのと等価\n  int i = binarySearchInsertion(nums, target);\n  // target が見つからなければ、-1 を返す\n  if (i == nums.length || nums[i] != target) {\n    return -1;\n  }\n  // target が見つかったら、インデックス i を返す\n  return i;\n}\n
binary_search_edge.rs
/* 最も左の target を二分探索 */\nfn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {\n    // target の挿入位置を探すのと等価\n    let i = binary_search_insertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if i == nums.len() as i32 || nums[i as usize] != target {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    i\n}\n
binary_search_edge.c
/* 最も左の target を二分探索 */\nint binarySearchLeftEdge(int *nums, int numSize, int target) {\n    // target の挿入位置を探すのと等価\n    int i = binarySearchInsertion(nums, numSize, target);\n    // target が見つからなければ、-1 を返す\n    if (i == numSize || nums[i] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.kt
/* 最も左の target を二分探索 */\nfun binarySearchLeftEdge(nums: IntArray, target: Int): Int {\n    // target の挿入位置を探すのと等価\n    val i = binarySearchInsertion(nums, target)\n    // target が見つからなければ、-1 を返す\n    if (i == nums.size || nums[i] != target) {\n        return -1\n    }\n    // target が見つかったら、インデックス i を返す\n    return i\n}\n
binary_search_edge.rb
### target の最左位置を二分探索 ###\ndef binary_search_left_edge(nums, target)\n  # target の挿入位置を探すのと等価\n  i = binary_search_insertion(nums, target)\n\n  # target が見つからなければ、-1 を返す\n  return -1 if i == nums.length || nums[i] != target\n\n  i # target が見つかったら、インデックス i を返す\nend\n
コードの可視化

全画面で見る >

","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1032","level":2,"title":"10.3.2   右端境界を探す","text":"

では、最も右にある target はどのように探せるでしょうか。最も直接的な方法はコードを修正し、nums[m] == target の場合のポインタの縮小操作を置き換えることです。ここではコードを省略するので、興味があれば自分で実装してみてください。

ここでは、より巧妙な 2 つの方法を紹介します。

","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1","level":3,"title":"1.   左端境界探索を再利用する","text":"

実際には、最も左の要素を探す関数を利用して最も右の要素を探せます。具体的には、最も右にある target を探すことを、最も左にある target + 1 を探すことに変換します。

下図のように、探索完了後、ポインタ \\(i\\) は最も左にある target + 1(存在する場合)を指し、\\(j\\) は最も右にある target を指します。したがって \\(j\\) を返せばよいです。

図 10-7   右端境界の探索を左端境界の探索に変換する

返される挿入位置は \\(i\\) なので、そこから \\(1\\) を引いて \\(j\\) を得る必要があることに注意してください:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
def binary_search_right_edge(nums: list[int], target: int) -> int:\n    \"\"\"最も右の target を二分探索\"\"\"\n    # 最左の target + 1 を探す問題に変換する\n    i = binary_search_insertion(nums, target + 1)\n    # j は最も右の target を指し、i は target より大きい最初の要素を指す\n    j = i - 1\n    # target が見つからなければ、-1 を返す\n    if j == -1 or nums[j] != target:\n        return -1\n    # target が見つかったら、インデックス j を返す\n    return j\n
binary_search_edge.cpp
/* 最も右の target を二分探索 */\nint binarySearchRightEdge(vector<int> &nums, int target) {\n    // 最左の target + 1 を探す問題に変換する\n    int i = binarySearchInsertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    int j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.java
/* 最も右の target を二分探索 */\nint binarySearchRightEdge(int[] nums, int target) {\n    // 最左の target + 1 を探す問題に変換する\n    int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    int j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.cs
/* 最も右の target を二分探索 */\nint BinarySearchRightEdge(int[] nums, int target) {\n    // 最左の target + 1 を探す問題に変換する\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    int j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.go
/* 最も右の target を二分探索 */\nfunc binarySearchRightEdge(nums []int, target int) int {\n    // 最左の target + 1 を探す問題に変換する\n    i := binarySearchInsertion(nums, target+1)\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    j := i - 1\n    // target が見つからなければ、-1 を返す\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // target が見つかったら、インデックス j を返す\n    return j\n}\n
binary_search_edge.swift
/* 最も右の target を二分探索 */\nfunc binarySearchRightEdge(nums: [Int], target: Int) -> Int {\n    // 最左の target + 1 を探す問題に変換する\n    let i = binarySearchInsertion(nums: nums, target: target + 1)\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    let j = i - 1\n    // target が見つからなければ、-1 を返す\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // target が見つかったら、インデックス j を返す\n    return j\n}\n
binary_search_edge.js
/* 最も右の target を二分探索 */\nfunction binarySearchRightEdge(nums, target) {\n    // 最左の target + 1 を探す問題に変換する\n    const i = binarySearchInsertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    const j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.ts
/* 最も右の target を二分探索 */\nfunction binarySearchRightEdge(nums: Array<number>, target: number): number {\n    // 最左の target + 1 を探す問題に変換する\n    const i = binarySearchInsertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    const j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.dart
/* 最も右の target を二分探索 */\nint binarySearchRightEdge(List<int> nums, int target) {\n  // 最左の target + 1 を探す問題に変換する\n  int i = binarySearchInsertion(nums, target + 1);\n  // j は最も右の target を指し、i は target より大きい最初の要素を指す\n  int j = i - 1;\n  // target が見つからなければ、-1 を返す\n  if (j == -1 || nums[j] != target) {\n    return -1;\n  }\n  // target が見つかったら、インデックス j を返す\n  return j;\n}\n
binary_search_edge.rs
/* 最も右の target を二分探索 */\nfn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {\n    // 最左の target + 1 を探す問題に変換する\n    let i = binary_search_insertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    let j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if j == -1 || nums[j as usize] != target {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    j\n}\n
binary_search_edge.c
/* 最も右の target を二分探索 */\nint binarySearchRightEdge(int *nums, int numSize, int target) {\n    // 最左の target + 1 を探す問題に変換する\n    int i = binarySearchInsertion(nums, numSize, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    int j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.kt
/* 最も右の target を二分探索 */\nfun binarySearchRightEdge(nums: IntArray, target: Int): Int {\n    // 最左の target + 1 を探す問題に変換する\n    val i = binarySearchInsertion(nums, target + 1)\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    val j = i - 1\n    // target が見つからなければ、-1 を返す\n    if (j == -1 || nums[j] != target) {\n        return -1\n    }\n    // target が見つかったら、インデックス j を返す\n    return j\n}\n
binary_search_edge.rb
### target の最右位置を二分探索 ###\ndef binary_search_right_edge(nums, target)\n  # 最左の target + 1 を探す問題に変換する\n  i = binary_search_insertion(nums, target + 1)\n\n  # j は最も右の target を指し、i は target より大きい最初の要素を指す\n  j = i - 1\n\n  # target が見つからなければ、-1 を返す\n  return -1 if j == -1 || nums[j] != target\n\n  j # target が見つかったら、インデックス j を返す\nend\n
コードの可視化

全画面で見る >

","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#2","level":3,"title":"2.   要素探索に変換する","text":"

配列に target が含まれない場合、最終的に \\(i\\) と \\(j\\) はそれぞれ target より大きい最初の要素と、target より小さい最初の要素を指すことになります。

したがって、下図のように、配列中に存在しない要素を構成して、それを使って左右の境界を探せます。

  • 最も左にある target の探索:target - 0.5 を探すことに変換でき、ポインタ \\(i\\) を返します。
  • 最も右にある target の探索:target + 0.5 を探すことに変換でき、ポインタ \\(j\\) を返します。

図 10-8   境界の探索を要素の探索に変換する

ここではコードを省略しますが、次の 2 点に注意が必要です。

  • 与えられた配列には小数が含まれないため、等しい場合をどう処理するかを気にする必要はありません。
  • この方法では小数を導入するため、関数内の変数 target を浮動小数点数型に変更する必要があります(Python は変更不要です)。
","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/","level":1,"title":"10.2   二分探索の挿入位置","text":"

二分探索は目標要素の検索だけでなく、目標要素の挿入位置を探すなど、多くの派生問題の解決にも利用できます。

","path":["第 10 章   探索","10.2   二分探索の挿入位置"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1021","level":2,"title":"10.2.1   重複要素がない場合","text":"

Question

長さ \\(n\\) の整列済み配列 nums と要素 target が与えられます。配列には重複要素は存在しません。ここで target を配列 nums に挿入し、その順序を保ちます。配列中にすでに要素 target が存在する場合は、その左側に挿入します。挿入後の配列における target のインデックスを返してください。例を以下の図に示します。

図 10-4   二分探索の挿入位置の例データ

前節の二分探索コードを再利用したい場合は、次の二つの問題に答える必要があります。

問題 1:配列に target が含まれる場合、挿入位置のインデックスはその要素のインデックスですか?

問題では target を等しい要素の左側に挿入するよう求めているため、新しく挿入された target は元の target の位置に入ります。つまり、配列に target が含まれる場合、挿入位置のインデックスはその target のインデックスです。

問題 2:配列に target が存在しない場合、挿入位置はどの要素のインデックスですか?

二分探索の過程をさらに考えると、nums[m] < target のときは \\(i\\) が移動します。これは、ポインタ \\(i\\) が target 以上の要素へ近づいていることを意味します。同様に、ポインタ \\(j\\) は常に target 以下の要素へ近づいています。

したがって二分探索の終了時には、\\(i\\) は最初の target より大きい要素を指し、\\(j\\) は最初の target より小さい要素を指します。よって、配列に target が含まれない場合、挿入インデックスは \\(i\\) です。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
def binary_search_insertion_simple(nums: list[int], target: int) -> int:\n    \"\"\"二分探索で挿入位置を探す(重複要素なし)\"\"\"\n    i, j = 0, len(nums) - 1  # 両閉区間 [0, n-1] を初期化\n    while i <= j:\n        m = (i + j) // 2  # 中点インデックス m を計算\n        if nums[m] < target:\n            i = m + 1  # target は区間 [m+1, j] にある\n        elif nums[m] > target:\n            j = m - 1  # target は区間 [i, m-1] にある\n        else:\n            return m  # target が見つかったら、挿入位置 m を返す\n    # target が見つからなければ、挿入位置 i を返す\n    return i\n
binary_search_insertion.cpp
/* 二分探索で挿入位置を探す(重複要素なし) */\nint binarySearchInsertionSimple(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.java
/* 二分探索で挿入位置を探す(重複要素なし) */\nint binarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.cs
/* 二分探索で挿入位置を探す(重複要素なし) */\nint BinarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.go
/* 二分探索で挿入位置を探す(重複要素なし) */\nfunc binarySearchInsertionSimple(nums []int, target int) int {\n    // 両閉区間 [0, n-1] を初期化\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // 中点インデックス m を計算\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target は区間 [m+1, j] にある\n            i = m + 1\n        } else if nums[m] > target {\n            // target は区間 [i, m-1] にある\n            j = m - 1\n        } else {\n            // target が見つかったら、挿入位置 m を返す\n            return m\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.swift
/* 二分探索で挿入位置を探す(重複要素なし) */\nfunc binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {\n    // 両閉区間 [0, n-1] を初期化\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // 中点インデックス m を計算\n        if nums[m] < target {\n            i = m + 1 // target は区間 [m+1, j] にある\n        } else if nums[m] > target {\n            j = m - 1 // target は区間 [i, m-1] にある\n        } else {\n            return m // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.js
/* 二分探索で挿入位置を探す(重複要素なし) */\nfunction binarySearchInsertionSimple(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.ts
/* 二分探索で挿入位置を探す(重複要素なし) */\nfunction binarySearchInsertionSimple(\n    nums: Array<number>,\n    target: number\n): number {\n    let i = 0,\n        j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.dart
/* 二分探索で挿入位置を探す(重複要素なし) */\nint binarySearchInsertionSimple(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // 中点インデックス m を計算\n    if (nums[m] < target) {\n      i = m + 1; // target は区間 [m+1, j] にある\n    } else if (nums[m] > target) {\n      j = m - 1; // target は区間 [i, m-1] にある\n    } else {\n      return m; // target が見つかったら、挿入位置 m を返す\n    }\n  }\n  // target が見つからなければ、挿入位置 i を返す\n  return i;\n}\n
binary_search_insertion.rs
/* 二分探索で挿入位置を探す(重複要素なし) */\nfn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // 両閉区間 [0, n-1] を初期化\n    while i <= j {\n        let m = i + (j - i) / 2; // 中点インデックス m を計算\n        if nums[m as usize] < target {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if nums[m as usize] > target {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m;\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    i\n}\n
binary_search_insertion.c
/* 二分探索で挿入位置を探す(重複要素なし) */\nint binarySearchInsertionSimple(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.kt
/* 二分探索で挿入位置を探す(重複要素なし) */\nfun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        val m = i + (j - i) / 2 // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1 // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1 // target は区間 [i, m-1] にある\n        } else {\n            return m // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.rb
### 二分探索の挿入位置(重複要素なし) ###\ndef binary_search_insertion_simple(nums, target)\n  # 両閉区間 [0, n-1] を初期化\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # 中点インデックス m を計算\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target は区間 [m+1, j] にある\n    elsif nums[m] > target\n      j = m - 1 # target は区間 [i, m-1] にある\n    else\n      return m  # target が見つかったら、挿入位置 m を返す\n    end\n  end\n\n  i # target が見つからなければ、挿入位置 i を返す\nend\n
コードの可視化

全画面で見る >

","path":["第 10 章   探索","10.2   二分探索の挿入位置"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1022","level":2,"title":"10.2.2   重複要素がある場合","text":"

Question

前問を踏まえ、配列には重複要素が含まれる可能性があるものとし、それ以外の条件は変わりません。

配列中に複数の target が存在する場合、通常の二分探索ではそのうち一つの target のインデックスしか返せず、その要素の左側と右側にあといくつ target があるかは分かりません。

問題では目標要素を最も左に挿入する必要があるため、配列中で最も左にある target のインデックスを探す必要があります。まずは以下の図に示す手順で実現することを考えます。

  1. 二分探索を実行し、任意の target のインデックスを得て、これを \\(k\\) とします。
  2. インデックス \\(k\\) から始めて左へ線形探索し、最も左の target を見つけたら返します。

図 10-5   線形探索による重複要素の挿入位置

この方法は使用できますが、線形探索を含むため、時間計算量は \\(O(n)\\) です。配列中に重複した target が多い場合、この方法の効率は低くなります。

次に、二分探索のコードを拡張することを考えます。以下の図に示すように、全体の流れは変えず、各反復でまず中点インデックス \\(m\\) を計算し、その後 targetnums[m] の大小関係を判定して、次のいくつかの状況に分けます。

  • nums[m] < target または nums[m] > target のときは、まだ target を見つけていないことを意味するため、通常の二分探索と同じ区間縮小を行い、ポインタ \\(i\\) と \\(j\\) を target に近づけます。
  • nums[m] == target のときは、target より小さい要素が区間 \\([i, m - 1]\\) にあることを意味するため、\\(j = m - 1\\) として区間を縮小し、ポインタ \\(j\\) を target より小さい要素に近づけます。

ループ終了後、\\(i\\) は最も左の target を指し、\\(j\\) は最初の target より小さい要素を指すため、インデックス \\(i\\) が挿入位置です。

<1><2><3><4><5><6><7><8>

図 10-6   重複要素に対する二分探索の挿入位置の手順

以下のコードを観察すると、分岐 nums[m] > targetnums[m] == target の処理は同じであるため、両者はまとめることができます。

それでも、判定条件を分けたままにしておくことは可能であり、そのほうがロジックがより明確で、可読性も高くなります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
def binary_search_insertion(nums: list[int], target: int) -> int:\n    \"\"\"二分探索で挿入位置を探す(重複要素あり)\"\"\"\n    i, j = 0, len(nums) - 1  # 両閉区間 [0, n-1] を初期化\n    while i <= j:\n        m = (i + j) // 2  # 中点インデックス m を計算\n        if nums[m] < target:\n            i = m + 1  # target は区間 [m+1, j] にある\n        elif nums[m] > target:\n            j = m - 1  # target は区間 [i, m-1] にある\n        else:\n            j = m - 1  # target より小さい最初の要素は区間 [i, m-1] にある\n    # 挿入位置 i を返す\n    return i\n
binary_search_insertion.cpp
/* 二分探索で挿入位置を探す(重複要素あり) */\nint binarySearchInsertion(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.java
/* 二分探索で挿入位置を探す(重複要素あり) */\nint binarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.cs
/* 二分探索で挿入位置を探す(重複要素あり) */\nint BinarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.go
/* 二分探索で挿入位置を探す(重複要素あり) */\nfunc binarySearchInsertion(nums []int, target int) int {\n    // 両閉区間 [0, n-1] を初期化\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // 中点インデックス m を計算\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target は区間 [m+1, j] にある\n            i = m + 1\n        } else if nums[m] > target {\n            // target は区間 [i, m-1] にある\n            j = m - 1\n        } else {\n            // target より小さい最初の要素は区間 [i, m-1] にある\n            j = m - 1\n        }\n    }\n    // 挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.swift
/* 二分探索で挿入位置を探す(重複要素あり) */\nfunc binarySearchInsertion(nums: [Int], target: Int) -> Int {\n    // 両閉区間 [0, n-1] を初期化\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // 中点インデックス m を計算\n        if nums[m] < target {\n            i = m + 1 // target は区間 [m+1, j] にある\n        } else if nums[m] > target {\n            j = m - 1 // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1 // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.js
/* 二分探索で挿入位置を探す(重複要素あり) */\nfunction binarySearchInsertion(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.ts
/* 二分探索で挿入位置を探す(重複要素あり) */\nfunction binarySearchInsertion(nums: Array<number>, target: number): number {\n    let i = 0,\n        j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.dart
/* 二分探索で挿入位置を探す(重複要素あり) */\nint binarySearchInsertion(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // 中点インデックス m を計算\n    if (nums[m] < target) {\n      i = m + 1; // target は区間 [m+1, j] にある\n    } else if (nums[m] > target) {\n      j = m - 1; // target は区間 [i, m-1] にある\n    } else {\n      j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n    }\n  }\n  // 挿入位置 i を返す\n  return i;\n}\n
binary_search_insertion.rs
/* 二分探索で挿入位置を探す(重複要素あり) */\npub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // 両閉区間 [0, n-1] を初期化\n    while i <= j {\n        let m = i + (j - i) / 2; // 中点インデックス m を計算\n        if nums[m as usize] < target {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if nums[m as usize] > target {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    i\n}\n
binary_search_insertion.c
/* 二分探索で挿入位置を探す(重複要素あり) */\nint binarySearchInsertion(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.kt
/* 二分探索で挿入位置を探す(重複要素あり) */\nfun binarySearchInsertion(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        val m = i + (j - i) / 2 // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1 // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1 // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1 // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.rb
### 二分探索の挿入位置(重複要素あり) ###\ndef binary_search_insertion(nums, target)\n  # 両閉区間 [0, n-1] を初期化\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # 中点インデックス m を計算\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target は区間 [m+1, j] にある\n    elsif nums[m] > target\n      j = m - 1 # target は区間 [i, m-1] にある\n    else\n      j = m - 1 # target より小さい最初の要素は区間 [i, m-1] にある\n    end\n  end\n\n  i # 挿入位置 i を返す\nend\n
コードの可視化

全画面で見る >

Tip

本節のコードはすべて「両閉区間」の書き方です。興味のある読者は「左閉右開」の書き方を自分で実装してみてください。

要するに、二分探索とはポインタ \\(i\\) と \\(j\\) にそれぞれ探索目標を設定することにほかなりません。目標は具体的な要素(たとえば target)である場合もあれば、要素の範囲(たとえば target より小さい要素)である場合もあります。

繰り返される二分のループの中で、ポインタ \\(i\\) と \\(j\\) はどちらも事前に定めた目標へ徐々に近づいていきます。最終的に、それらは答えを見つけるか、境界を越えたところで停止します。

","path":["第 10 章   探索","10.2   二分探索の挿入位置"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/","level":1,"title":"10.4   ハッシュによる最適化戦略","text":"

アルゴリズムの問題では,線形探索をハッシュ探索に置き換えることでアルゴリズムの時間計算量を下げることがよくあります。ここでは,あるアルゴリズム問題を通じて理解を深めましょう。

Question

整数配列 nums と目標要素 target が与えられたとき,配列内から和が target となる 2 つの要素を探索し,それらの配列インデックスを返してください。任意の 1 つの解を返せば十分です。

","path":["第 10 章   探索","10.4   ハッシュによる最適化戦略"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041","level":2,"title":"10.4.1   線形探索:時間と引き換えに空間を節約","text":"

考えられるすべての組み合わせを直接走査することを考えます。次の図に示すように,2 重ループを開始し,各ラウンドで 2 つの整数の和が target であるかを判定します。そうであれば,それらのインデックスを返します。

図 10-9   線形探索で 2 数の和を求める

コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n    \"\"\"方法 1:総当たり列挙\"\"\"\n    # 2重ループのため、時間計算量は O(n^2)\n    for i in range(len(nums) - 1):\n        for j in range(i + 1, len(nums)):\n            if nums[i] + nums[j] == target:\n                return [i, j]\n    return []\n
two_sum.cpp
/* 方法 1:総当たり列挙 */\nvector<int> twoSumBruteForce(vector<int> &nums, int target) {\n    int size = nums.size();\n    // 2重ループのため、時間計算量は O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return {i, j};\n        }\n    }\n    return {};\n}\n
two_sum.java
/* 方法 1:総当たり列挙 */\nint[] twoSumBruteForce(int[] nums, int target) {\n    int size = nums.length;\n    // 2重ループのため、時間計算量は O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return new int[] { i, j };\n        }\n    }\n    return new int[0];\n}\n
two_sum.cs
/* 方法 1:総当たり列挙 */\nint[] TwoSumBruteForce(int[] nums, int target) {\n    int size = nums.Length;\n    // 2重ループのため、時間計算量は O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return [i, j];\n        }\n    }\n    return [];\n}\n
two_sum.go
/* 方法 1:総当たり列挙 */\nfunc twoSumBruteForce(nums []int, target int) []int {\n    size := len(nums)\n    // 2重ループのため、時間計算量は O(n^2)\n    for i := 0; i < size-1; i++ {\n        for j := i + 1; j < size; j++ {\n            if nums[i]+nums[j] == target {\n                return []int{i, j}\n            }\n        }\n    }\n    return nil\n}\n
two_sum.swift
/* 方法 1:総当たり列挙 */\nfunc twoSumBruteForce(nums: [Int], target: Int) -> [Int] {\n    // 2重ループのため、時間計算量は O(n^2)\n    for i in nums.indices.dropLast() {\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[i] + nums[j] == target {\n                return [i, j]\n            }\n        }\n    }\n    return [0]\n}\n
two_sum.js
/* 方法 1:総当たり列挙 */\nfunction twoSumBruteForce(nums, target) {\n    const n = nums.length;\n    // 2重ループのため、時間計算量は O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
two_sum.ts
/* 方法 1:総当たり列挙 */\nfunction twoSumBruteForce(nums: number[], target: number): number[] {\n    const n = nums.length;\n    // 2重ループのため、時間計算量は O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
two_sum.dart
/* 方法1: 総当たり列挙 */\nList<int> twoSumBruteForce(List<int> nums, int target) {\n  int size = nums.length;\n  // 2重ループのため、時間計算量は O(n^2)\n  for (var i = 0; i < size - 1; i++) {\n    for (var j = i + 1; j < size; j++) {\n      if (nums[i] + nums[j] == target) return [i, j];\n    }\n  }\n  return [0];\n}\n
two_sum.rs
/* 方法 1:総当たり列挙 */\npub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    let size = nums.len();\n    // 2重ループのため、時間計算量は O(n^2)\n    for i in 0..size - 1 {\n        for j in i + 1..size {\n            if nums[i] + nums[j] == target {\n                return Some(vec![i as i32, j as i32]);\n            }\n        }\n    }\n    None\n}\n
two_sum.c
/* 方法 1:総当たり列挙 */\nint *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {\n    for (int i = 0; i < numsSize; ++i) {\n        for (int j = i + 1; j < numsSize; ++j) {\n            if (nums[i] + nums[j] == target) {\n                int *res = malloc(sizeof(int) * 2);\n                res[0] = i, res[1] = j;\n                *returnSize = 2;\n                return res;\n            }\n        }\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
two_sum.kt
/* 方法 1:総当たり列挙 */\nfun twoSumBruteForce(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // 2重ループのため、時間計算量は O(n^2)\n    for (i in 0..<size - 1) {\n        for (j in i + 1..<size) {\n            if (nums[i] + nums[j] == target) return intArrayOf(i, j)\n        }\n    }\n    return IntArray(0)\n}\n
two_sum.rb
### 方法1:総当たり列挙 ###\ndef two_sum_brute_force(nums, target)\n  # 2重ループのため、時間計算量は O(n^2)\n  for i in 0...(nums.length - 1)\n    for j in (i + 1)...nums.length\n      return [i, j] if nums[i] + nums[j] == target\n    end\n  end\n\n  []\nend\n
コードの可視化

全画面で見る >

この方法の時間計算量は \\(O(n^2)\\) ,空間計算量は \\(O(1)\\) であり,大規模データでは非常に時間がかかります。

","path":["第 10 章   探索","10.4   ハッシュによる最適化戦略"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1042","level":2,"title":"10.4.2   ハッシュ探索:空間と引き換えに時間を節約","text":"

ハッシュテーブルを利用し,キーと値をそれぞれ配列要素と要素のインデックスにします。配列をループで走査し,各ラウンドで次の図に示す手順を実行します。

  1. 数値 target - nums[i] がハッシュテーブル内にあるかを判定します。あれば,この 2 つの要素のインデックスを直接返します。
  2. キーと値の組 nums[i] とインデックス i をハッシュテーブルに追加します。
<1><2><3>

図 10-10   補助ハッシュテーブルで 2 数の和を求める

実装コードは次のとおりで,単一ループだけで済みます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
def two_sum_hash_table(nums: list[int], target: int) -> list[int]:\n    \"\"\"方法 2:補助ハッシュテーブル\"\"\"\n    # 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    dic = {}\n    # 単一ループで、時間計算量は O(n)\n    for i in range(len(nums)):\n        if target - nums[i] in dic:\n            return [dic[target - nums[i]], i]\n        dic[nums[i]] = i\n    return []\n
two_sum.cpp
/* 方法 2:補助ハッシュテーブル */\nvector<int> twoSumHashTable(vector<int> &nums, int target) {\n    int size = nums.size();\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    unordered_map<int, int> dic;\n    // 単一ループで、時間計算量は O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.find(target - nums[i]) != dic.end()) {\n            return {dic[target - nums[i]], i};\n        }\n        dic.emplace(nums[i], i);\n    }\n    return {};\n}\n
two_sum.java
/* 方法 2:補助ハッシュテーブル */\nint[] twoSumHashTable(int[] nums, int target) {\n    int size = nums.length;\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    Map<Integer, Integer> dic = new HashMap<>();\n    // 単一ループで、時間計算量は O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.containsKey(target - nums[i])) {\n            return new int[] { dic.get(target - nums[i]), i };\n        }\n        dic.put(nums[i], i);\n    }\n    return new int[0];\n}\n
two_sum.cs
/* 方法 2:補助ハッシュテーブル */\nint[] TwoSumHashTable(int[] nums, int target) {\n    int size = nums.Length;\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    Dictionary<int, int> dic = [];\n    // 単一ループで、時間計算量は O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.ContainsKey(target - nums[i])) {\n            return [dic[target - nums[i]], i];\n        }\n        dic.Add(nums[i], i);\n    }\n    return [];\n}\n
two_sum.go
/* 方法 2:補助ハッシュテーブル */\nfunc twoSumHashTable(nums []int, target int) []int {\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    hashTable := map[int]int{}\n    // 単一ループで、時間計算量は O(n)\n    for idx, val := range nums {\n        if preIdx, ok := hashTable[target-val]; ok {\n            return []int{preIdx, idx}\n        }\n        hashTable[val] = idx\n    }\n    return nil\n}\n
two_sum.swift
/* 方法 2:補助ハッシュテーブル */\nfunc twoSumHashTable(nums: [Int], target: Int) -> [Int] {\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    var dic: [Int: Int] = [:]\n    // 単一ループで、時間計算量は O(n)\n    for i in nums.indices {\n        if let j = dic[target - nums[i]] {\n            return [j, i]\n        }\n        dic[nums[i]] = i\n    }\n    return [0]\n}\n
two_sum.js
/* 方法 2:補助ハッシュテーブル */\nfunction twoSumHashTable(nums, target) {\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    let m = {};\n    // 単一ループで、時間計算量は O(n)\n    for (let i = 0; i < nums.length; i++) {\n        if (m[target - nums[i]] !== undefined) {\n            return [m[target - nums[i]], i];\n        } else {\n            m[nums[i]] = i;\n        }\n    }\n    return [];\n}\n
two_sum.ts
/* 方法 2:補助ハッシュテーブル */\nfunction twoSumHashTable(nums: number[], target: number): number[] {\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    let m: Map<number, number> = new Map();\n    // 単一ループで、時間計算量は O(n)\n    for (let i = 0; i < nums.length; i++) {\n        let index = m.get(target - nums[i]);\n        if (index !== undefined) {\n            return [index, i];\n        } else {\n            m.set(nums[i], i);\n        }\n    }\n    return [];\n}\n
two_sum.dart
/* 方法2: 補助ハッシュテーブル */\nList<int> twoSumHashTable(List<int> nums, int target) {\n  int size = nums.length;\n  // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n  Map<int, int> dic = HashMap();\n  // 単一ループで、時間計算量は O(n)\n  for (var i = 0; i < size; i++) {\n    if (dic.containsKey(target - nums[i])) {\n      return [dic[target - nums[i]]!, i];\n    }\n    dic.putIfAbsent(nums[i], () => i);\n  }\n  return [0];\n}\n
two_sum.rs
/* 方法 2:補助ハッシュテーブル */\npub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    let mut dic = HashMap::new();\n    // 単一ループで、時間計算量は O(n)\n    for (i, num) in nums.iter().enumerate() {\n        match dic.get(&(target - num)) {\n            Some(v) => return Some(vec![*v as i32, i as i32]),\n            None => dic.insert(num, i as i32),\n        };\n    }\n    None\n}\n
two_sum.c
/* ハッシュテーブル */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // uthash.h を用いて実装\n} HashTable;\n\n/* ハッシュテーブルを検索する */\nHashTable *find(HashTable *h, int key) {\n    HashTable *tmp;\n    HASH_FIND_INT(h, &key, tmp);\n    return tmp;\n}\n\n/* ハッシュテーブルに要素を挿入する */\nvoid insert(HashTable **h, int key, int val) {\n    HashTable *t = find(*h, key);\n    if (t == NULL) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = key, tmp->val = val;\n        HASH_ADD_INT(*h, key, tmp);\n    } else {\n        t->val = val;\n    }\n}\n\n/* 方法 2:補助ハッシュテーブル */\nint *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {\n    HashTable *hashtable = NULL;\n    for (int i = 0; i < numsSize; i++) {\n        HashTable *t = find(hashtable, target - nums[i]);\n        if (t != NULL) {\n            int *res = malloc(sizeof(int) * 2);\n            res[0] = t->val, res[1] = i;\n            *returnSize = 2;\n            return res;\n        }\n        insert(&hashtable, nums[i], i);\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
two_sum.kt
/* 方法 2:補助ハッシュテーブル */\nfun twoSumHashTable(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    val dic = HashMap<Int, Int>()\n    // 単一ループで、時間計算量は O(n)\n    for (i in 0..<size) {\n        if (dic.containsKey(target - nums[i])) {\n            return intArrayOf(dic[target - nums[i]]!!, i)\n        }\n        dic[nums[i]] = i\n    }\n    return IntArray(0)\n}\n
two_sum.rb
### 方法2:補助ハッシュテーブル ###\ndef two_sum_hash_table(nums, target)\n  # 補助ハッシュテーブルを使用し、空間計算量は O(n)\n  dic = {}\n  # 単一ループで、時間計算量は O(n)\n  for i in 0...nums.length\n    return [dic[target - nums[i]], i] if dic.has_key?(target - nums[i])\n\n    dic[nums[i]] = i\n  end\n\n  []\nend\n
コードの可視化

全画面で見る >

この方法ではハッシュ探索によって時間計算量を \\(O(n^2)\\) から \\(O(n)\\) に下げ,実行効率を大幅に向上させます。

追加のハッシュテーブルを維持する必要があるため,空間計算量は \\(O(n)\\) です。それでも,この方法は全体として時間と空間の効率のバランスがより良く,本問の最適解です。

","path":["第 10 章   探索","10.4   ハッシュによる最適化戦略"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/","level":1,"title":"10.5   探索アルゴリズム再考","text":"

探索アルゴリズム(searching algorithm)は、データ構造(配列、連結リスト、木、グラフなど)の中から、特定の条件を満たす 1 つまたは複数の要素を探索するために用いられます。

探索アルゴリズムは、実装の考え方に応じて次の 2 種類に分けられます。

  • データ構造を走査して目標要素を特定する方法。配列、連結リスト、木、グラフの走査などがこれに当たります。
  • データの構成やデータに含まれる事前情報を利用して、要素を効率よく探す方法。二分探索、ハッシュ探索、二分探索木による探索などがこれに当たります。

これらのトピックはすでに前の章で扱っているため、探索アルゴリズムは私たちにとって見慣れたものです。本節では、より体系的な視点から探索アルゴリズムをあらためて見直します。

","path":["第 10 章   探索","10.5   探索アルゴリズム再考"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051","level":2,"title":"10.5.1   総当たり探索","text":"

総当たり探索は、データ構造の各要素を順に調べて目標要素を特定します。

  • “線形探索”は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から始めて、要素を 1 つずつ調べ、目標要素が見つかるか、もう一方の端に達しても見つからないまで続けます。
  • “幅優先探索”と“深さ優先探索”は、グラフと木における 2 つの走査戦略です。幅優先探索は初期ノードから始めて層ごとに探索し、近いところから遠いところへ各ノードを訪れます。深さ優先探索は初期ノードから始めて 1 本の経路を最後までたどり、その後でバックトラックしてほかの経路を試し、データ構造全体を走査し終えるまで続けます。

総当たり探索の利点は、単純で汎用性が高く、**データの前処理や追加のデータ構造を必要としない**ことです。

しかし、この種のアルゴリズムの時間計算量は \\(O(n)\\) です。ここで \\(n\\) は要素数であり、そのためデータ量が大きい場合は性能が低くなります。

","path":["第 10 章   探索","10.5   探索アルゴリズム再考"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1052","level":2,"title":"10.5.2   適応的な探索","text":"

適応的な探索は、データが持つ固有の性質(整列性など)を利用して探索過程を最適化し、目標要素をより効率よく特定します。

  • “二分探索”は、データの順序性を利用して効率的な探索を行う方法で、配列にしか適用できません。
  • “ハッシュ探索”は、ハッシュ表を用いて探索対象のデータと目標データをキーと値の対応にし、問い合わせ操作を実現します。
  • “木探索”は、特定の木構造(たとえば二分探索木)の中で、ノード値の比較に基づいて不要なノードをすばやく除外し、目標要素を特定します。

この種のアルゴリズムの利点は効率が高く、**時間計算量が \\(O(\\log n)\\) あるいは \\(O(1)\\) に達する**ことです。

しかし、これらのアルゴリズムを使うには、たいていデータの前処理が必要です。たとえば、二分探索では事前に配列をソートする必要があり、ハッシュ探索と木探索では追加のデータ構造が必要です。これらのデータ構造を維持するにも、追加の時間と空間のコストがかかります。

Tip

適応的な探索アルゴリズムは、しばしば検索アルゴリズムとも呼ばれ、主に特定のデータ構造の中で目標要素を高速に取得するために用いられます。

","path":["第 10 章   探索","10.5   探索アルゴリズム再考"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1053","level":2,"title":"10.5.3   探索手法の選択","text":"

大きさ \\(n\\) のデータ集合が与えられたとき、線形探索、二分探索、木探索、ハッシュ探索など、さまざまな方法で目標要素を探索できます。各手法の動作原理を下図に示します。

図 10-11   複数の探索戦略

上記のいくつかの手法について、操作効率と特性を次の表に示します。

表 10-1   探索アルゴリズムの効率比較

線形探索 二分探索 木探索 ハッシュ探索 要素探索 \\(O(n)\\) \\(O(\\log n)\\) \\(O(\\log n)\\) \\(O(1)\\) 要素挿入 \\(O(1)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) 要素削除 \\(O(n)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) 追加領域 \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\) データ前処理 / ソート \\(O(n \\log n)\\) 木構築 \\(O(n \\log n)\\) ハッシュ表構築 \\(O(n)\\) データの順序性 なし あり あり なし

探索アルゴリズムの選択は、規模、探索性能の要求、データの問い合わせ頻度や更新頻度などにも左右されます。

線形探索

  • 汎用性が高く、データの前処理をまったく必要としません。データを 1 回だけ問い合わせればよい場合、ほか 3 つの手法では前処理にかかる時間のほうが、線形探索そのものより長くなることがあります。
  • 規模の小さいデータに適しています。この場合、時間計算量が効率に与える影響は比較的小さいです。
  • データ更新頻度が高い場面に適しています。この手法では、データに対する追加の保守が不要だからです。

二分探索

  • 大規模データに適しており、効率が安定しています。最悪時間計算量は \\(O(\\log n)\\) です。
  • データ量が大きすぎる場合には向きません。配列の格納には連続したメモリ領域が必要だからです。
  • 頻繁な挿入・削除がある場面には向きません。整列配列を維持するコストが高いためです。

ハッシュ探索

  • 問い合わせ性能への要求が高い場面に適しており、平均時間計算量は \\(O(1)\\) です。
  • 順序付きデータや範囲探索が必要な場面には向きません。ハッシュ表ではデータの順序性を維持できないからです。
  • ハッシュ関数とハッシュ衝突処理戦略への依存度が高く、性能劣化のリスクが大きいです。
  • データ量が大きすぎる場合には向きません。ハッシュ表は衝突をできるだけ減らして良好な問い合わせ性能を出すために、追加の空間を必要とするからです。

木探索

  • 巨大データに適しています。木ノードはメモリ上に分散して格納されるためです。
  • 順序付きデータの維持や範囲探索が必要な場面に適しています。
  • ノードの挿入・削除を続ける過程で、二分探索木は偏ることがあり、時間計算量は \\(O(n)\\) まで劣化する可能性があります。
  • AVL 木や赤黒木を使えば、各種操作を \\(O(\\log n)\\) の効率で安定して実行できますが、木の平衡を保つ処理による追加コストが発生します。
","path":["第 10 章   探索","10.5   探索アルゴリズム再考"],"tags":[]},{"location":"chapter_searching/summary/","level":1,"title":"10.6   まとめ","text":"","path":["第 10 章   探索","10.6   まとめ"],"tags":[]},{"location":"chapter_searching/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • 二分探索はデータの順序性に依存し、ループによって探索区間を半分ずつ縮小しながら探索を行う。入力データがソート済みであることを前提とし、配列または配列ベースで実装されたデータ構造にのみ適用できる。
  • 総当たり探索はデータ構造を走査してデータを特定する。線形探索は配列と連結リストに適しており、幅優先探索と深さ優先探索はグラフと木に適している。この種のアルゴリズムは汎用性が高く、データの前処理を必要としないが、時間計算量 \\(O(n)\\) は高い。
  • ハッシュ探索、木探索、二分探索は高効率な探索手法であり、特定のデータ構造内で目的の要素を高速に特定できる。この種のアルゴリズムは効率が高く、時間計算量は \\(O(\\log n)\\) あるいは \\(O(1)\\) に達するが、通常は追加のデータ構造を必要とする。
  • 実際には、データ規模、探索性能の要件、データの問い合わせ頻度や更新頻度などの要因を具体的に分析し、そのうえで適切な探索手法を選択する必要がある。
  • 線形探索は小規模または頻繁に更新されるデータに適している。二分探索は大規模でソート済みのデータに適している。ハッシュ探索は問い合わせ効率への要求が高く、範囲検索を必要としないデータに適している。木探索は順序の維持と範囲検索のサポートが必要な大規模動的データに適している。
  • ハッシュ探索で線形探索を置き換えることは、実行時間を最適化するための一般的な戦略であり、時間計算量を \\(O(n)\\) から \\(O(1)\\) へと下げられる。
","path":["第 10 章   探索","10.6   まとめ"],"tags":[]},{"location":"chapter_sorting/","level":1,"title":"第 11 章   ソート","text":"

Abstract

ソートは、混沌を秩序へと変える魔法の鍵のようなものであり、データをより効率的に理解し処理することを可能にします。

単純な昇順であれ、複雑な分類配列であれ、ソートはデータの調和のとれた美しさを私たちに示してくれます。

","path":["第 11 章   ソート"],"tags":[]},{"location":"chapter_sorting/#_1","level":2,"title":"章の内容","text":"
  • 11.1   ソートアルゴリズム
  • 11.2   選択ソート
  • 11.3   バブルソート
  • 11.4   挿入ソート
  • 11.5   クイックソート
  • 11.6   マージソート
  • 11.7   ヒープソート
  • 11.8   バケットソート
  • 11.9   計数ソート
  • 11.10   基数ソート
  • 11.11   まとめ
","path":["第 11 章   ソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   バブルソート","text":"

バブルソート(bubble sort)は、隣接する要素を繰り返し比較して交換することで整列を行います。この過程が泡のように下から上へ浮かび上がることから、バブルソートと呼ばれます。

次の図に示すように、バブル処理は要素の交換操作によってシミュレートできます。配列の最も左の端から右へ走査し、隣接する要素の大小を順に比較して、「左要素 > 右要素」であれば両者を交換します。走査が終わると、最大の要素は配列の最も右端へ移動します。

<1><2><3><4><5><6><7>

図 11-4   要素の交換操作でバブル処理をシミュレート

","path":["第 11 章   ソート","11.3   バブルソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1131","level":2,"title":"11.3.1   アルゴリズムの流れ","text":"

配列の長さを \\(n\\) とすると、バブルソートの手順は次の図のとおりです。

  1. まず、\\(n\\) 個の要素に対して「バブル処理」を行い、配列中の最大要素を正しい位置へ交換します。
  2. 次に、残りの \\(n - 1\\) 個の要素に対して「バブル処理」を行い、2 番目に大きい要素を正しい位置へ交換します。
  3. このようにして、\\(n - 1\\) 回の「バブル処理」を終えると、大きいほうから \\(n - 1\\) 個の要素がすべて正しい位置へ交換されます。
  4. 残った 1 つの要素は必ず最小要素なので、並べ替える必要はなく、これで配列のソートが完了します。

図 11-5   バブルソートの流れ

コード例は次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
def bubble_sort(nums: list[int]):\n    \"\"\"バブルソート\"\"\"\n    n = len(nums)\n    # 外側のループ:未ソート区間は [0, i]\n    for i in range(n - 1, 0, -1):\n        # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # nums[j] と nums[j + 1] を交換\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n
bubble_sort.cpp
/* バブルソート */\nvoid bubbleSort(vector<int> &nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換する\n                // ここでは std::swap() 関数を使用する\n                swap(nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
bubble_sort.java
/* バブルソート */\nvoid bubbleSort(int[] nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.cs
/* バブルソート */\nvoid BubbleSort(int[] nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
bubble_sort.go
/* バブルソート */\nfunc bubbleSort(nums []int) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // nums[j] と nums[j + 1] を交換\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n            }\n        }\n    }\n}\n
bubble_sort.swift
/* バブルソート */\nfunc bubbleSort(nums: inout [Int]) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                nums.swapAt(j, j + 1)\n            }\n        }\n    }\n}\n
bubble_sort.js
/* バブルソート */\nfunction bubbleSort(nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.ts
/* バブルソート */\nfunction bubbleSort(nums: number[]): void {\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.dart
/* バブルソート */\nvoid bubbleSort(List<int> nums) {\n  // 外側のループ:未ソート区間は [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // nums[j] と nums[j + 1] を交換\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n      }\n    }\n  }\n}\n
bubble_sort.rs
/* バブルソート */\nfn bubble_sort(nums: &mut [i32]) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i in (1..nums.len()).rev() {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                nums.swap(j, j + 1);\n            }\n        }\n    }\n}\n
bubble_sort.c
/* バブルソート */\nvoid bubbleSort(int nums[], int size) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n            }\n        }\n    }\n}\n
bubble_sort.kt
/* バブルソート */\nfun bubbleSort(nums: IntArray) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n            }\n        }\n    }\n}\n
bubble_sort.rb
### バブルソート ###\ndef bubble_sort(nums)\n  n = nums.length\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (n - 1).downto(1)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.3   バブルソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1132","level":2,"title":"11.3.2   効率の最適化","text":"

ある回の「バブル処理」で交換操作が一度も行われなければ、配列はすでにソート済みであり、結果をそのまま返せることがわかります。したがって、この状況を検出するためのフラグ flag を追加し、発生した時点で直ちに返すようにできます。

最適化後も、バブルソートの最悪時間計算量と平均時間計算量は依然として \\(O(n^2)\\) です。ただし、入力配列が完全に整列済みであれば、最良時間計算量は \\(O(n)\\) に達します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
def bubble_sort_with_flag(nums: list[int]):\n    \"\"\"バブルソート(フラグ最適化)\"\"\"\n    n = len(nums)\n    # 外側のループ:未ソート区間は [0, i]\n    for i in range(n - 1, 0, -1):\n        flag = False  # フラグを初期化する\n        # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # nums[j] と nums[j + 1] を交換\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n                flag = True  # 交換する要素を記録\n        if not flag:\n            break  # このバブル処理で要素交換が一度もなければそのまま終了\n
bubble_sort.cpp
/* バブルソート(フラグ最適化) */\nvoid bubbleSortWithFlag(vector<int> &nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        bool flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換する\n                // ここでは std::swap() 関数を使用する\n                swap(nums[j], nums[j + 1]);\n                flag = true; // 交換する要素を記録\n            }\n        }\n        if (!flag)\n            break; // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.java
/* バブルソート(フラグ最適化) */\nvoid bubbleSortWithFlag(int[] nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        boolean flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // 交換する要素を記録\n            }\n        }\n        if (!flag)\n            break; // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.cs
/* バブルソート(フラグ最適化) */\nvoid BubbleSortWithFlag(int[] nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        bool flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                flag = true;  // 交換する要素を記録\n            }\n        }\n        if (!flag) break;     // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.go
/* バブルソート(フラグ最適化) */\nfunc bubbleSortWithFlag(nums []int) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        flag := false // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // nums[j] と nums[j + 1] を交換\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n                flag = true // 交換する要素を記録\n            }\n        }\n        if flag == false { // このバブル処理で要素交換が一度もなければそのまま終了\n            break\n        }\n    }\n}\n
bubble_sort.swift
/* バブルソート(フラグ最適化) */\nfunc bubbleSortWithFlag(nums: inout [Int]) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        var flag = false // フラグを初期化する\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                nums.swapAt(j, j + 1)\n                flag = true // 交換する要素を記録\n            }\n        }\n        if !flag { // このバブル処理で要素交換が一度もなければそのまま終了\n            break\n        }\n    }\n}\n
bubble_sort.js
/* バブルソート(フラグ最適化) */\nfunction bubbleSortWithFlag(nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // 交換する要素を記録\n            }\n        }\n        if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.ts
/* バブルソート(フラグ最適化) */\nfunction bubbleSortWithFlag(nums: number[]): void {\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // 交換する要素を記録\n            }\n        }\n        if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.dart
/* バブルソート(フラグ最適化) */\nvoid bubbleSortWithFlag(List<int> nums) {\n  // 外側のループ:未ソート区間は [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    bool flag = false; // フラグを初期化する\n    // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // nums[j] と nums[j + 1] を交換\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        flag = true; // 交換する要素を記録\n      }\n    }\n    if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了\n  }\n}\n
bubble_sort.rs
/* バブルソート(フラグ最適化) */\nfn bubble_sort_with_flag(nums: &mut [i32]) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i in (1..nums.len()).rev() {\n        let mut flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                nums.swap(j, j + 1);\n                flag = true; // 交換する要素を記録\n            }\n        }\n        if !flag {\n            break; // このバブル処理で要素交換が一度もなければそのまま終了\n        };\n    }\n}\n
bubble_sort.c
/* バブルソート(フラグ最適化) */\nvoid bubbleSortWithFlag(int nums[], int size) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        bool flag = false;\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n                flag = true;\n            }\n        }\n        if (!flag)\n            break;\n    }\n}\n
bubble_sort.kt
/* バブルソート(フラグ最適化) */\nfun bubbleSortWithFlag(nums: IntArray) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        var flag = false // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                flag = true // 交換する要素を記録\n            }\n        }\n        if (!flag) break // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.rb
### バブルソート ###\ndef bubble_sort(nums)\n  n = nums.length\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (n - 1).downto(1)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n\n# ## バブルソート(フラグ最適化)###\ndef bubble_sort_with_flag(nums)\n  n = nums.length\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (n - 1).downto(1)\n    flag = false # フラグを初期化する\n\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n        flag = true # 交換する要素を記録\n      end\n    end\n\n    break unless flag # このバブル処理で要素交換が一度もなければそのまま終了\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.3   バブルソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1133","level":2,"title":"11.3.3   アルゴリズムの特徴","text":"
  • 時間計算量は \\(O(n^2)\\)、適応的ソート:各回の「バブル処理」で走査する配列の長さは順に \\(n - 1\\)、\\(n - 2\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) であり、その総和は \\((n - 1) n / 2\\) です。flag による最適化を導入すると、最良時間計算量は \\(O(n)\\) に達します。
  • 空間計算量は \\(O(1)\\)、インプレースソート:ポインタ \\(i\\) と \\(j\\) は定数サイズの追加領域しか使用しません。
  • 安定ソート:「バブル処理」では等しい要素に出会っても交換しないためです。
","path":["第 11 章   ソート","11.3   バブルソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/","level":1,"title":"11.8   バケットソート","text":"

前述のいくつかのソートアルゴリズムは、いずれも「比較ベースのソートアルゴリズム」に属し、要素間の大小を比較することで整列を実現します。この種のソートアルゴリズムの時間計算量は \\(O(n \\log n)\\) を超えられません。続いて、時間計算量が線形オーダーに達しうる「非比較ソートアルゴリズム」をいくつか見ていきます。

バケットソート(bucket sort)は分割統治戦略の典型的な応用です。大小関係をもつ複数のバケットを用意し、各バケットがあるデータ範囲に対応するようにして、データを各バケットへ均等に分配します。その後、各バケット内でそれぞれソートを行い、最後にバケットの順序に従ってすべてのデータを結合します。

","path":["第 11 章   ソート","11.8   バケットソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1181","level":2,"title":"11.8.1   アルゴリズムの流れ","text":"

長さ \\(n\\) の配列を考え、その要素は範囲 \\([0, 1)\\) の浮動小数点数であるとします。バケットソートの流れを以下の図に示します。

  1. \\(k\\) 個のバケットを初期化し、\\(n\\) 個の要素を \\(k\\) 個のバケットに分配します。
  2. 各バケットに対してそれぞれソートを実行します(ここではプログラミング言語の組み込みソート関数を用います)。
  3. バケットを小さい順にたどって結果を結合します。

図 11-13   バケットソートの流れ

コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bucket_sort.py
def bucket_sort(nums: list[float]):\n    \"\"\"バケットソート\"\"\"\n    # k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    k = len(nums) // 2\n    buckets = [[] for _ in range(k)]\n    # 1. 配列要素を各バケットに振り分ける\n    for num in nums:\n        # 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        i = int(num * k)\n        # num をバケット i に追加\n        buckets[i].append(num)\n    # 2. 各バケットをソートする\n    for bucket in buckets:\n        # 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.sort()\n    # 3. バケットを走査して結果を結合\n    i = 0\n    for bucket in buckets:\n        for num in bucket:\n            nums[i] = num\n            i += 1\n
bucket_sort.cpp
/* バケットソート */\nvoid bucketSort(vector<float> &nums) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    int k = nums.size() / 2;\n    vector<vector<float>> buckets(k);\n    // 1. 配列要素を各バケットに振り分ける\n    for (float num : nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        int i = num * k;\n        // num をバケット bucket_idx に追加\n        buckets[i].push_back(num);\n    }\n    // 2. 各バケットをソートする\n    for (vector<float> &bucket : buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        sort(bucket.begin(), bucket.end());\n    }\n    // 3. バケットを走査して結果を結合\n    int i = 0;\n    for (vector<float> &bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.java
/* バケットソート */\nvoid bucketSort(float[] nums) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    int k = nums.length / 2;\n    List<List<Float>> buckets = new ArrayList<>();\n    for (int i = 0; i < k; i++) {\n        buckets.add(new ArrayList<>());\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for (float num : nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        int i = (int) (num * k);\n        // num をバケット i に追加\n        buckets.get(i).add(num);\n    }\n    // 2. 各バケットをソートする\n    for (List<Float> bucket : buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        Collections.sort(bucket);\n    }\n    // 3. バケットを走査して結果を結合\n    int i = 0;\n    for (List<Float> bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.cs
/* バケットソート */\nvoid BucketSort(float[] nums) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    int k = nums.Length / 2;\n    List<List<float>> buckets = [];\n    for (int i = 0; i < k; i++) {\n        buckets.Add([]);\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    foreach (float num in nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        int i = (int)(num * k);\n        // num をバケット i に追加\n        buckets[i].Add(num);\n    }\n    // 2. 各バケットをソートする\n    foreach (List<float> bucket in buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.Sort();\n    }\n    // 3. バケットを走査して結果を結合\n    int j = 0;\n    foreach (List<float> bucket in buckets) {\n        foreach (float num in bucket) {\n            nums[j++] = num;\n        }\n    }\n}\n
bucket_sort.go
/* バケットソート */\nfunc bucketSort(nums []float64) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    k := len(nums) / 2\n    buckets := make([][]float64, k)\n    for i := 0; i < k; i++ {\n        buckets[i] = make([]float64, 0)\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for _, num := range nums {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        i := int(num * float64(k))\n        // num をバケット i に追加\n        buckets[i] = append(buckets[i], num)\n    }\n    // 2. 各バケットをソートする\n    for i := 0; i < k; i++ {\n        // 組み込みのスライスソート関数を使う。ほかのソートアルゴリズムに置き換えてもよい\n        sort.Float64s(buckets[i])\n    }\n    // 3. バケットを走査して結果を結合\n    i := 0\n    for _, bucket := range buckets {\n        for _, num := range bucket {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
bucket_sort.swift
/* バケットソート */\nfunc bucketSort(nums: inout [Double]) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    let k = nums.count / 2\n    var buckets = (0 ..< k).map { _ in [Double]() }\n    // 1. 配列要素を各バケットに振り分ける\n    for num in nums {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        let i = Int(num * Double(k))\n        // num をバケット i に追加\n        buckets[i].append(num)\n    }\n    // 2. 各バケットをソートする\n    for i in buckets.indices {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        buckets[i].sort()\n    }\n    // 3. バケットを走査して結果を結合\n    var i = nums.startIndex\n    for bucket in buckets {\n        for num in bucket {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
bucket_sort.js
/* バケットソート */\nfunction bucketSort(nums) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    const k = nums.length / 2;\n    const buckets = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for (const num of nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        const i = Math.floor(num * k);\n        // num をバケット i に追加\n        buckets[i].push(num);\n    }\n    // 2. 各バケットをソートする\n    for (const bucket of buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. バケットを走査して結果を結合\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.ts
/* バケットソート */\nfunction bucketSort(nums: number[]): void {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    const k = nums.length / 2;\n    const buckets: number[][] = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for (const num of nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        const i = Math.floor(num * k);\n        // num をバケット i に追加\n        buckets[i].push(num);\n    }\n    // 2. 各バケットをソートする\n    for (const bucket of buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. バケットを走査して結果を結合\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.dart
/* バケットソート */\nvoid bucketSort(List<double> nums) {\n  // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n  int k = nums.length ~/ 2;\n  List<List<double>> buckets = List.generate(k, (index) => []);\n\n  // 1. 配列要素を各バケットに振り分ける\n  for (double _num in nums) {\n    // 入力データの範囲は [0, 1) であり、_num * k を用いてインデックス範囲 [0, k-1] に写像する\n    int i = (_num * k).toInt();\n    // _num をバケット bucket_idx に追加\n    buckets[i].add(_num);\n  }\n  // 2. 各バケットをソートする\n  for (List<double> bucket in buckets) {\n    bucket.sort();\n  }\n  // 3. バケットを走査して結果を結合\n  int i = 0;\n  for (List<double> bucket in buckets) {\n    for (double _num in bucket) {\n      nums[i++] = _num;\n    }\n  }\n}\n
bucket_sort.rs
/* バケットソート */\nfn bucket_sort(nums: &mut [f64]) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    let k = nums.len() / 2;\n    let mut buckets = vec![vec![]; k];\n    // 1. 配列要素を各バケットに振り分ける\n    for &num in nums.iter() {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        let i = (num * k as f64) as usize;\n        // num をバケット i に追加\n        buckets[i].push(num);\n    }\n    // 2. 各バケットをソートする\n    for bucket in &mut buckets {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());\n    }\n    // 3. バケットを走査して結果を結合\n    let mut i = 0;\n    for bucket in buckets.iter() {\n        for &num in bucket.iter() {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
bucket_sort.c
/* バケットソート */\nvoid bucketSort(float nums[], int n) {\n    int k = n / 2;                                 // k = n/2 個のバケットを初期化する\n    int *sizes = malloc(k * sizeof(int));          // 各バケットのサイズを記録する\n    float **buckets = malloc(k * sizeof(float *)); // 動的配列の配列(バケット)\n    // 各バケットに十分な容量を事前確保する\n    for (int i = 0; i < k; ++i) {\n        buckets[i] = (float *)malloc(n * sizeof(float));\n        sizes[i] = 0;\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for (int i = 0; i < n; ++i) {\n        int idx = (int)(nums[i] * k);\n        buckets[idx][sizes[idx]++] = nums[i];\n    }\n    // 2. 各バケットをソートする\n    for (int i = 0; i < k; ++i) {\n        qsort(buckets[i], sizes[i], sizeof(float), compare);\n    }\n    // 3. ソート済みのバケットを結合する\n    int idx = 0;\n    for (int i = 0; i < k; ++i) {\n        for (int j = 0; j < sizes[i]; ++j) {\n            nums[idx++] = buckets[i][j];\n        }\n        // メモリを解放する\n        free(buckets[i]);\n    }\n}\n
bucket_sort.kt
/* バケットソート */\nfun bucketSort(nums: FloatArray) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    val k = nums.size / 2\n    val buckets = mutableListOf<MutableList<Float>>()\n    for (i in 0..<k) {\n        buckets.add(mutableListOf())\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for (num in nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        val i = (num * k).toInt()\n        // num をバケット i に追加\n        buckets[i].add(num)\n    }\n    // 2. 各バケットをソートする\n    for (bucket in buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.sort()\n    }\n    // 3. バケットを走査して結果を結合\n    var i = 0\n    for (bucket in buckets) {\n        for (num in bucket) {\n            nums[i++] = num\n        }\n    }\n}\n
bucket_sort.rb
### バケットソート ###\ndef bucket_sort(nums)\n  # k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n  k = nums.length / 2\n  buckets = Array.new(k) { [] }\n\n  # 1. 配列要素を各バケットに振り分ける\n  nums.each do |num|\n    # 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n    i = (num * k).to_i\n    # num をバケット i に追加\n    buckets[i] << num\n  end\n\n  # 2. 各バケットをソートする\n  buckets.each do |bucket|\n    # 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n    bucket.sort!\n  end\n\n  # 3. バケットを走査して結果を結合\n  i = 0\n  buckets.each do |bucket|\n    bucket.each do |num|\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.8   バケットソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1182","level":2,"title":"11.8.2   アルゴリズムの特性","text":"

バケットソートは、非常に大規模なデータの処理に適しています。たとえば、入力データに 100 万個の要素が含まれ、空間の制約によりシステムメモリへすべてのデータを一度に読み込めない場合です。このとき、データを 1000 個のバケットに分け、それぞれのバケットを個別にソートしてから、最後に結果を結合できます。

  • 時間計算量は \\(O(n + k)\\) :要素が各バケット内に平均的に分布していると仮定すると、各バケット内の要素数は \\(\\frac{n}{k}\\) です。1 つのバケットをソートするのに \\(O(\\frac{n}{k} \\log\\frac{n}{k})\\) の時間がかかるなら、すべてのバケットのソートには \\(O(n \\log\\frac{n}{k})\\) の時間がかかります。バケット数 \\(k\\) が十分大きいとき、時間計算量は \\(O(n)\\) に近づきます。結果を結合する際には、すべてのバケットと要素を走査する必要があり、\\(O(n + k)\\) の時間を要します。最悪の場合、すべてのデータが 1 つのバケットに割り当てられ、そのバケットのソートに \\(O(n^2)\\) の時間がかかります。
  • 空間計算量は \\(O(n + k)\\)、非インプレースソート:\\(k\\) 個のバケットと合計 \\(n\\) 個の要素ぶんの追加領域が必要です。
  • バケットソートが安定かどうかは、バケット内要素のソートに用いるアルゴリズムが安定かどうかに依存します。
","path":["第 11 章   ソート","11.8   バケットソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1183","level":2,"title":"11.8.3   均等な分配を実現するには","text":"

バケットソートの時間計算量は理論上 \\(O(n)\\) に達しますが、鍵は要素を各バケットへ均等に分配すること にあります。実際のデータは均一に分布していないことが多いからです。たとえば、Taobao 上のすべての商品を価格帯ごとに 10 個のバケットへ均等に分けたいとしても、商品の価格分布は偏っており、100 元未満は非常に多く、1000 元超は非常に少ないかもしれません。価格区間を単純に 10 等分すると、各バケットの商品数には大きな差が生じます。

均等な分配を実現するために、まず大まかな境界線を設定し、データをひとまず 3 個のバケットに粗く振り分けます。分配後は、商品数の多いバケットをさらに 3 個のバケットに分割し、すべてのバケット内の要素数がおおむね等しくなるまでこれを続けます。

以下の図に示すように、この方法の本質は再帰木を構築することにあり、目標は葉ノードの値をできるだけ均等にすることです。もちろん、毎回データを 3 個のバケットに分割する必要はなく、具体的な分け方はデータの特徴に応じて柔軟に選べます。

図 11-14   再帰的にバケットを分割

商品価格の確率分布をあらかじめ把握しているなら、データの確率分布に基づいて各バケットの価格境界を設定できます。なお、データ分布は必ずしも特別に統計を取る必要はなく、データの特徴に応じて何らかの確率モデルで近似することもできます。

以下の図に示すように、商品価格が正規分布に従うと仮定すれば、価格区間を合理的に設定でき、それによって商品を各バケットへ均等に分配できます。

図 11-15   確率分布に基づいてバケットを分割

","path":["第 11 章   ソート","11.8   バケットソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/","level":1,"title":"11.9   計数ソート","text":"

計数ソート(counting sort)は要素数を集計することでソートを実現し、通常は整数配列に適用されます。

","path":["第 11 章   ソート","11.9   計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1191","level":2,"title":"11.9.1   単純な実装","text":"

まず簡単な例を見てみましょう。長さ \\(n\\) の配列 nums が与えられ、その要素はすべて「非負整数」であるとします。計数ソートの全体的な流れを以下の図に示します。

  1. 配列を走査し、その中の最大値を見つけて \\(m\\) とし、続いて長さ \\(m + 1\\) の補助配列 counter を作成します。
  2. counter を用いて nums 内の各数値の出現回数を集計します。ここで counter[num] は数値 num の出現回数に対応します。集計方法は非常に簡単で、nums を走査し(現在の数値を num とする)、各回で counter[num] を \\(1\\) 増やせばよいです。
  3. counter の各インデックスは自然に順序づけられているため、すべての数値はすでに整列された状態とみなせます。続いて counter を走査し、各数値の出現回数に応じて小さい順に nums へ書き戻せば完了です。

図 11-16   計数ソートの流れ

コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
def counting_sort_naive(nums: list[int]):\n    \"\"\"計数ソート\"\"\"\n    # 簡易版。オブジェクトのソートには使えない\n    # 1. 配列の最大要素 m を求める\n    m = max(nums)\n    # 2. 各数値の出現回数を数える\n    # counter[num] は num の出現回数を表す\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    i = 0\n    for num in range(m + 1):\n        for _ in range(counter[num]):\n            nums[i] = num\n            i += 1\n
counting_sort.cpp
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nvoid countingSortNaive(vector<int> &nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.java
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nvoid countingSortNaive(int[] nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.cs
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nvoid CountingSortNaive(int[] nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.go
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfunc countingSortNaive(nums []int) {\n    // 1. 配列の最大要素 m を求める\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    for i, num := 0, 0; num < m+1; num++ {\n        for j := 0; j < counter[num]; j++ {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
counting_sort.swift
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfunc countingSortNaive(nums: inout [Int]) {\n    // 1. 配列の最大要素 m を求める\n    let m = nums.max()!\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    var i = 0\n    for num in 0 ..< m + 1 {\n        for _ in 0 ..< counter[num] {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
counting_sort.js
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfunction countingSortNaive(nums) {\n    // 1. 配列の最大要素 m を求める\n    let m = Math.max(...nums);\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.ts
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfunction countingSortNaive(nums: number[]): void {\n    // 1. 配列の最大要素 m を求める\n    let m: number = Math.max(...nums);\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.dart
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nvoid countingSortNaive(List<int> nums) {\n  // 1. 配列の最大要素 m を求める\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. 各数値の出現回数を数える\n  // counter[_num] は _num の出現回数を表す\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n  int i = 0;\n  for (int _num = 0; _num < m + 1; _num++) {\n    for (int j = 0; j < counter[_num]; j++, i++) {\n      nums[i] = _num;\n    }\n  }\n}\n
counting_sort.rs
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfn counting_sort_naive(nums: &mut [i32]) {\n    // 1. 配列の最大要素 m を求める\n    let m = *nums.iter().max().unwrap();\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    let mut counter = vec![0; m as usize + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    let mut i = 0;\n    for num in 0..m + 1 {\n        for _ in 0..counter[num as usize] {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
counting_sort.c
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nvoid countingSortNaive(int nums[], int size) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int *counter = calloc(m + 1, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n    // 4. メモリを解放する\n    free(counter);\n}\n
counting_sort.kt
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfun countingSortNaive(nums: IntArray) {\n    // 1. 配列の最大要素 m を求める\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    var i = 0\n    for (num in 0..<m + 1) {\n        var j = 0\n        while (j < counter[num]) {\n            nums[i] = num\n            j++\n            i++\n        }\n    }\n}\n
counting_sort.rb
### 計数ソート ###\ndef counting_sort_naive(nums)\n  # 簡易版。オブジェクトのソートには使えない\n  # 1. 配列の最大要素 m を求める\n  m = 0\n  nums.each { |num| m = [m, num].max }\n  # 2. 各数値の出現回数を数える\n  # counter[num] は num の出現回数を表す\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. counter を走査し、各要素を元の配列 nums に書き戻す\n  i = 0\n  for num in 0...(m + 1)\n    (0...counter[num]).each do\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
コードの可視化

全画面で見る >

計数ソートとバケットソートの関係

バケットソートの観点から見ると、計数ソートにおける計数配列 counter の各インデックスを 1 つのバケットとみなし、個数を数える過程を各要素を対応するバケットへ振り分ける操作とみなせます。本質的には、計数ソートは整数データにおけるバケットソートの特殊な一例です。

","path":["第 11 章   ソート","11.9   計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1192","level":2,"title":"11.9.2   完全な実装","text":"

注意深い読者なら、**入力データがオブジェクトである場合、上記の手順 3. は機能しない**ことに気づくかもしれません。入力データが商品オブジェクトであり、商品価格(クラスのメンバ変数)に基づいて商品をソートしたいとします。しかし上記のアルゴリズムが返せるのは価格のソート結果だけです。

では、元のデータのソート結果を得るにはどうすればよいのでしょうか。まず counter の「累積和」を計算します。名前のとおり、インデックス i における累積和 prefix[i] は、配列の先頭から i 番目までの要素の総和に等しくなります:

\\[ \\text{prefix}[i] = \\sum_{j=0}^i \\text{counter[j]} \\]

累積和には明確な意味があり、prefix[num] - 1 は要素 num が結果配列 res に最後に現れるインデックスを表します。この情報は非常に重要で、各要素が結果配列のどの位置に現れるべきかを示してくれます。続いて元の配列 nums を逆順に走査し、各要素 num に対して各反復で次の 2 つの手順を行います。

  1. num を配列 res のインデックス prefix[num] - 1 に格納します。
  2. 累積和 prefix[num] を \\(1\\) 減らし、次に num を配置するインデックスを得ます。

走査が完了すると、配列 res にソート済みの結果が格納されます。最後に res で元の配列 nums を上書きすれば完了です。以下の図は完全な計数ソートの流れを示しています。

<1><2><3><4><5><6><7><8>

図 11-17   計数ソートの手順

計数ソートの実装コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
def counting_sort(nums: list[int]):\n    \"\"\"計数ソート\"\"\"\n    # 完全版。オブジェクトをソートでき、かつ安定ソートである\n    # 1. 配列の最大要素 m を求める\n    m = max(nums)\n    # 2. 各数値の出現回数を数える\n    # counter[num] は num の出現回数を表す\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    # つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for i in range(m):\n        counter[i + 1] += counter[i]\n    # 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    # 結果を記録するための配列 res を初期化\n    n = len(nums)\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        num = nums[i]\n        res[counter[num] - 1] = num  # num を対応するインデックスに配置\n        counter[num] -= 1  # 累積和を 1 減らして、次に num を配置するインデックスを得る\n    # 結果配列 res で元の配列 nums を上書きする\n    for i in range(n):\n        nums[i] = res[i]\n
counting_sort.cpp
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nvoid countingSort(vector<int> &nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    int n = nums.size();\n    vector<int> res(n);\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--;              // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    nums = res;\n}\n
counting_sort.java
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nvoid countingSort(int[] nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    int n = nums.length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.cs
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nvoid CountingSort(int[] nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    int n = nums.Length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.go
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfunc countingSort(nums []int) {\n    // 1. 配列の最大要素 m を求める\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for i := 0; i < m; i++ {\n        counter[i+1] += counter[i]\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    n := len(nums)\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        num := nums[i]\n        // num を対応するインデックスに配置\n        res[counter[num]-1] = num\n        // 累積和を 1 減らして、次に num を配置するインデックスを得る\n        counter[num]--\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    copy(nums, res)\n}\n
counting_sort.swift
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfunc countingSort(nums: inout [Int]) {\n    // 1. 配列の最大要素 m を求める\n    let m = nums.max()!\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for i in 0 ..< m {\n        counter[i + 1] += counter[i]\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let num = nums[i]\n        res[counter[num] - 1] = num // num を対応するインデックスに配置\n        counter[num] -= 1 // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n
counting_sort.js
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfunction countingSort(nums) {\n    // 1. 配列の最大要素 m を求める\n    let m = Math.max(...nums);\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    const n = nums.length;\n    const res = new Array(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.ts
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfunction countingSort(nums: number[]): void {\n    // 1. 配列の最大要素 m を求める\n    let m: number = Math.max(...nums);\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    const n = nums.length;\n    const res: number[] = new Array<number>(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.dart
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nvoid countingSort(List<int> nums) {\n  // 1. 配列の最大要素 m を求める\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. 各数値の出現回数を数える\n  // counter[_num] は _num の出現回数を表す\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. counter の累積和を求め、「出現回数」を「末尾インデックス」に変換する\n  // つまり counter[_num]-1 は、res において _num が最後に出現する位置のインデックスである\n  for (int i = 0; i < m; i++) {\n    counter[i + 1] += counter[i];\n  }\n  // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n  // 結果を記録するための配列 res を初期化\n  int n = nums.length;\n  List<int> res = List.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int _num = nums[i];\n    res[counter[_num] - 1] = _num; // _num を対応する添字に配置\n    counter[_num]--; // 累積和を 1 減らし、次に _num を配置するインデックスを得る\n  }\n  // 結果配列 res で元の配列 nums を上書きする\n  nums.setAll(0, res);\n}\n
counting_sort.rs
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfn counting_sort(nums: &mut [i32]) {\n    // 1. 配列の最大要素 m を求める\n    let m = *nums.iter().max().unwrap() as usize;\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    let mut counter = vec![0; m + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for i in 0..m {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    let n = nums.len();\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let num = nums[i];\n        res[counter[num as usize] - 1] = num; // num を対応するインデックスに配置\n        counter[num as usize] -= 1; // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    nums.copy_from_slice(&res)\n}\n
counting_sort.c
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nvoid countingSort(int nums[], int size) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int *counter = calloc(m, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    int *res = malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--;              // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    memcpy(nums, res, size * sizeof(int));\n    // 5. メモリを解放する\n    free(res);\n    free(counter);\n}\n
counting_sort.kt
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfun countingSort(nums: IntArray) {\n    // 1. 配列の最大要素 m を求める\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (i in 0..<m) {\n        counter[i + 1] += counter[i]\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    val n = nums.size\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val num = nums[i]\n        res[counter[num] - 1] = num // num を対応するインデックスに配置\n        counter[num]-- // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for (i in 0..<n) {\n        nums[i] = res[i]\n    }\n}\n
counting_sort.rb
### 計数ソート ###\ndef counting_sort(nums)\n  # 完全版。オブジェクトをソートでき、かつ安定ソートである\n  # 1. 配列の最大要素 m を求める\n  m = nums.max\n  # 2. 各数値の出現回数を数える\n  # counter[num] は num の出現回数を表す\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n  # つまり counter[num]-1 は、num が res に最後に現れるインデックス\n  (0...m).each { |i| counter[i + 1] += counter[i] }\n  # 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n  # 結果を記録するための配列 res を初期化する\n  n = nums.length\n  res = Array.new(n, 0)\n  (n - 1).downto(0).each do |i|\n    num = nums[i]\n    res[counter[num] - 1] = num # num を対応するインデックスに配置\n    counter[num] -= 1 # 累積和を 1 減らして、次に num を配置するインデックスを得る\n  end\n  # 結果配列 res で元の配列 nums を上書きする\n  (0...n).each { |i| nums[i] = res[i] }\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.9   計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1193","level":2,"title":"11.9.3   アルゴリズムの特性","text":"
  • 時間計算量は \\(O(n + m)\\)、非適応ソート :nums の走査と counter の走査が含まれ、いずれも線形時間です。一般には \\(n \\gg m\\) であり、時間計算量は \\(O(n)\\) に近づきます。
  • 空間計算量は \\(O(n + m)\\)、非インプレースソート:長さがそれぞれ \\(n\\) と \\(m\\) の配列 rescounter を利用します。
  • 安定ソート:res に要素を埋める順序が「右から左」であるため、nums を逆順に走査することで等しい要素どうしの相対位置が変化するのを防ぎ、安定ソートを実現できます。実際には、nums を順方向に走査しても正しいソート結果は得られますが、その結果は安定ではありません。
","path":["第 11 章   ソート","11.9   計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1194","level":2,"title":"11.9.4   制約","text":"

ここまで読むと、計数ソートは非常に巧妙で、個数を数えるだけで効率的なソートを実現できると感じるかもしれません。しかし、計数ソートを利用するための前提条件は比較的厳格です。

計数ソートは非負整数にしか適用できません。ほかの型のデータに適用したい場合は、それらのデータを非負整数に変換でき、かつ変換の過程で要素間の相対的な大小関係が変わらないことを保証する必要があります。たとえば、負数を含む整数配列に対しては、すべての数値に定数を加えて正数へ変換し、ソート後に元へ戻すことができます。

計数ソートはデータ量が多く、値域が小さい場合に適しています。たとえば上記の例では \\(m\\) が大きすぎてはならず、そうでないと過剰な空間を消費します。また、\\(n \\ll m\\) のとき、計数ソートは \\(O(m)\\) 時間を要するため、\\(O(n \\log n)\\) のソートアルゴリズムより遅くなる可能性があります。

","path":["第 11 章   ソート","11.9   計数ソート"],"tags":[]},{"location":"chapter_sorting/heap_sort/","level":1,"title":"11.7   ヒープソート","text":"

Tip

本節を読む前に、「ヒープ」の章を学習済みであることを確認してください。

ヒープソート(heap sort)は、ヒープデータ構造に基づいて実装される効率的なソートアルゴリズムです。すでに学んだ「ヒープ構築操作」と「要素の取り出し操作」を利用してヒープソートを実現できます。

  1. 配列を入力して最小ヒープを構築すると、このとき最小要素はヒープの頂点にあります。
  2. 取り出し操作を繰り返し実行し、取り出された要素を順に記録すれば、昇順に並んだ列が得られます。

以上の方法でも実行できますが、取り出した要素を保存するために追加の配列が必要となり、空間をやや無駄にします。実際には、通常はより洗練された実装方法を用います。

","path":["第 11 章   ソート","11.7   ヒープソート"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1171","level":2,"title":"11.7.1   アルゴリズムの流れ","text":"

配列の長さを \\(n\\) とすると、ヒープソートの流れは次図のとおりです。

  1. 配列を入力して最大ヒープを構築します。完了後、最大要素はヒープの頂点にあります。
  2. ヒープ頂点の要素(最初の要素)とヒープ末尾の要素(最後の要素)を交換します。交換後、ヒープの長さは \\(1\\) 減少し、整列済み要素数は \\(1\\) 増加します。
  3. ヒープ頂点の要素から始めて、上から下へヒープ化操作(sift down)を実行します。ヒープ化が完了すると、ヒープの性質が回復します。
  4. 2. ステップと第 3. ステップを繰り返し実行します。これを \\(n - 1\\) 回繰り返すと、配列の整列が完了します。

Tip

実際には、要素の取り出し操作にも第 2. ステップと第 3. ステップが含まれており、要素を取り出す処理が 1 つ加わるだけです。

<1><2><3><4><5><6><7><8><9><10><11><12>

図 11-12   ヒープソートの手順

コード実装では、「ヒープ」の章と同じ上から下へのヒープ化 sift_down() 関数を使用します。注意すべき点として、ヒープの長さは最大要素を取り出すたびに短くなるため、sift_down() 関数に長さパラメータ \\(n\\) を追加し、ヒープの現在の有効な長さを指定する必要があります。コードは以下のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap_sort.py
def sift_down(nums: list[int], n: int, i: int):\n    \"\"\"ヒープの長さは n。ノード i から下方向にヒープ化\"\"\"\n    while True:\n        # ノード i, l, r のうち値が最大のノードを ma とする\n        l = 2 * i + 1\n        r = 2 * i + 2\n        ma = i\n        if l < n and nums[l] > nums[ma]:\n            ma = l\n        if r < n and nums[r] > nums[ma]:\n            ma = r\n        # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i:\n            break\n        # 2 つのノードを交換\n        nums[i], nums[ma] = nums[ma], nums[i]\n        # ループで上から下へヒープ化\n        i = ma\n\ndef heap_sort(nums: list[int]):\n    \"\"\"ヒープソート\"\"\"\n    # ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for i in range(len(nums) // 2 - 1, -1, -1):\n        sift_down(nums, len(nums), i)\n    # ヒープから最大要素を取り出し、n-1 回繰り返す\n    for i in range(len(nums) - 1, 0, -1):\n        # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        nums[0], nums[i] = nums[i], nums[0]\n        # 根ノードを起点に、上から下へヒープ化\n        sift_down(nums, i, 0)\n
heap_sort.cpp
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nvoid siftDown(vector<int> &nums, int n, int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i) {\n            break;\n        }\n        // 2 つのノードを交換\n        swap(nums[i], nums[ma]);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid heapSort(vector<int> &nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (int i = nums.size() / 2 - 1; i >= 0; --i) {\n        siftDown(nums, nums.size(), i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (int i = nums.size() - 1; i > 0; --i) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        swap(nums[0], nums[i]);\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.java
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nvoid siftDown(int[] nums, int n, int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i)\n            break;\n        // 2 つのノードを交換\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid heapSort(int[] nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (int i = nums.length / 2 - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (int i = nums.length - 1; i > 0; i--) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.cs
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nvoid SiftDown(int[] nums, int n, int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i)\n            break;\n        // 2 つのノードを交換\n        (nums[ma], nums[i]) = (nums[i], nums[ma]);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid HeapSort(int[] nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (int i = nums.Length / 2 - 1; i >= 0; i--) {\n        SiftDown(nums, nums.Length, i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        (nums[i], nums[0]) = (nums[0], nums[i]);\n        // 根ノードを起点に、上から下へヒープ化\n        SiftDown(nums, i, 0);\n    }\n}\n
heap_sort.go
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfunc siftDown(nums *[]int, n, i int) {\n    for true {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        l := 2*i + 1\n        r := 2*i + 2\n        ma := i\n        if l < n && (*nums)[l] > (*nums)[ma] {\n            ma = l\n        }\n        if r < n && (*nums)[r] > (*nums)[ma] {\n            ma = r\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i {\n            break\n        }\n        // 2 つのノードを交換\n        (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n\n/* ヒープソート */\nfunc heapSort(nums *[]int) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for i := len(*nums)/2 - 1; i >= 0; i-- {\n        siftDown(nums, len(*nums), i)\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for i := len(*nums) - 1; i > 0; i-- {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0)\n    }\n}\n
heap_sort.swift
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfunc siftDown(nums: inout [Int], n: Int, i: Int) {\n    var i = i\n    while true {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let l = 2 * i + 1\n        let r = 2 * i + 2\n        var ma = i\n        if l < n, nums[l] > nums[ma] {\n            ma = l\n        }\n        if r < n, nums[r] > nums[ma] {\n            ma = r\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i {\n            break\n        }\n        // 2 つのノードを交換\n        nums.swapAt(i, ma)\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n\n/* ヒープソート */\nfunc heapSort(nums: inout [Int]) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {\n        siftDown(nums: &nums, n: nums.count, i: i)\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for i in nums.indices.dropFirst().reversed() {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        nums.swapAt(0, i)\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums: &nums, n: i, i: 0)\n    }\n}\n
heap_sort.js
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfunction siftDown(nums, n, i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma === i) {\n            break;\n        }\n        // 2 つのノードを交換\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfunction heapSort(nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.ts
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfunction siftDown(nums: number[], n: number, i: number): void {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma === i) {\n            break;\n        }\n        // 2 つのノードを交換\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfunction heapSort(nums: number[]): void {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.dart
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nvoid siftDown(List<int> nums, int n, int i) {\n  while (true) {\n    // ノード i, l, r のうち値が最大のノードを ma とする\n    int l = 2 * i + 1;\n    int r = 2 * i + 2;\n    int ma = i;\n    if (l < n && nums[l] > nums[ma]) ma = l;\n    if (r < n && nums[r] > nums[ma]) ma = r;\n    // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n    if (ma == i) break;\n    // 2 つのノードを交換\n    int temp = nums[i];\n    nums[i] = nums[ma];\n    nums[ma] = temp;\n    // ループで上から下へヒープ化\n    i = ma;\n  }\n}\n\n/* ヒープソート */\nvoid heapSort(List<int> nums) {\n  // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n  for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {\n    siftDown(nums, nums.length, i);\n  }\n  // ヒープから最大要素を取り出し、n-1 回繰り返す\n  for (int i = nums.length - 1; i > 0; i--) {\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    int tmp = nums[0];\n    nums[0] = nums[i];\n    nums[i] = tmp;\n    // 根ノードを起点に、上から下へヒープ化\n    siftDown(nums, i, 0);\n  }\n}\n
heap_sort.rs
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfn sift_down(nums: &mut [i32], n: usize, mut i: usize) {\n    loop {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let mut ma = i;\n        if l < n && nums[l] > nums[ma] {\n            ma = l;\n        }\n        if r < n && nums[r] > nums[ma] {\n            ma = r;\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i {\n            break;\n        }\n        // 2 つのノードを交換\n        nums.swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfn heap_sort(nums: &mut [i32]) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for i in (0..nums.len() / 2).rev() {\n        sift_down(nums, nums.len(), i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for i in (1..nums.len()).rev() {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        nums.swap(0, i);\n        // 根ノードを起点に、上から下へヒープ化\n        sift_down(nums, i, 0);\n    }\n}\n
heap_sort.c
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nvoid siftDown(int nums[], int n, int i) {\n    while (1) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i) {\n            break;\n        }\n        // 2 つのノードを交換\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid heapSort(int nums[], int n) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (int i = n / 2 - 1; i >= 0; --i) {\n        siftDown(nums, n, i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (int i = n - 1; i > 0; --i) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.kt
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfun siftDown(nums: IntArray, n: Int, li: Int) {\n    var i = li\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        val l = 2 * i + 1\n        val r = 2 * i + 2\n        var ma = i\n        if (l < n && nums[l] > nums[ma]) \n            ma = l\n        if (r < n && nums[r] > nums[ma]) \n            ma = r\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i) \n            break\n        // 2 つのノードを交換\n        val temp = nums[i]\n        nums[i] = nums[ma]\n        nums[ma] = temp\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n\n/* ヒープソート */\nfun heapSort(nums: IntArray) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (i in nums.size / 2 - 1 downTo 0) {\n        siftDown(nums, nums.size, i)\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (i in nums.size - 1 downTo 1) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        val temp = nums[0]\n        nums[0] = nums[i]\n        nums[i] = temp\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0)\n    }\n}\n
heap_sort.rb
### ヒープ長 n で、ノード i から上から下へヒープ化 ###\ndef sift_down(nums, n, i)\n  while true\n    # ノード i, l, r のうち値が最大のノードを ma とする\n    l = 2 * i + 1\n    r = 2 * i + 2\n    ma = i\n    ma = l if l < n && nums[l] > nums[ma]\n    ma = r if r < n && nums[r] > nums[ma]\n    # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n    break if ma == i\n    # 2 つのノードを交換\n    nums[i], nums[ma] = nums[ma], nums[i]\n    # ループで上から下へヒープ化\n    i = ma\n  end\nend\n\n### ヒープソート ###\ndef heap_sort(nums)\n  # ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n  (nums.length / 2 - 1).downto(0) do |i|\n    sift_down(nums, nums.length, i)\n  end\n  # ヒープから最大要素を取り出し、n-1 回繰り返す\n  (nums.length - 1).downto(1) do |i|\n    # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    nums[0], nums[i] = nums[i], nums[0]\n    # 根ノードを起点に、上から下へヒープ化\n    sift_down(nums, i, 0)\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.7   ヒープソート"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1172","level":2,"title":"11.7.2   アルゴリズムの特性","text":"
  • 時間計算量は \\(O(n \\log n)\\)、非適応ソート:ヒープ構築操作には \\(O(n)\\) の時間がかかります。ヒープから最大要素を取り出す時間計算量は \\(O(\\log n)\\) であり、これを合計 \\(n - 1\\) 回繰り返します。
  • 空間計算量は \\(O(1)\\)、インプレースソート:いくつかのポインタ変数が使う空間は \\(O(1)\\) です。要素の交換とヒープ化操作はいずれも元の配列上で行われます。
  • 非安定ソート:ヒープ頂点の要素とヒープ末尾の要素を交換する際、等しい要素どうしの相対位置が変化する可能性があります。
","path":["第 11 章   ソート","11.7   ヒープソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/","level":1,"title":"11.4   挿入ソート","text":"

挿入ソート(insertion sort)は単純なソートアルゴリズムであり、その動作原理は手作業でトランプの山を整える過程と非常によく似ています。

具体的には、未ソート区間から基準要素を 1 つ選び、その要素を左側の整列済み区間の要素と 1 つずつ比較し、正しい位置に挿入します。

以下の図は、配列に要素を挿入する操作の流れを示しています。基準要素を base とすると、目的のインデックスから base までのすべての要素を 1 つずつ右に移動し、その後 base を目的のインデックスに代入する必要があります。

図 11-6   1 回の挿入操作

","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141","level":2,"title":"11.4.1   アルゴリズムの流れ","text":"

挿入ソート全体の流れを以下の図に示します。

  1. 初期状態では、配列の 1 番目の要素はすでに整列済みです。
  2. 配列の 2 番目の要素を base として選び、正しい位置に挿入すると、**配列の先頭 2 要素が整列済み**になります。
  3. 3 番目の要素を base として選び、正しい位置に挿入すると、**配列の先頭 3 要素が整列済み**になります。
  4. このように繰り返し、最後のラウンドで最後の要素を base として選んで正しい位置に挿入すると、**すべての要素が整列済み**になります。

図 11-7   挿入ソートの流れ

コード例は以下のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby insertion_sort.py
def insertion_sort(nums: list[int]):\n    \"\"\"挿入ソート\"\"\"\n    # 外側ループ:整列済み区間は [0, i-1]\n    for i in range(1, len(nums)):\n        base = nums[i]\n        j = i - 1\n        # 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while j >= 0 and nums[j] > base:\n            nums[j + 1] = nums[j]  # nums[j] を 1 つ右へ移動する\n            j -= 1\n        nums[j + 1] = base  # base を正しい位置に配置する\n
insertion_sort.cpp
/* 挿入ソート */\nvoid insertionSort(vector<int> &nums) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (int i = 1; i < nums.size(); i++) {\n        int base = nums[i], j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n            j--;\n        }\n        nums[j + 1] = base; // base を正しい位置に配置する\n    }\n}\n
insertion_sort.java
/* 挿入ソート */\nvoid insertionSort(int[] nums) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (int i = 1; i < nums.length; i++) {\n        int base = nums[i], j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n            j--;\n        }\n        nums[j + 1] = base;        // base を正しい位置に配置する\n    }\n}\n
insertion_sort.cs
/* 挿入ソート */\nvoid InsertionSort(int[] nums) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (int i = 1; i < nums.Length; i++) {\n        int bas = nums[i], j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > bas) {\n            nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n            j--;\n        }\n        nums[j + 1] = bas;         // base を正しい位置に配置する\n    }\n}\n
insertion_sort.go
/* 挿入ソート */\nfunc insertionSort(nums []int) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for i := 1; i < len(nums); i++ {\n        base := nums[i]\n        j := i - 1\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        for j >= 0 && nums[j] > base {\n            nums[j+1] = nums[j] // nums[j] を 1 つ右へ移動する\n            j--\n        }\n        nums[j+1] = base // base を正しい位置に配置する\n    }\n}\n
insertion_sort.swift
/* 挿入ソート */\nfunc insertionSort(nums: inout [Int]) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for i in nums.indices.dropFirst() {\n        let base = nums[i]\n        var j = i - 1\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while j >= 0, nums[j] > base {\n            nums[j + 1] = nums[j] // nums[j] を 1 つ右へ移動する\n            j -= 1\n        }\n        nums[j + 1] = base // base を正しい位置に配置する\n    }\n}\n
insertion_sort.js
/* 挿入ソート */\nfunction insertionSort(nums) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        let base = nums[i],\n            j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n            j--;\n        }\n        nums[j + 1] = base; // base を正しい位置に配置する\n    }\n}\n
insertion_sort.ts
/* 挿入ソート */\nfunction insertionSort(nums: number[]): void {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        const base = nums[i];\n        let j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n            j--;\n        }\n        nums[j + 1] = base; // base を正しい位置に配置する\n    }\n}\n
insertion_sort.dart
/* 挿入ソート */\nvoid insertionSort(List<int> nums) {\n  // 外側ループ:整列済み区間は [0, i-1]\n  for (int i = 1; i < nums.length; i++) {\n    int base = nums[i], j = i - 1;\n    // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n    while (j >= 0 && nums[j] > base) {\n      nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n      j--;\n    }\n    nums[j + 1] = base; // base を正しい位置に配置する\n  }\n}\n
insertion_sort.rs
/* 挿入ソート */\nfn insertion_sort(nums: &mut [i32]) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for i in 1..nums.len() {\n        let (base, mut j) = (nums[i], (i - 1) as i32);\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while j >= 0 && nums[j as usize] > base {\n            nums[(j + 1) as usize] = nums[j as usize]; // nums[j] を 1 つ右へ移動する\n            j -= 1;\n        }\n        nums[(j + 1) as usize] = base; // base を正しい位置に配置する\n    }\n}\n
insertion_sort.c
/* 挿入ソート */\nvoid insertionSort(int nums[], int size) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (int i = 1; i < size; i++) {\n        int base = nums[i], j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            // nums[j] を 1 つ右へ移動する\n            nums[j + 1] = nums[j];\n            j--;\n        }\n        // base を正しい位置に配置する\n        nums[j + 1] = base;\n    }\n}\n
insertion_sort.kt
/* 挿入ソート */\nfun insertionSort(nums: IntArray) {\n    // 外側ループ: ソート済み要素は 1, 2, ..., n\n    for (i in nums.indices) {\n        val base = nums[i]\n        var j = i - 1\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j] // nums[j] を 1 つ右へ移動する\n            j--\n        }\n        nums[j + 1] = base        // base を正しい位置に配置する\n    }\n}\n
insertion_sort.rb
### 挿入ソート ###\ndef insertion_sort(nums)\n  n = nums.length\n  # 外側ループ:整列済み区間は [0, i-1]\n  for i in 1...n\n    base = nums[i]\n    j = i - 1\n    # 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n    while j >= 0 && nums[j] > base\n      nums[j + 1] = nums[j] # nums[j] を 1 つ右へ移動する\n      j -= 1\n    end\n    nums[j + 1] = base # base を正しい位置に配置する\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1142","level":2,"title":"11.4.2   アルゴリズムの特徴","text":"
  • 計算量は \\(O(n^2)\\)、適応的ソート:最悪の場合、各挿入操作ではそれぞれ \\(n - 1\\)、\\(n-2\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) 回のループが必要であり、合計は \\((n - 1) n / 2\\) となるため、時間計算量は \\(O(n^2)\\) です。データが整列済みであれば、挿入操作は早期に終了します。入力配列が完全に整列済みである場合、挿入ソートは最良の時間計算量 \\(O(n)\\) に達します。
  • 空間計算量は \\(O(1)\\)、インプレースソート:ポインタ \\(i\\) と \\(j\\) は定数サイズの追加領域しか使用しません。
  • 安定ソート:挿入操作の過程では、要素を等しい要素の右側に挿入するため、それらの順序は変化しません。
","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1143","level":2,"title":"11.4.3   挿入ソートの利点","text":"

挿入ソートの時間計算量は \\(O(n^2)\\) であり、これから学ぶクイックソートの時間計算量は \\(O(n \\log n)\\) です。挿入ソートの時間計算量のほうが大きいにもかかわらず、**データ量が小さい場合には、挿入ソートのほうが通常は高速**です。

この結論は、線形探索と二分探索の適用条件に関する結論と似ています。クイックソートのような \\(O(n \\log n)\\) のアルゴリズムは分割統治法に基づくソートアルゴリズムであり、一般により多くの基本演算を含みます。一方、データ量が小さい場合は、\\(n^2\\) と \\(n \\log n\\) の値は比較的近く、計算量が支配的ではなくなり、各ラウンドにおける基本演算の回数が決定的な役割を果たします。

実際、多くのプログラミング言語(たとえば Java)の組み込みソート関数では挿入ソートが採用されており、その大まかな考え方は次のとおりです。長い配列にはクイックソートなどの分割統治法に基づくソートアルゴリズムを使い、短い配列には直接挿入ソートを使います。

バブルソート、選択ソート、挿入ソートはいずれも時間計算量が \\(O(n^2)\\) ですが、実際には、挿入ソートはバブルソートや選択ソートよりもはるかに高い頻度で使われます。主な理由は次のとおりです。

  • バブルソートは要素の交換によって実装され、1 つの一時変数を必要とするため、合計で 3 回の基本演算が関わります。これに対して、挿入ソートは要素の代入に基づいており、必要な基本演算は 1 回だけです。したがって、バブルソートの計算コストは通常、挿入ソートより高くなります。
  • 選択ソートの時間計算量はどのような場合でも \\(O(n^2)\\) です。**部分的に整列されたデータが与えられた場合、挿入ソートは通常、選択ソートより効率的**です。
  • 選択ソートは安定ではないため、多段ソートには適用できません。
","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   マージソート","text":"

マージソート(merge sort)は分割統治戦略に基づくソートアルゴリズムであり、以下の図に示す「分割」と「マージ」の段階から構成されます。

  1. 分割段階:再帰によって配列を中点で繰り返し分割し、長い配列のソート問題を短い配列のソート問題へ変換します。
  2. マージ段階:部分配列の長さが 1 になったら分割を終了し、マージを開始して、左右 2 つの短いソート済み配列をより長いソート済み配列へと繰り返しマージしていきます。

図 11-10   マージソートの分割とマージの段階

","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161","level":2,"title":"11.6.1   アルゴリズムの流れ","text":"

以下の図に示すように、「分割段階」では配列を上から下へ再帰的に中点で 2 つの部分配列へ分割します。

  1. 配列の中点 mid を計算し、左部分配列(区間 [left, mid] )と右部分配列(区間 [mid + 1, right] )を再帰的に分割します。
  2. 手順 1. を再帰的に実行し、部分配列区間の長さが 1 になった時点で終了します。

「マージ段階」では左部分配列と右部分配列を下から上へとマージし、1 つのソート済み配列にします。長さ 1 の部分配列からマージを始めるため、この段階の各部分配列はすでに整列されています。

<1><2><3><4><5><6><7><8><9><10>

図 11-11   マージソートの手順

観察すると、マージソートの再帰順序は二分木の後順走査と一致していることがわかります。

  • 後順走査:まず左部分木を再帰し、次に右部分木を再帰し、最後に根ノードを処理します。
  • マージソート:まず左部分配列を再帰し、次に右部分配列を再帰し、最後にマージを処理します。

マージソートの実装を以下のコードに示します。注意として、nums のマージ対象区間は [left, right] であり、tmp の対応区間は [0, right - left] です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby merge_sort.py
def merge(nums: list[int], left: int, mid: int, right: int):\n    \"\"\"左部分配列と右部分配列をマージ\"\"\"\n    # 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    # マージ結果を格納する一時配列 tmp を作成\n    tmp = [0] * (right - left + 1)\n    # 左右の部分配列の開始インデックスを初期化する\n    i, j, k = left, mid + 1, 0\n    # 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while i <= mid and j <= right:\n        if nums[i] <= nums[j]:\n            tmp[k] = nums[i]\n            i += 1\n        else:\n            tmp[k] = nums[j]\n            j += 1\n        k += 1\n    # 左右の部分配列の残り要素を一時配列にコピーする\n    while i <= mid:\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    while j <= right:\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    # 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for k in range(0, len(tmp)):\n        nums[left + k] = tmp[k]\n\ndef merge_sort(nums: list[int], left: int, right: int):\n    \"\"\"マージソート\"\"\"\n    # 終了条件\n    if left >= right:\n        return  # 部分配列の長さが 1 になったら再帰を終了\n    # 分割フェーズ\n    mid = (left + right) // 2 # 中点を計算\n    merge_sort(nums, left, mid)  # 左部分配列を再帰処理\n    merge_sort(nums, mid + 1, right)  # 右部分配列を再帰処理\n    # マージフェーズ\n    merge(nums, left, mid, right)\n
merge_sort.cpp
/* 左部分配列と右部分配列をマージ */\nvoid merge(vector<int> &nums, int left, int mid, int right) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    vector<int> tmp(right - left + 1);\n    // 左右の部分配列の開始インデックスを初期化する\n    int i = left, j = mid + 1, k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmp.size(); k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nvoid mergeSort(vector<int> &nums, int left, int right) {\n    // 終了条件\n    if (left >= right)\n        return; // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    int mid = left + (right - left) / 2;    // 中点を計算\n    mergeSort(nums, left, mid);      // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.java
/* 左部分配列と右部分配列をマージ */\nvoid merge(int[] nums, int left, int mid, int right) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    int[] tmp = new int[right - left + 1];\n    // 左右の部分配列の開始インデックスを初期化する\n    int i = left, j = mid + 1, k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nvoid mergeSort(int[] nums, int left, int right) {\n    // 終了条件\n    if (left >= right)\n        return; // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    int mid = left + (right - left) / 2; // 中点を計算\n    mergeSort(nums, left, mid); // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.cs
/* 左部分配列と右部分配列をマージ */\nvoid Merge(int[] nums, int left, int mid, int right) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    int[] tmp = new int[right - left + 1];\n    // 左右の部分配列の開始インデックスを初期化する\n    int i = left, j = mid + 1, k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmp.Length; ++k) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nvoid MergeSort(int[] nums, int left, int right) {\n    // 終了条件\n    if (left >= right) return;       // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    int mid = left + (right - left) / 2;    // 中点を計算\n    MergeSort(nums, left, mid);      // 左部分配列を再帰処理\n    MergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    Merge(nums, left, mid, right);\n}\n
merge_sort.go
/* 左部分配列と右部分配列をマージ */\nfunc merge(nums []int, left, mid, right int) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    tmp := make([]int, right-left+1)\n    // 左右の部分配列の開始インデックスを初期化する\n    i, j, k := left, mid+1, 0\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    for i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i++\n        } else {\n            tmp[k] = nums[j]\n            j++\n        }\n        k++\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    for i <= mid {\n        tmp[k] = nums[i]\n        i++\n        k++\n    }\n    for j <= right {\n        tmp[k] = nums[j]\n        j++\n        k++\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for k := 0; k < len(tmp); k++ {\n        nums[left+k] = tmp[k]\n    }\n}\n\n/* マージソート */\nfunc mergeSort(nums []int, left, right int) {\n    // 終了条件\n    if left >= right {\n        return\n    }\n    // 分割フェーズ\n    mid := left + (right - left) / 2\n    mergeSort(nums, left, mid)\n    mergeSort(nums, mid+1, right)\n    // マージフェーズ\n    merge(nums, left, mid, right)\n}\n
merge_sort.swift
/* 左部分配列と右部分配列をマージ */\nfunc merge(nums: inout [Int], left: Int, mid: Int, right: Int) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    var tmp = Array(repeating: 0, count: right - left + 1)\n    // 左右の部分配列の開始インデックスを初期化する\n    var i = left, j = mid + 1, k = 0\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while i <= mid, j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i += 1\n        } else {\n            tmp[k] = nums[j]\n            j += 1\n        }\n        k += 1\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while i <= mid {\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    }\n    while j <= right {\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for k in tmp.indices {\n        nums[left + k] = tmp[k]\n    }\n}\n\n/* マージソート */\nfunc mergeSort(nums: inout [Int], left: Int, right: Int) {\n    // 終了条件\n    if left >= right { // 部分配列の長さが 1 になったら再帰を終了\n        return\n    }\n    // 分割フェーズ\n    let mid = left + (right - left) / 2 // 中点を計算\n    mergeSort(nums: &nums, left: left, right: mid) // 左部分配列を再帰処理\n    mergeSort(nums: &nums, left: mid + 1, right: right) // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums: &nums, left: left, mid: mid, right: right)\n}\n
merge_sort.js
/* 左部分配列と右部分配列をマージ */\nfunction merge(nums, left, mid, right) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    const tmp = new Array(right - left + 1);\n    // 左右の部分配列の開始インデックスを初期化する\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nfunction mergeSort(nums, left, right) {\n    // 終了条件\n    if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    let mid = Math.floor(left + (right - left) / 2); // 中点を計算\n    mergeSort(nums, left, mid); // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.ts
/* 左部分配列と右部分配列をマージ */\nfunction merge(nums: number[], left: number, mid: number, right: number): void {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    const tmp = new Array(right - left + 1);\n    // 左右の部分配列の開始インデックスを初期化する\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nfunction mergeSort(nums: number[], left: number, right: number): void {\n    // 終了条件\n    if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    let mid = Math.floor(left + (right - left) / 2); // 中点を計算\n    mergeSort(nums, left, mid); // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.dart
/* 左部分配列と右部分配列をマージ */\nvoid merge(List<int> nums, int left, int mid, int right) {\n  // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n  // マージ結果を格納する一時配列 tmp を作成\n  List<int> tmp = List.filled(right - left + 1, 0);\n  // 左右の部分配列の開始インデックスを初期化する\n  int i = left, j = mid + 1, k = 0;\n  // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n  while (i <= mid && j <= right) {\n    if (nums[i] <= nums[j])\n      tmp[k++] = nums[i++];\n    else\n      tmp[k++] = nums[j++];\n  }\n  // 左右の部分配列の残り要素を一時配列にコピーする\n  while (i <= mid) {\n    tmp[k++] = nums[i++];\n  }\n  while (j <= right) {\n    tmp[k++] = nums[j++];\n  }\n  // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n  for (k = 0; k < tmp.length; k++) {\n    nums[left + k] = tmp[k];\n  }\n}\n\n/* マージソート */\nvoid mergeSort(List<int> nums, int left, int right) {\n  // 終了条件\n  if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了\n  // 分割フェーズ\n  int mid = left + (right - left) ~/ 2; // 中点を計算\n  mergeSort(nums, left, mid); // 左部分配列を再帰処理\n  mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n  // マージフェーズ\n  merge(nums, left, mid, right);\n}\n
merge_sort.rs
/* 左部分配列と右部分配列をマージ */\nfn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    let tmp_size = right - left + 1;\n    let mut tmp = vec![0; tmp_size];\n    // 左右の部分配列の開始インデックスを初期化する\n    let (mut i, mut j, mut k) = (left, mid + 1, 0);\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i];\n            i += 1;\n        } else {\n            tmp[k] = nums[j];\n            j += 1;\n        }\n        k += 1;\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while i <= mid {\n        tmp[k] = nums[i];\n        k += 1;\n        i += 1;\n    }\n    while j <= right {\n        tmp[k] = nums[j];\n        k += 1;\n        j += 1;\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for k in 0..tmp_size {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nfn merge_sort(nums: &mut [i32], left: usize, right: usize) {\n    // 終了条件\n    if left >= right {\n        return; // 部分配列の長さが 1 になったら再帰を終了\n    }\n\n    // 分割フェーズ\n    let mid = left + (right - left) / 2; // 中点を計算\n    merge_sort(nums, left, mid); // 左部分配列を再帰処理\n    merge_sort(nums, mid + 1, right); // 右部分配列を再帰処理\n\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.c
/* 左部分配列と右部分配列をマージ */\nvoid merge(int *nums, int left, int mid, int right) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    int tmpSize = right - left + 1;\n    int *tmp = (int *)malloc(tmpSize * sizeof(int));\n    // 左右の部分配列の開始インデックスを初期化する\n    int i = left, j = mid + 1, k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmpSize; ++k) {\n        nums[left + k] = tmp[k];\n    }\n    // メモリを解放する\n    free(tmp);\n}\n\n/* マージソート */\nvoid mergeSort(int *nums, int left, int right) {\n    // 終了条件\n    if (left >= right)\n        return; // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    int mid = left + (right - left) / 2;    // 中点を計算\n    mergeSort(nums, left, mid);      // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.kt
/* 左部分配列と右部分配列をマージ */\nfun merge(nums: IntArray, left: Int, mid: Int, right: Int) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    val tmp = IntArray(right - left + 1)\n    // 左右の部分配列の開始インデックスを初期化する\n    var i = left\n    var j = mid + 1\n    var k = 0\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++]\n        else\n            tmp[k++] = nums[j++]\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++]\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++]\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (l in tmp.indices) {\n        nums[left + l] = tmp[l]\n    }\n}\n\n/* マージソート */\nfun mergeSort(nums: IntArray, left: Int, right: Int) {\n    // 終了条件\n    if (left >= right) return  // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    val mid = left + (right - left) / 2 // 中点を計算\n    mergeSort(nums, left, mid) // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right) // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right)\n}\n
merge_sort.rb
### 左部分配列と右部分配列をマージ ###\ndef merge(nums, left, mid, right)\n  # 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n  # マージ結果を格納する一時配列 tmp を作成\n  tmp = Array.new(right - left + 1, 0)\n  # 左右の部分配列の開始インデックスを初期化する\n  i, j, k = left, mid + 1, 0\n  # 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n  while i <= mid && j <= right\n    if nums[i] <= nums[j]\n      tmp[k] = nums[i]\n      i += 1\n    else\n      tmp[k] = nums[j]\n      j += 1\n    end\n    k += 1\n  end\n  # 左右の部分配列の残り要素を一時配列にコピーする\n  while i <= mid\n    tmp[k] = nums[i]\n    i += 1\n    k += 1\n  end\n  while j <= right\n    tmp[k] = nums[j]\n    j += 1\n    k += 1\n  end\n  # 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n  (0...tmp.length).each do |k|\n    nums[left + k] = tmp[k]\n  end\nend\n\n### マージソート ###\ndef merge_sort(nums, left, right)\n  # 終了条件\n  # 部分配列の長さが 1 になったら再帰を終了する\n  return if left >= right\n  # 分割フェーズ\n  mid = left + (right - left) / 2 # 中点を計算\n  merge_sort(nums, left, mid) # 左部分配列を再帰処理\n  merge_sort(nums, mid + 1, right) # 右部分配列を再帰処理\n  # マージフェーズ\n  merge(nums, left, mid, right)\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1162","level":2,"title":"11.6.2   アルゴリズムの特性","text":"
  • 時間計算量は \\(O(n \\log n)\\)、非適応型ソート:分割によって高さ \\(\\log n\\) の再帰木が生成され、各層でのマージ操作の総数は \\(n\\) であるため、全体の時間計算量は \\(O(n \\log n)\\) です。
  • 空間計算量は \\(O(n)\\)、インプレースではないソート:再帰の深さは \\(\\log n\\) であり、サイズ \\(O(\\log n)\\) のスタックフレーム領域を使用します。マージ操作は補助配列を用いて実装する必要があり、サイズ \\(O(n)\\) の追加領域を使用します。
  • 安定ソート:マージの過程では、等しい要素の順序は変化しません。
","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1163","level":2,"title":"11.6.3   連結リストのソート","text":"

連結リストに対しては、マージソートは他のソートアルゴリズムと比べて顕著な利点があり、連結リストのソート問題の空間計算量を \\(O(1)\\) まで最適化できます 。

  • 分割段階:連結リストの分割は「再帰」の代わりに「反復」で実装できるため、再帰で使用するスタックフレーム領域を省けます。
  • マージ段階:連結リストでは、ノードの追加や削除は参照(ポインタ)を変更するだけで実現できるため、マージ段階(2 つの短いソート済み連結リストを 1 つの長いソート済み連結リストにマージすること)では追加の連結リストを作成する必要がありません。

具体的な実装の詳細は比較的複雑なので、興味のある読者は関連資料を参照して学習してください。

","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   クイックソート","text":"

クイックソート(quick sort)は分割統治戦略に基づくソートアルゴリズムであり、実行効率が高く、広く利用されています。

クイックソートの中核操作は「パーティション」であり、その目的は、配列内のある要素を「基準数」として選び、基準数より小さいすべての要素を左側へ、大きい要素を右側へ移動することです。具体的には、パーティションの流れを下図に示します。

  1. 配列の最左端の要素を基準数として選び、2 つのポインタ ij を初期化して、それぞれ配列の両端を指すようにします。
  2. ループを設定し、各ラウンドで ij)を使ってそれぞれ基準数より大きい(小さい)最初の要素を探し、その後この 2 つの要素を交換します。
  3. ij が出会うまでステップ 2. を繰り返し、最後に基準数を 2 つの部分配列の境界へ交換します。
<1><2><3><4><5><6><7><8><9>

図 11-8   パーティションの手順

パーティションが完了すると、元の配列は左部分配列、基準数、右部分配列の 3 つに分けられ、「左部分配列の任意の要素 \\(\\leq\\) 基準数 \\(\\leq\\) 右部分配列の任意の要素」を満たします。したがって、次はこの 2 つの部分配列だけをソートすれば済みます。

クイックソートの分割統治戦略

パーティションの本質は、長い配列のソート問題を 2 つの短い配列のソート問題へ簡略化することです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"番兵分割\"\"\"\n    # nums[left] を基準値とする\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # 右から左へ基準値未満の最初の要素を探す\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # 左から右へ基準値より大きい最初の要素を探す\n        # 要素の交換\n        nums[i], nums[j] = nums[j], nums[i]\n    # 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # 基準値のインデックスを返す\n
quick_sort.cpp
/* 番兵分割 */\nint partition(vector<int> &nums, int left, int right) {\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;                // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums[i], nums[j]); // この 2 つの要素を交換\n    }\n    swap(nums[i], nums[left]);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;                   // 基準値のインデックスを返す\n}\n
quick_sort.java
/* 要素の交換 */\nvoid swap(int[] nums, int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 番兵分割 */\nint partition(int[] nums, int left, int right) {\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums, i, j); // この 2 つの要素を交換\n    }\n    swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
quick_sort.cs
/* 要素の交換 */\nvoid Swap(int[] nums, int i, int j) {\n    (nums[j], nums[i]) = (nums[i], nums[j]);\n}\n\n/* 番兵分割 */\nint Partition(int[] nums, int left, int right) {\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 左から右へ基準値より大きい最初の要素を探す\n        Swap(nums, i, j); // この 2 つの要素を交換\n    }\n    Swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
quick_sort.go
/* 番兵分割 */\nfunc (q *quickSort) partition(nums []int, left, right int) int {\n    // nums[left] を基準値とする\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // 右から左へ基準値未満の最初の要素を探す\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // 左から右へ基準値より大きい最初の要素を探す\n        }\n        // 要素の交換\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // 基準値のインデックスを返す\n}\n
quick_sort.swift
/* 番兵分割 */\nfunc partition(nums: inout [Int], left: Int, right: Int) -> Int {\n    // nums[left] を基準値とする\n    var i = left\n    var j = right\n    while i < j {\n        while i < j, nums[j] >= nums[left] {\n            j -= 1 // 右から左へ基準値未満の最初の要素を探す\n        }\n        while i < j, nums[i] <= nums[left] {\n            i += 1 // 左から右へ基準値より大きい最初の要素を探す\n        }\n        nums.swapAt(i, j) // この 2 つの要素を交換\n    }\n    nums.swapAt(i, left) // 基準値を 2 つの部分配列の境界へ交換する\n    return i // 基準値のインデックスを返す\n}\n
quick_sort.js
/* 要素の交換 */\nswap(nums, i, j) {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 番兵分割 */\npartition(nums, left, right) {\n    // nums[left] を基準値とする\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        // 要素の交換\n        this.swap(nums, i, j); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
quick_sort.ts
/* 要素の交換 */\nswap(nums: number[], i: number, j: number): void {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 番兵分割 */\npartition(nums: number[], left: number, right: number): number {\n    // nums[left] を基準値とする\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        // 要素の交換\n        this.swap(nums, i, j); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
quick_sort.dart
/* 要素の交換 */\nvoid _swap(List<int> nums, int i, int j) {\n  int tmp = nums[i];\n  nums[i] = nums[j];\n  nums[j] = tmp;\n}\n\n/* 番兵分割 */\nint _partition(List<int> nums, int left, int right) {\n  // nums[left] を基準値とする\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // 右から左へ基準値未満の最初の要素を探す\n    while (i < j && nums[i] <= nums[left]) i++; // 左から右へ基準値より大きい最初の要素を探す\n    _swap(nums, i, j); // この 2 つの要素を交換\n  }\n  _swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n  return i; // 基準値のインデックスを返す\n}\n
quick_sort.rs
/* 番兵分割 */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // nums[left] を基準値とする\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        nums.swap(i, j); // この 2 つの要素を交換\n    }\n    nums.swap(i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    i // 基準値のインデックスを返す\n}\n
quick_sort.c
/* 要素の交換 */\nvoid swap(int nums[], int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 番兵分割 */\nint partition(int nums[], int left, int right) {\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        // この 2 つの要素を交換\n        swap(nums, i, j);\n    }\n    // 基準値を 2 つの部分配列の境界へ交換する\n    swap(nums, i, left);\n    // 基準値のインデックスを返す\n    return i;\n}\n
quick_sort.kt
/* 要素の交換 */\nfun swap(nums: IntArray, i: Int, j: Int) {\n    val temp = nums[i]\n    nums[i] = nums[j]\n    nums[j] = temp\n}\n\n/* 番兵分割 */\nfun partition(nums: IntArray, left: Int, right: Int): Int {\n    // nums[left] を基準値とする\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--           // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++           // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums, i, j)  // この 2 つの要素を交換\n    }\n    swap(nums, i, left)   // 基準値を 2 つの部分配列の境界へ交換する\n    return i              // 基準値のインデックスを返す\n}\n
quick_sort.rb
### 番兵分割 ###\ndef partition(nums, left, right)\n  # nums[left] を基準値とする\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # 右から左へ基準値未満の最初の要素を探す\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # 左から右へ基準値より大きい最初の要素を探す\n    end\n    # 要素の交換\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # 基準値を 2 つの部分配列の境界へ交換する\n  nums[i], nums[left] = nums[left], nums[i]\n  i # 基準値のインデックスを返す\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1151","level":2,"title":"11.5.1   アルゴリズムの流れ","text":"

クイックソート全体の流れを下図に示します。

  1. まず、元の配列に対して 1 回「パーティション」を実行し、未ソートの左部分配列と右部分配列を得ます。
  2. 次に、左部分配列と右部分配列に対してそれぞれ再帰的に「パーティション」を実行します。
  3. 部分配列の長さが 1 になるまで再帰を続け、配列全体のソートを完了します。

図 11-9   クイックソートの流れ

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"クイックソート\"\"\"\n    # 部分配列の長さが 1 なら再帰を終了する\n    if left >= right:\n        return\n    # 番兵分割\n    pivot = self.partition(nums, left, right)\n    # 左右の部分配列を再帰処理\n    self.quick_sort(nums, left, pivot - 1)\n    self.quick_sort(nums, pivot + 1, right)\n
quick_sort.cpp
/* クイックソート */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right)\n        return;\n    // 番兵分割\n    int pivot = partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.java
/* クイックソート */\nvoid quickSort(int[] nums, int left, int right) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right)\n        return;\n    // 番兵分割\n    int pivot = partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.cs
/* クイックソート */\nvoid QuickSort(int[] nums, int left, int right) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right)\n        return;\n    // 番兵分割\n    int pivot = Partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    QuickSort(nums, left, pivot - 1);\n    QuickSort(nums, pivot + 1, right);\n}\n
quick_sort.go
/* クイックソート */\nfunc (q *quickSort) quickSort(nums []int, left, right int) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if left >= right {\n        return\n    }\n    // 番兵分割\n    pivot := q.partition(nums, left, right)\n    // 左右の部分配列を再帰処理\n    q.quickSort(nums, left, pivot-1)\n    q.quickSort(nums, pivot+1, right)\n}\n
quick_sort.swift
/* クイックソート */\nfunc quickSort(nums: inout [Int], left: Int, right: Int) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if left >= right {\n        return\n    }\n    // 番兵分割\n    let pivot = partition(nums: &nums, left: left, right: right)\n    // 左右の部分配列を再帰処理\n    quickSort(nums: &nums, left: left, right: pivot - 1)\n    quickSort(nums: &nums, left: pivot + 1, right: right)\n}\n
quick_sort.js
/* クイックソート */\nquickSort(nums, left, right) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right) return;\n    // 番兵分割\n    const pivot = this.partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
quick_sort.ts
/* クイックソート */\nquickSort(nums: number[], left: number, right: number): void {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right) {\n        return;\n    }\n    // 番兵分割\n    const pivot = this.partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
quick_sort.dart
/* クイックソート */\nvoid quickSort(List<int> nums, int left, int right) {\n  // 部分配列の長さが 1 なら再帰を終了する\n  if (left >= right) return;\n  // 番兵分割\n  int pivot = _partition(nums, left, right);\n  // 左右の部分配列を再帰処理\n  quickSort(nums, left, pivot - 1);\n  quickSort(nums, pivot + 1, right);\n}\n
quick_sort.rs
/* クイックソート */\npub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if left >= right {\n        return;\n    }\n    // 番兵分割\n    let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n    // 左右の部分配列を再帰処理\n    Self::quick_sort(left, pivot - 1, nums);\n    Self::quick_sort(pivot + 1, right, nums);\n}\n
quick_sort.c
/* クイックソート */\nvoid quickSort(int nums[], int left, int right) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right) {\n        return;\n    }\n    // 番兵分割\n    int pivot = partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.kt
/* クイックソート */\nfun quickSort(nums: IntArray, left: Int, right: Int) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right) return\n    // 番兵分割\n    val pivot = partition(nums, left, right)\n    // 左右の部分配列を再帰処理\n    quickSort(nums, left, pivot - 1)\n    quickSort(nums, pivot + 1, right)\n}\n
quick_sort.rb
### クイックソートクラス ###\ndef quick_sort(nums, left, right)\n  # 部分配列の長さが 1 でない場合は再帰する\n  if left < right\n    # 番兵分割\n    pivot = partition(nums, left, right)\n    # 左右の部分配列を再帰処理\n    quick_sort(nums, left, pivot - 1)\n    quick_sort(nums, pivot + 1, right)\n  end\n  nums\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1152","level":2,"title":"11.5.2   アルゴリズムの特性","text":"
  • 時間計算量は \\(O(n \\log n)\\)、非適応型ソート:平均的な場合、パーティションの再帰の深さは \\(\\log n\\) で、各層の総ループ回数は \\(n\\) のため、全体で \\(O(n \\log n)\\) 時間を要します。最悪の場合、各回のパーティション操作で長さ \\(n\\) の配列が長さ \\(0\\) と \\(n - 1\\) の 2 つの部分配列に分割され、このとき再帰の深さは \\(n\\) に達し、各層のループ回数は \\(n\\) となるため、全体で \\(O(n^2)\\) 時間を要します。
  • 空間計算量は \\(O(n)\\)、インプレースソート:入力配列が完全な逆順の場合、最悪の再帰深さ \\(n\\) に達し、\\(O(n)\\) のスタックフレーム空間を使用します。ソート操作は元の配列上で行われ、追加の配列は用いません。
  • 非安定ソート:パーティションの最後のステップで、基準数が等しい要素の右側へ交換される可能性があります。
","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1153","level":2,"title":"11.5.3   クイックソートが速い理由","text":"

名前からも分かるように、クイックソートは効率面で一定の優位性を持っています。クイックソートの平均時間計算量は「マージソート」や「ヒープソート」と同じですが、通常はクイックソートのほうが高効率であり、主な理由は次のとおりです。

  • 最悪ケースが起こる確率が低い:クイックソートの最悪時間計算量は \\(O(n^2)\\) で、「マージソート」ほど安定ではありませんが、大半のケースでは \\(O(n \\log n)\\) の時間計算量で動作します。
  • キャッシュ利用効率が高い:パーティション操作の実行時には、システムが部分配列全体をキャッシュに読み込めるため、要素アクセスの効率が高くなります。一方、「ヒープソート」のようなアルゴリズムは要素へ飛び飛びにアクセスする必要があり、この性質を持ちません。
  • 計算量の定数係数が小さい:上記 3 つのアルゴリズムの中で、クイックソートは比較、代入、交換などの操作総数が最も少なくなります。これは「挿入ソート」が「バブルソート」より速い理由と似ています。
","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154","level":2,"title":"11.5.4   基準数の最適化","text":"

クイックソートは、入力によっては時間効率が低下する可能性があります。極端な例として、入力配列が完全な逆順であるとします。最左端の要素を基準数として選ぶため、パーティション完了後には基準数が配列の最右端へ交換され、左部分配列の長さが \\(n - 1\\)、右部分配列の長さが \\(0\\) になります。この再帰を続けると、各回のパーティション後に必ず一方の部分配列の長さが \\(0\\) となり、分割統治戦略が機能せず、クイックソートは「バブルソート」に近い形へ退化します。

この状況をできるだけ避けるため、パーティションにおける基準数の選び方を最適化できます。たとえば、ランダムに 1 つの要素を選んで基準数にできます。しかし、運が悪く毎回望ましくない基準数を選んでしまうと、効率は依然として十分ではありません。

注意すべき点として、プログラミング言語が通常生成するのは「疑似乱数」です。疑似乱数列に合わせて特定のテストケースを構築すると、クイックソートの効率はやはり劣化する可能性があります。

さらに改善するために、配列から 3 つの候補要素(通常は先頭、末尾、中間の要素)を選び、**その 3 つの候補要素の中央値を基準数とする**ことができます。こうすると、基準数が「小さすぎず大きすぎもしない」確率が大幅に上がります。もちろん、候補要素をさらに増やして、アルゴリズムの頑健性をいっそう高めることも可能です。この方法を採用すると、時間計算量が \\(O(n^2)\\) まで劣化する確率は大きく下がります。

コード例を以下に示します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n    \"\"\"3つの候補要素の中央値を選ぶ\"\"\"\n    l, m, r = nums[left], nums[mid], nums[right]\n    if (l <= m <= r) or (r <= m <= l):\n        return mid  # m は l と r の間\n    if (m <= l <= r) or (r <= l <= m):\n        return left  # l は m と r の間\n    return right\n\ndef partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"番兵による分割処理(3 点中央値)\"\"\"\n    # nums[left] を基準値とする\n    med = self.median_three(nums, left, (left + right) // 2, right)\n    # 中央値を配列の最左端に交換する\n    nums[left], nums[med] = nums[med], nums[left]\n    # nums[left] を基準値とする\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # 右から左へ基準値未満の最初の要素を探す\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # 左から右へ基準値より大きい最初の要素を探す\n        # 要素の交換\n        nums[i], nums[j] = nums[j], nums[i]\n    # 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # 基準値のインデックスを返す\n
quick_sort.cpp
/* 3つの候補要素の中央値を選ぶ */\nint medianThree(vector<int> &nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m は l と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l は m と r の間\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\nint partition(vector<int> &nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // 中央値を配列の最左端に交換する\n    swap(nums[left], nums[med]);\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;                // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums[i], nums[j]); // この 2 つの要素を交換\n    }\n    swap(nums[i], nums[left]);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;                   // 基準値のインデックスを返す\n}\n
quick_sort.java
/* 3つの候補要素の中央値を選ぶ */\nint medianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m は l と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l は m と r の間\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\nint partition(int[] nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // 中央値を配列の最左端に交換する\n    swap(nums, left, med);\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums, i, j); // この 2 つの要素を交換\n    }\n    swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
quick_sort.cs
/* 3つの候補要素の中央値を選ぶ */\nint MedianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m は l と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l は m と r の間\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\nint Partition(int[] nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\n    int med = MedianThree(nums, left, (left + right) / 2, right);\n    // 中央値を配列の最左端に交換する\n    Swap(nums, left, med);\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 左から右へ基準値より大きい最初の要素を探す\n        Swap(nums, i, j); // この 2 つの要素を交換\n    }\n    Swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
quick_sort.go
/* 3つの候補要素の中央値を選ぶ */\nfunc (q *quickSortMedian) medianThree(nums []int, left, mid, right int) int {\n    l, m, r := nums[left], nums[mid], nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m は l と r の間\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l は m と r の間\n    }\n    return right\n}\n\n/* 番兵による分割処理(3 点中央値) */\nfunc (q *quickSortMedian) partition(nums []int, left, right int) int {\n    // nums[left] を基準値とする\n    med := q.medianThree(nums, left, (left+right)/2, right)\n    // 中央値を配列の最左端に交換する\n    nums[left], nums[med] = nums[med], nums[left]\n    // nums[left] を基準値とする\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // 右から左へ基準値未満の最初の要素を探す\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // 左から右へ基準値より大きい最初の要素を探す\n        }\n        // 要素の交換\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // 基準値のインデックスを返す\n}\n
quick_sort.swift
/* 3つの候補要素の中央値を選ぶ */\nfunc medianThree(nums: [Int], left: Int, mid: Int, right: Int) -> Int {\n    let l = nums[left]\n    let m = nums[mid]\n    let r = nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m は l と r の間\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l は m と r の間\n    }\n    return right\n}\n\n/* 番兵による分割処理(3 点中央値) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int {\n    // 3つの候補要素の中央値を選ぶ\n    let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right)\n    // 中央値を配列の最左端に交換する\n    nums.swapAt(left, med)\n    return partition(nums: &nums, left: left, right: right)\n}\n
quick_sort.js
/* 3つの候補要素の中央値を選ぶ */\nmedianThree(nums, left, mid, right) {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m は l と r の間\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l は m と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\npartition(nums, left, right) {\n    // 3つの候補要素の中央値を選ぶ\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // 中央値を配列の最左端に交換する\n    this.swap(nums, left, med);\n    // nums[left] を基準値とする\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) j--; // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left]) i++; // 左から右へ基準値より大きい最初の要素を探す\n        this.swap(nums, i, j); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
quick_sort.ts
/* 3つの候補要素の中央値を選ぶ */\nmedianThree(\n    nums: number[],\n    left: number,\n    mid: number,\n    right: number\n): number {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m は l と r の間\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l は m と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\npartition(nums: number[], left: number, right: number): number {\n    // 3つの候補要素の中央値を選ぶ\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // 中央値を配列の最左端に交換する\n    this.swap(nums, left, med);\n    // nums[left] を基準値とする\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        this.swap(nums, i, j); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
quick_sort.dart
/* 3つの候補要素の中央値を選ぶ */\nint _medianThree(List<int> nums, int left, int mid, int right) {\n  int l = nums[left], m = nums[mid], r = nums[right];\n  if ((l <= m && m <= r) || (r <= m && m <= l))\n    return mid; // m は l と r の間\n  if ((m <= l && l <= r) || (r <= l && l <= m))\n    return left; // l は m と r の間\n  return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\nint _partition(List<int> nums, int left, int right) {\n  // 3つの候補要素の中央値を選ぶ\n  int med = _medianThree(nums, left, (left + right) ~/ 2, right);\n  // 中央値を配列の最左端に交換する\n  _swap(nums, left, med);\n  // nums[left] を基準値とする\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // 右から左へ基準値未満の最初の要素を探す\n    while (i < j && nums[i] <= nums[left]) i++; // 左から右へ基準値より大きい最初の要素を探す\n    _swap(nums, i, j); // この 2 つの要素を交換\n  }\n  _swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n  return i; // 基準値のインデックスを返す\n}\n
quick_sort.rs
/* 3つの候補要素の中央値を選ぶ */\nfn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize {\n    let (l, m, r) = (nums[left], nums[mid], nums[right]);\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid; // m は l と r の間\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left; // l は m と r の間\n    }\n    right\n}\n\n/* 番兵による分割処理(3 点中央値) */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // 3つの候補要素の中央値を選ぶ\n    let med = Self::median_three(nums, left, (left + right) / 2, right);\n    // 中央値を配列の最左端に交換する\n    nums.swap(left, med);\n    // nums[left] を基準値とする\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        nums.swap(i, j); // この 2 つの要素を交換\n    }\n    nums.swap(i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    i // 基準値のインデックスを返す\n}\n
quick_sort.c
/* 3つの候補要素の中央値を選ぶ */\nint medianThree(int nums[], int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m は l と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l は m と r の間\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\nint partitionMedian(int nums[], int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // 中央値を配列の最左端に交換する\n    swap(nums, left, med);\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--; // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums, i, j); // この 2 つの要素を交換\n    }\n    swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i;            // 基準値のインデックスを返す\n}\n
quick_sort.kt
/* 3つの候補要素の中央値を選ぶ */\nfun medianThree(nums: IntArray, left: Int, mid: Int, right: Int): Int {\n    val l = nums[left]\n    val m = nums[mid]\n    val r = nums[right]\n    if ((m in l..r) || (m in r..l))\n        return mid  // m は l と r の間\n    if ((l in m..r) || (l in r..m))\n        return left // l は m と r の間\n    return right\n}\n\n/* 番兵による分割処理(3 点中央値) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n    // 3つの候補要素の中央値を選ぶ\n    val med = medianThree(nums, left, (left + right) / 2, right)\n    // 中央値を配列の最左端に交換する\n    swap(nums, left, med)\n    // nums[left] を基準値とする\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--                      // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++                      // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums, i, j)             // この 2 つの要素を交換\n    }\n    swap(nums, i, left)              // 基準値を 2 つの部分配列の境界へ交換する\n    return i                         // 基準値のインデックスを返す\n}\n
quick_sort.rb
### 3 つの候補要素の中央値を選ぶ ###\ndef median_three(nums, left, mid, right)\n  # 3つの候補要素の中央値を選ぶ\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m は l と r の間\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l は m と r の間\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n### 3 つの候補要素の中央値を選ぶ ###\ndef median_three(nums, left, mid, right)\n  # 3つの候補要素の中央値を選ぶ\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m は l と r の間\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l は m と r の間\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n# ## 番兵分割(三数中央値)###\ndef partition(nums, left, right)\n  # ## nums[left] を基準値とする\n  med = median_three(nums, left, (left + right) / 2, right)\n  # 中央値を配列の最左端に交換する\n  nums[left], nums[med] = nums[med], nums[left]\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # 右から左へ基準値未満の最初の要素を探す\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # 左から右へ基準値より大きい最初の要素を探す\n    end\n    # 要素の交換\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # 基準値を 2 つの部分配列の境界へ交換する\n  nums[i], nums[left] = nums[left], nums[i]\n  i # 基準値のインデックスを返す\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1155","level":2,"title":"11.5.5   再帰の深さの最適化","text":"

一部の入力では、クイックソートは多くの空間を消費する可能性があります。完全に整列済みの入力配列を例にとり、再帰中の部分配列の長さを \\(m\\) とします。各回のパーティション操作では長さ \\(0\\) の左部分配列と長さ \\(m - 1\\) の右部分配列が生成されます。これは、各再帰呼び出しで減る問題サイズが非常に小さいこと(要素が 1 つ減るだけ)を意味し、再帰木の高さは \\(n - 1\\) に達するため、このとき \\(O(n)\\) のスタックフレーム空間を占有します。

スタックフレーム空間の蓄積を防ぐために、各回のパーティション完了後に 2 つの部分配列の長さを比較し、**短いほうの部分配列に対してのみ再帰**を行えます。短い部分配列の長さは \\(n / 2\\) を超えないため、この方法なら再帰の深さを \\(\\log n\\) 以下に抑えられ、最悪時の空間計算量を \\(O(\\log n)\\) まで最適化できます。コードを以下に示します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"クイックソート(再帰深度最適化)\"\"\"\n    # 部分配列の長さが 1 なら終了\n    while left < right:\n        # 番兵による分割処理\n        pivot = self.partition(nums, left, right)\n        # 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if pivot - left < right - pivot:\n            self.quick_sort(nums, left, pivot - 1)  # 左部分配列を再帰的にソート\n            left = pivot + 1  # 未ソート区間の残りは [pivot + 1, right]\n        else:\n            self.quick_sort(nums, pivot + 1, right)  # 右部分配列を再帰的にソート\n            right = pivot - 1  # 未ソート区間の残りは [left, pivot - 1]\n
quick_sort.cpp
/* クイックソート(再帰深度最適化) */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        int pivot = partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート\n            left = pivot + 1;                 // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n            right = pivot - 1;                 // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.java
/* クイックソート(再帰深度最適化) */\nvoid quickSort(int[] nums, int left, int right) {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        int pivot = partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート\n            left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n            right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.cs
/* クイックソート(再帰深度最適化) */\nvoid QuickSort(int[] nums, int left, int right) {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        int pivot = Partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            QuickSort(nums, left, pivot - 1);  // 左部分配列を再帰的にソート\n            left = pivot + 1;  // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            QuickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n            right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.go
/* クイックソート(再帰深度最適化) */\nfunc (q *quickSortTailCall) quickSort(nums []int, left, right int) {\n    // 部分配列の長さが 1 なら終了\n    for left < right {\n        // 番兵による分割処理\n        pivot := q.partition(nums, left, right)\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if pivot-left < right-pivot {\n            q.quickSort(nums, left, pivot-1) // 左部分配列を再帰的にソート\n            left = pivot + 1                 // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            q.quickSort(nums, pivot+1, right) // 右部分配列を再帰的にソート\n            right = pivot - 1                 // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.swift
/* クイックソート(再帰深度最適化) */\nfunc quickSortTailCall(nums: inout [Int], left: Int, right: Int) {\n    var left = left\n    var right = right\n    // 部分配列の長さが 1 なら終了\n    while left < right {\n        // 番兵による分割処理\n        let pivot = partition(nums: &nums, left: left, right: right)\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left) < (right - pivot) {\n            quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // 左部分配列を再帰的にソート\n            left = pivot + 1 // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // 右部分配列を再帰的にソート\n            right = pivot - 1 // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.js
/* クイックソート(再帰深度最適化) */\nquickSort(nums, left, right) {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        let pivot = this.partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート\n            left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n            right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.ts
/* クイックソート(再帰深度最適化) */\nquickSort(nums: number[], left: number, right: number): void {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        let pivot = this.partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート\n            left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n            right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.dart
/* クイックソート(再帰深度最適化) */\nvoid quickSort(List<int> nums, int left, int right) {\n  // 部分配列の長さが 1 なら終了\n  while (left < right) {\n    // 番兵による分割処理\n    int pivot = _partition(nums, left, right);\n    // 2 つの部分配列のうち短いほうにクイックソートを適用する\n    if (pivot - left < right - pivot) {\n      quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート\n      left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right]\n    } else {\n      quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n      right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n    }\n  }\n}\n
quick_sort.rs
/* クイックソート(再帰深度最適化) */\npub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {\n    // 部分配列の長さが 1 なら終了\n    while left < right {\n        // 番兵による分割処理\n        let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if pivot - left < right - pivot {\n            Self::quick_sort(left, pivot - 1, nums); // 左部分配列を再帰的にソート\n            left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            Self::quick_sort(pivot + 1, right, nums); // 右部分配列を再帰的にソート\n            right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.c
/* クイックソート(再帰深度最適化) */\nvoid quickSortTailCall(int nums[], int left, int right) {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        int pivot = partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            // 左部分配列を再帰的にソート\n            quickSortTailCall(nums, left, pivot - 1);\n            // 未ソート区間の残りは [pivot + 1, right]\n            left = pivot + 1;\n        } else {\n            // 右部分配列を再帰的にソート\n            quickSortTailCall(nums, pivot + 1, right);\n            // 未ソート区間の残りは [left, pivot - 1]\n            right = pivot - 1;\n        }\n    }\n}\n
quick_sort.kt
/* クイックソート(再帰深度最適化) */\nfun quickSortTailCall(nums: IntArray, left: Int, right: Int) {\n    // 部分配列の長さが 1 なら終了\n    var l = left\n    var r = right\n    while (l < r) {\n        // 番兵による分割処理\n        val pivot = partition(nums, l, r)\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - l < r - pivot) {\n            quickSort(nums, l, pivot - 1) // 左部分配列を再帰的にソート\n            l = pivot + 1 // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, r) // 右部分配列を再帰的にソート\n            r = pivot - 1 // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.rb
### 番兵分割 ###\ndef partition(nums, left, right)\n  # nums[left] を基準値とする\n  i = left\n  j = right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # 右から左へ基準値未満の最初の要素を探す\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # 左から右へ基準値より大きい最初の要素を探す\n    end\n    # 要素の交換\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # 基準値を 2 つの部分配列の境界へ交換する\n  nums[i], nums[left] = nums[left], nums[i]\n  i # 基準値のインデックスを返す\nend\n\n# ## クイックソート(再帰深度最適化)###\ndef quick_sort(nums, left, right)\n  # 部分配列の長さが 1 でない場合は再帰する\n  while left < right\n    # 番兵分割\n    pivot = partition(nums, left, right)\n    # 2 つの部分配列のうち短いほうにクイックソートを適用する\n    if pivot - left < right - pivot\n      quick_sort(nums, left, pivot - 1)\n      left = pivot + 1 # 未ソート区間の残りは [pivot + 1, right]\n    else\n      quick_sort(nums, pivot + 1, right)\n      right = pivot - 1 # 未ソート区間の残りは [left, pivot - 1]\n    end\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/radix_sort/","level":1,"title":"11.10   基数ソート","text":"

前節では計数ソートを紹介しました。これは、データ量 \\(n\\) が大きく、データ範囲 \\(m\\) が小さい場合に適しています。\\(n = 10^6\\) 個の学籍番号をソートすると仮定すると、学籍番号は \\(8\\) 桁の数字なので、データ範囲 \\(m = 10^8\\) は非常に大きくなります。計数ソートでは大量のメモリ空間を確保する必要がありますが、基数ソートではこの問題を回避できます。

基数ソート(radix sort)の基本的な考え方は計数ソートと同じで、個数を数えることによってソートを実現します。そのうえで、基数ソートは各桁の段階的な関係を利用し、各桁を順にソートすることで、最終的なソート結果を得ます。

","path":["第 11 章   ソート","11.10   基数ソート"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11101","level":2,"title":"11.10.1   アルゴリズムの流れ","text":"

学籍番号データを例にすると、数字の最下位桁を第 \\(1\\) 位、最上位桁を第 \\(8\\) 位としたとき、基数ソートの流れは次図のようになります。

  1. 桁番号 \\(k = 1\\) を初期化します。
  2. 学籍番号の第 \\(k\\) 位に対して「計数ソート」を実行します。完了すると、データは第 \\(k\\) 位に従って昇順に並びます。
  3. \\(k\\) を \\(1\\) 増やし、手順 2. に戻って反復を続けます。すべての桁のソートが完了したら終了します。

図 11-18   基数ソートのアルゴリズムの流れ

以下ではコード実装を分解して見ていきます。\\(d\\) 進数の数値 \\(x\\) について、その第 \\(k\\) 位 \\(x_k\\) を取得するには、次の計算式を用います。

\\[ x_k = \\lfloor\\frac{x}{d^{k-1}}\\rfloor \\bmod d \\]

ここで、\\(\\lfloor a \\rfloor\\) は浮動小数点数 \\(a\\) の切り捨てを表し、\\(\\bmod \\: d\\) は \\(d\\) による剰余を表します。学籍番号データでは、\\(d = 10\\) かつ \\(k \\in [1, 8]\\) です。

さらに、数字の第 \\(k\\) 位に基づいてソートできるように、計数ソートのコードを少し変更する必要があります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby radix_sort.py
def digit(num: int, exp: int) -> int:\n    \"\"\"要素 num の下から k 桁目を取得(exp = 10^(k-1))\"\"\"\n    # ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"計数ソート(nums の k 桁目でソート)\"\"\"\n    # 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    counter = [0] * 10\n    n = len(nums)\n    # 0~9 の各数字の出現回数を集計する\n    for i in range(n):\n        d = digit(nums[i], exp)  # nums[i] の第 k 位を取得し、d とする\n        counter[d] += 1  # 数字 d の出現回数を数える\n    # 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for i in range(1, 10):\n        counter[i] += counter[i - 1]\n    # 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        d = digit(nums[i], exp)\n        j = counter[d] - 1  # d の配列内インデックス j を取得する\n        res[j] = nums[i]  # 現在の要素をインデックス j に格納する\n        counter[d] -= 1  # d の個数を 1 減らす\n    # 結果で元の配列 nums を上書きする\n    for i in range(n):\n        nums[i] = res[i]\n\ndef radix_sort(nums: list[int]):\n    \"\"\"基数ソート\"\"\"\n    # 最大桁数の判定用に配列の最大要素を取得\n    m = max(nums)\n    # 下位桁から上位桁の順に走査する\n    exp = 1\n    while exp <= m:\n        # 配列要素の k 桁目に対して計数ソートを行う\n        # k = 1 -> exp = 1\n        # k = 2 -> exp = 10\n        # つまり exp = 10^(k-1)\n        counting_sort_digit(nums, exp)\n        exp *= 10\n
radix_sort.cpp
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    vector<int> counter(10, 0);\n    int n = nums.size();\n    // 0~9 の各数字の出現回数を集計する\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d]++;                // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    vector<int> res(n, 0);\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i];       // 現在の要素をインデックス j に格納する\n        counter[d]--;           // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* 基数ソート */\nvoid radixSort(vector<int> &nums) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    int m = *max_element(nums.begin(), nums.end());\n    // 下位桁から上位桁の順に走査する\n    for (int exp = 1; exp <= m; exp *= 10)\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n}\n
radix_sort.java
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(int[] nums, int exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    int[] counter = new int[10];\n    int n = nums.length;\n    // 0~9 の各数字の出現回数を集計する\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d]++;                // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i];       // 現在の要素をインデックス j に格納する\n        counter[d]--;           // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* 基数ソート */\nvoid radixSort(int[] nums) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    int m = Integer.MIN_VALUE;\n    for (int num : nums)\n        if (num > m)\n            m = num;\n    // 下位桁から上位桁の順に走査する\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.cs
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint Digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid CountingSortDigit(int[] nums, int exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    int[] counter = new int[10];\n    int n = nums.Length;\n    // 0~9 の各数字の出現回数を集計する\n    for (int i = 0; i < n; i++) {\n        int d = Digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d]++;                // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = Digit(nums[i], exp);\n        int j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i];       // 現在の要素をインデックス j に格納する\n        counter[d]--;           // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* 基数ソート */\nvoid RadixSort(int[] nums) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    int m = int.MinValue;\n    foreach (int num in nums) {\n        if (num > m) m = num;\n    }\n    // 下位桁から上位桁の順に走査する\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        CountingSortDigit(nums, exp);\n    }\n}\n
radix_sort.go
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunc digit(num, exp int) int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunc countingSortDigit(nums []int, exp int) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    counter := make([]int, 10)\n    n := len(nums)\n    // 0~9 の各数字の出現回数を集計する\n    for i := 0; i < n; i++ {\n        d := digit(nums[i], exp) // nums[i] の第 k 位を取得し、d とする\n        counter[d]++             // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for i := 1; i < 10; i++ {\n        counter[i] += counter[i-1]\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        d := digit(nums[i], exp)\n        j := counter[d] - 1 // d の配列内インデックス j を取得する\n        res[j] = nums[i]    // 現在の要素をインデックス j に格納する\n        counter[d]--        // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for i := 0; i < n; i++ {\n        nums[i] = res[i]\n    }\n}\n\n/* 基数ソート */\nfunc radixSort(nums []int) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    max := math.MinInt\n    for _, num := range nums {\n        if num > max {\n            max = num\n        }\n    }\n    // 下位桁から上位桁の順に走査する\n    for exp := 1; max >= exp; exp *= 10 {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n    }\n}\n
radix_sort.swift
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    var counter = Array(repeating: 0, count: 10)\n    // 0~9 の各数字の出現回数を集計する\n    for i in nums.indices {\n        let d = digit(num: nums[i], exp: exp) // nums[i] の第 k 位を取得し、d とする\n        counter[d] += 1 // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for i in 1 ..< 10 {\n        counter[i] += counter[i - 1]\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let d = digit(num: nums[i], exp: exp)\n        let j = counter[d] - 1 // d の配列内インデックス j を取得する\n        res[j] = nums[i] // 現在の要素をインデックス j に格納する\n        counter[d] -= 1 // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n\n/* 基数ソート */\nfunc radixSort(nums: inout [Int]) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    var m = Int.min\n    for num in nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 下位桁から上位桁の順に走査する\n    for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums: &nums, exp: exp)\n    }\n}\n
radix_sort.js
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunction digit(num, exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunction countingSortDigit(nums, exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // 0~9 の各数字の出現回数を集計する\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d]++; // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i]; // 現在の要素をインデックス j に格納する\n        counter[d]--; // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* 基数ソート */\nfunction radixSort(nums) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    let m = Math.max(... nums);\n    // 下位桁から上位桁の順に走査する\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.ts
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunction digit(num: number, exp: number): number {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // 0~9 の各数字の出現回数を集計する\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d]++; // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i]; // 現在の要素をインデックス j に格納する\n        counter[d]--; // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* 基数ソート */\nfunction radixSort(nums: number[]): void {\n    // 最大桁数の判定用に配列の最大要素を取得\n    let m: number = Math.max(... nums);\n    // 下位桁から上位桁の順に走査する\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.dart
/* 要素 `_num` の第 k 桁を取得する。ここで `exp = 10^(k-1)` */\nint digit(int _num, int exp) {\n  // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n  return (_num ~/ exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(List<int> nums, int exp) {\n  // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n  List<int> counter = List<int>.filled(10, 0);\n  int n = nums.length;\n  // 0~9 の各数字の出現回数を集計する\n  for (int i = 0; i < n; i++) {\n    int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n    counter[d]++; // 数字 d の出現回数を数える\n  }\n  // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n  for (int i = 1; i < 10; i++) {\n    counter[i] += counter[i - 1];\n  }\n  // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n  List<int> res = List<int>.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int d = digit(nums[i], exp);\n    int j = counter[d] - 1; // d の配列内インデックス j を取得する\n    res[j] = nums[i]; // 現在の要素をインデックス j に格納する\n    counter[d]--; // d の個数を 1 減らす\n  }\n  // 結果で元の配列 nums を上書きする\n  for (int i = 0; i < n; i++) nums[i] = res[i];\n}\n\n/* 基数ソート */\nvoid radixSort(List<int> nums) {\n  // 最大桁数の判定用に配列の最大要素を取得する\n  // dart の `int` の長さは 64 ビット\n  int m = -1 << 63;\n  for (int _num in nums) if (_num > m) m = _num;\n  // 下位桁から上位桁の順に走査する\n  for (int exp = 1; exp <= m; exp *= 10)\n    // 配列要素の k 桁目に対して計数ソートを行う\n    // k = 1 -> exp = 1\n    // k = 2 -> exp = 10\n    // つまり exp = 10^(k-1)\n    countingSortDigit(nums, exp);\n}\n
radix_sort.rs
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfn digit(num: i32, exp: i32) -> usize {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return ((num / exp) % 10) as usize;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfn counting_sort_digit(nums: &mut [i32], exp: i32) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    let mut counter = [0; 10];\n    let n = nums.len();\n    // 0~9 の各数字の出現回数を集計する\n    for i in 0..n {\n        let d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d] += 1; // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for i in 1..10 {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let d = digit(nums[i], exp);\n        let j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i]; // 現在の要素をインデックス j に格納する\n        counter[d] -= 1; // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    nums.copy_from_slice(&res);\n}\n\n/* 基数ソート */\nfn radix_sort(nums: &mut [i32]) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    let m = *nums.into_iter().max().unwrap();\n    // 下位桁から上位桁の順に走査する\n    let mut exp = 1;\n    while exp <= m {\n        counting_sort_digit(nums, exp);\n        exp *= 10;\n    }\n}\n
radix_sort.c
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    int *counter = (int *)malloc((sizeof(int) * 10));\n    memset(counter, 0, sizeof(int) * 10); // 後続のメモリ解放に備えて 0 で初期化する\n    // 0~9 の各数字の出現回数を集計する\n    for (int i = 0; i < size; i++) {\n        // nums[i] の第 k 位を取得し、d とする\n        int d = digit(nums[i], exp);\n        // 数字 d の出現回数を数える\n        counter[d]++;\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    int *res = (int *)malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i];       // 現在の要素をインデックス j に格納する\n        counter[d]--;           // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (int i = 0; i < size; i++) {\n        nums[i] = res[i];\n    }\n    // メモリを解放する\n    free(res);\n    free(counter);\n}\n\n/* 基数ソート */\nvoid radixSort(int nums[], int size) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    int max = INT32_MIN;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > max) {\n            max = nums[i];\n        }\n    }\n    // 下位桁から上位桁の順に走査する\n    for (int exp = 1; max >= exp; exp *= 10)\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, size, exp);\n}\n
radix_sort.kt
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfun digit(num: Int, exp: Int): Int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    val counter = IntArray(10)\n    val n = nums.size\n    // 0~9 の各数字の出現回数を集計する\n    for (i in 0..<n) {\n        val d = digit(nums[i], exp) // nums[i] の第 k 位を取得し、d とする\n        counter[d]++                // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (i in 1..9) {\n        counter[i] += counter[i - 1]\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val d = digit(nums[i], exp)\n        val j = counter[d] - 1 // d の配列内インデックス j を取得する\n        res[j] = nums[i]       // 現在の要素をインデックス j に格納する\n        counter[d]--           // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (i in 0..<n)\n        nums[i] = res[i]\n}\n\n/* 基数ソート */\nfun radixSort(nums: IntArray) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    var m = Int.MIN_VALUE\n    for (num in nums) if (num > m) m = num\n    var exp = 1\n    // 下位桁から上位桁の順に走査する\n    while (exp <= m) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n        exp *= 10\n    }\n}\n
radix_sort.rb
### num の第 k 桁を取得する。ここで exp = 10^(k-1) ###\ndef digit(num, exp)\n  # k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる\n  (num / exp) % 10\nend\n\n### num の第 k 桁を取得する。ここで exp = 10^(k-1) ###\ndef digit(num, exp)\n  # k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる\n  (num / exp) % 10\nend\n\n# ## 計数ソート(nums の k 桁目でソート)###\ndef counting_sort_digit(nums, exp)\n  # 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n  counter = Array.new(10, 0)\n  n = nums.length\n  # 0~9 の各数字の出現回数を集計する\n  for i in 0...n\n    d = digit(nums[i], exp) # nums[i] の第 k 位を取得し、d とする\n    counter[d] += 1 # 数字 d の出現回数を数える\n  end\n  # 累積和を求め、「出現回数」を「配列インデックス」に変換する\n  (1...10).each { |i| counter[i] += counter[i - 1] }\n  # 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n  res = Array.new(n, 0)\n  for i in (n - 1).downto(0)\n    d = digit(nums[i], exp)\n    j = counter[d] - 1 # d の配列内インデックス j を取得する\n    res[j] = nums[i] # 現在の要素をインデックス j に格納する\n    counter[d] -= 1 # d の個数を 1 減らす\n  end\n  # 結果で元の配列 nums を上書きする\n  (0...n).each { |i| nums[i] = res[i] }\nend\n\n### 基数ソート ###\ndef radix_sort(nums)\n  # 最大桁数の判定用に配列の最大要素を取得\n  m = nums.max\n  # 下位桁から上位桁の順に走査する\n  exp = 1\n  while exp <= m\n    # 配列要素の k 桁目に対して計数ソートを行う\n    # k = 1 -> exp = 1\n    # k = 2 -> exp = 10\n    # つまり exp = 10^(k-1)\n    counting_sort_digit(nums, exp)\n    exp *= 10\n  end\nend\n
コードの可視化

全画面で見る >

なぜ最下位桁からソートするのですか?

連続するソートの各ラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。たとえば、第1ラウンドで \\(a < b\\) となっていても、第2ラウンドで \\(a > b\\) となれば、第2ラウンドの結果が優先されます。数字では高位の優先度が低位より高いため、先に低位をソートし、その後で高位をソートする必要があります。

","path":["第 11 章   ソート","11.10   基数ソート"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11102","level":2,"title":"11.10.2   アルゴリズムの特徴","text":"

計数ソートと比べると、基数ソートは値の範囲が大きい場合に適しています。ただし、データが固定桁数の形式で表せること、かつ桁数が大きすぎないことが前提です。たとえば、浮動小数点数は基数ソートに適していません。桁数 \\(k\\) が大きすぎて、時間計算量が \\(O(nk) \\gg O(n^2)\\) になる可能性があるためです。

  • 時間計算量は \\(O(nk)\\)、非適応ソート:データ量を \\(n\\)、データが \\(d\\) 進数、最大桁数を \\(k\\) とすると、ある1桁に対して計数ソートを実行する時間は \\(O(n + d)\\) であり、全 \\(k\\) 桁をソートする時間は \\(O((n + d)k)\\) です。通常、\\(d\\) と \\(k\\) はどちらも比較的小さいため、時間計算量は \\(O(n)\\) に近づきます。
  • 空間計算量は \\(O(n + d)\\)、非原地ソート:計数ソートと同様に、基数ソートでは長さ \\(n\\) と \\(d\\) の配列 rescounter を補助的に用います。
  • 安定ソート:計数ソートが安定であれば基数ソートも安定です。計数ソートが不安定な場合、基数ソートでは正しいソート結果を保証できません。
","path":["第 11 章   ソート","11.10   基数ソート"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   選択ソート","text":"

選択ソート(selection sort)の仕組みは非常に単純です。ループを開始し、各ラウンドで未ソート区間から最小の要素を選び、整列済み区間の末尾に配置します。

配列の長さを \\(n\\) とすると、選択ソートの手順は次の図のようになります。

  1. 初期状態では、すべての要素が未ソートであり、未ソートな(インデックス)区間は \\([0, n-1]\\) です。
  2. 区間 \\([0, n-1]\\) 内の最小要素を選び、インデックス \\(0\\) の要素と交換します。これにより、配列の先頭 1 要素が整列済みになります。
  3. 区間 \\([1, n-1]\\) 内の最小要素を選び、インデックス \\(1\\) の要素と交換します。これにより、配列の先頭 2 要素が整列済みになります。
  4. これを繰り返します。\\(n - 1\\) 回の選択と交換を経ると、配列の先頭 \\(n - 1\\) 要素が整列済みになります。
  5. 残った 1 つの要素は必ず最大要素なので、ソートは不要です。これで配列のソートは完了します。
<1><2><3><4><5><6><7><8><9><10><11>

図 11-2   選択ソートの手順

コードでは、\\(k\\) を用いて未ソート区間内の最小要素を記録します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby selection_sort.py
def selection_sort(nums: list[int]):\n    \"\"\"選択ソート\"\"\"\n    n = len(nums)\n    # 外側ループ:未整列区間は [i, n-1]\n    for i in range(n - 1):\n        # 内側のループ:未ソート区間の最小要素を見つける\n        k = i\n        for j in range(i + 1, n):\n            if nums[j] < nums[k]:\n                k = j  # 最小要素のインデックスを記録\n        # その最小要素を未整列区間の先頭要素と交換する\n        nums[i], nums[k] = nums[k], nums[i]\n
selection_sort.cpp
/* 選択ソート */\nvoid selectionSort(vector<int> &nums) {\n    int n = nums.size();\n    // 外側ループ:未整列区間は [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 最小要素のインデックスを記録\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        swap(nums[i], nums[k]);\n    }\n}\n
selection_sort.java
/* 選択ソート */\nvoid selectionSort(int[] nums) {\n    int n = nums.length;\n    // 外側ループ:未整列区間は [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 最小要素のインデックスを記録\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
selection_sort.cs
/* 選択ソート */\nvoid SelectionSort(int[] nums) {\n    int n = nums.Length;\n    // 外側ループ:未整列区間は [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 最小要素のインデックスを記録\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        (nums[k], nums[i]) = (nums[i], nums[k]);\n    }\n}\n
selection_sort.go
/* 選択ソート */\nfunc selectionSort(nums []int) {\n    n := len(nums)\n    // 外側ループ:未整列区間は [i, n-1]\n    for i := 0; i < n-1; i++ {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        k := i\n        for j := i + 1; j < n; j++ {\n            if nums[j] < nums[k] {\n                // 最小要素のインデックスを記録\n                k = j\n            }\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        nums[i], nums[k] = nums[k], nums[i]\n\n    }\n}\n
selection_sort.swift
/* 選択ソート */\nfunc selectionSort(nums: inout [Int]) {\n    // 外側ループ:未整列区間は [i, n-1]\n    for i in nums.indices.dropLast() {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        var k = i\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[j] < nums[k] {\n                k = j // 最小要素のインデックスを記録\n            }\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        nums.swapAt(i, k)\n    }\n}\n
selection_sort.js
/* 選択ソート */\nfunction selectionSort(nums) {\n    let n = nums.length;\n    // 外側ループ:未整列区間は [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // 最小要素のインデックスを記録\n            }\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
selection_sort.ts
/* 選択ソート */\nfunction selectionSort(nums: number[]): void {\n    let n = nums.length;\n    // 外側ループ:未整列区間は [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // 最小要素のインデックスを記録\n            }\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
selection_sort.dart
/* 選択ソート */\nvoid selectionSort(List<int> nums) {\n  int n = nums.length;\n  // 外側ループ:未整列区間は [i, n-1]\n  for (int i = 0; i < n - 1; i++) {\n    // 内側のループ:未ソート区間の最小要素を見つける\n    int k = i;\n    for (int j = i + 1; j < n; j++) {\n      if (nums[j] < nums[k]) k = j; // 最小要素のインデックスを記録\n    }\n    // その最小要素を未整列区間の先頭要素と交換する\n    int temp = nums[i];\n    nums[i] = nums[k];\n    nums[k] = temp;\n  }\n}\n
selection_sort.rs
/* 選択ソート */\nfn selection_sort(nums: &mut [i32]) {\n    if nums.is_empty() {\n        return;\n    }\n    let n = nums.len();\n    // 外側ループ:未整列区間は [i, n-1]\n    for i in 0..n - 1 {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        let mut k = i;\n        for j in i + 1..n {\n            if nums[j] < nums[k] {\n                k = j; // 最小要素のインデックスを記録\n            }\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        nums.swap(i, k);\n    }\n}\n
selection_sort.c
/* 選択ソート */\nvoid selectionSort(int nums[], int n) {\n    // 外側ループ:未整列区間は [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 最小要素のインデックスを記録\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
selection_sort.kt
/* 選択ソート */\nfun selectionSort(nums: IntArray) {\n    val n = nums.size\n    // 外側ループ:未整列区間は [i, n-1]\n    for (i in 0..<n - 1) {\n        var k = i\n        // 内側のループ:未ソート区間の最小要素を見つける\n        for (j in i + 1..<n) {\n            if (nums[j] < nums[k])\n                k = j // 最小要素のインデックスを記録\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        val temp = nums[i]\n        nums[i] = nums[k]\n        nums[k] = temp\n    }\n}\n
selection_sort.rb
### 選択ソート ###\ndef selection_sort(nums)\n  n = nums.length\n  # 外側ループ:未整列区間は [i, n-1]\n  for i in 0...(n - 1)\n    # 内側のループ:未ソート区間の最小要素を見つける\n    k = i\n    for j in (i + 1)...n\n      if nums[j] < nums[k]\n        k = j # 最小要素のインデックスを記録\n      end\n    end\n    # その最小要素を未整列区間の先頭要素と交換する\n    nums[i], nums[k] = nums[k], nums[i]\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.2   選択ソート"],"tags":[]},{"location":"chapter_sorting/selection_sort/#1121","level":2,"title":"11.2.1   アルゴリズムの特徴","text":"
  • 時間計算量は \\(O(n^2)\\)、非適応ソート:外側のループは合計 \\(n - 1\\) 回です。最初のラウンドの未ソート区間の長さは \\(n\\)、最後のラウンドでは \\(2\\) であり、各ラウンドの内側のループ回数はそれぞれ \\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(3\\)、\\(2\\) となります。総和は \\(\\frac{(n - 1)(n + 2)}{2}\\) です。
  • 空間計算量は \\(O(1)\\)、インプレースソート:ポインタ \\(i\\) と \\(j\\) は定数サイズの追加領域しか使用しません。
  • 不安定ソート:次の図のように、要素 nums[i] がそれと等しい要素の右側へ交換され、両者の相対的な順序が変わる可能性があります。

図 11-3   選択ソートの不安定な例

","path":["第 11 章   ソート","11.2   選択ソート"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   ソートアルゴリズム","text":"

ソートアルゴリズム(sorting algorithm)は、データの集合を特定の順序に従って並べ替えるために用いられます。ソートアルゴリズムは幅広く応用されており、整列済みデータは通常、より効率的に検索、分析、処理できるためです。

下図に示すように、ソートアルゴリズムにおけるデータ型は整数、浮動小数点数、文字、文字列などです。ソートの判定規則は、数値の大小、文字の ASCII コード順、またはカスタムルールなど、要件に応じて設定できます。

図 11-1   データ型と判定規則の例

","path":["第 11 章   ソート","11.1   ソートアルゴリズム"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111","level":2,"title":"11.1.1   評価軸","text":"

実行効率:ソートアルゴリズムの時間計算量はできるだけ低く、かつ全体の操作回数も少ないこと(時間計算量における定数項が小さいこと)が望まれます。大量データの場合、実行効率はとりわけ重要です。

インプレース性:その名のとおり、インプレースソートは元の配列を直接操作して並べ替えを行うため、追加の補助配列を必要とせず、メモリを節約できます。通常、インプレースソートはデータの移動操作が少なく、実行速度もより高速です。

安定性:安定ソートは、並べ替え完了後も、等しい要素の配列内での相対順序が変化しません。

安定ソートは多段ソートの場面で必要条件となります。学生情報を保存した表があり、第 1 列と第 2 列がそれぞれ氏名と年齢であると仮定します。この場合、不安定ソートによって入力データの順序性が失われる可能性があります。

# 入力データは氏名順にソートされている\n# (name, age)\n  ('A', 19)\n  ('B', 18)\n  ('C', 21)\n  ('D', 19)\n  ('E', 23)\n\n# 不安定ソートアルゴリズムで年齢順にリストを並べ替えると仮定すると、\n# 結果では ('D', 19) と ('A', 19) の相対位置が変わり、\n# 入力データが氏名順である性質が失われる\n  ('B', 18)\n  ('D', 19)\n  ('A', 19)\n  ('C', 21)\n  ('E', 23)\n

適応性:適応的ソートは、入力データに既に存在する順序情報を利用して計算量を減らし、より優れた時間効率を実現できます。適応的ソートアルゴリズムの最良時間計算量は、通常、平均時間計算量より優れています。

比較ベースかどうか:比較ベースのソートは、比較演算子(\\(<\\)、\\(=\\)、\\(>\\))に依存して要素の相対順序を判定し、それによって配列全体をソートします。理論上の最良時間計算量は \\(O(n \\log n)\\) です。一方、非比較ソートは比較演算子を使用せず、時間計算量は \\(O(n)\\) に達しますが、汎用性は相対的に低くなります。

","path":["第 11 章   ソート","11.1   ソートアルゴリズム"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1112","level":2,"title":"11.1.2   理想的なソートアルゴリズム","text":"

高速、インプレース、安定、適応的、高い汎用性。明らかに、これまでのところ、以上のすべての特性を兼ね備えたソートアルゴリズムはまだ見つかっていません。そのため、ソートアルゴリズムを選択する際には、具体的なデータの特徴と問題の要件に応じて判断する必要があります。

次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価軸に基づいて各ソートアルゴリズムの長所と短所を分析していきます。

","path":["第 11 章   ソート","11.1   ソートアルゴリズム"],"tags":[]},{"location":"chapter_sorting/summary/","level":1,"title":"11.11   まとめ","text":"","path":["第 11 章   ソート","11.11   まとめ"],"tags":[]},{"location":"chapter_sorting/summary/#1","level":3,"title":"1.   重要なポイントの振り返り","text":"
  • バブルソートは隣接する要素を交換することで整列を行います。フラグを追加して早期リターンを可能にすると、バブルソートの最良時間計算量を \\(O(n)\\) に最適化できます。
  • 挿入ソートは各ラウンドで未整列区間の要素を整列済み区間の正しい位置に挿入することで整列を完了します。挿入ソートの時間計算量は \\(O(n^2)\\) ですが、基本操作が比較的少ないため、小規模データのソート処理で非常に人気があります。
  • クイックソートは番兵分割操作に基づいて整列を行います。番兵分割では毎回最悪の基準値を選んでしまう可能性があり、その結果、時間計算量は \\(O(n^2)\\) まで劣化することがあります。中央値の基準値やランダムな基準値を導入すると、この劣化の確率を下げられます。短い部分配列を優先して再帰すれば、再帰の深さを効果的に抑え、空間計算量を \\(O(\\log n)\\) に最適化できます。
  • マージソートは分割とマージという 2 つの段階からなり、分割統治戦略を典型的に体現しています。マージソートでは配列を整列する際に補助配列の作成が必要で、空間計算量は \\(O(n)\\) です。一方、連結リストを整列する場合の空間計算量は \\(O(1)\\) まで最適化できます。
  • バケットソートはデータのバケット分配、バケット内ソート、結果の結合という 3 つの手順を含みます。これも分割統治戦略を体現しており、データ量が非常に大きい場合に適しています。バケットソートの鍵は、データを平均的に分配することにあります。
  • カウントソートはバケットソートの特例であり、データの出現回数を数えることで整列を行います。カウントソートはデータ量が大きく、かつデータ範囲が限られている場合に適しており、データを正の整数に変換できることが前提です。
  • 基数ソートは各桁ごとの整列によってデータを整列し、データが固定桁数の数値として表せることを前提とします。
  • 総じて言えば、私たちは高効率で、安定で、インプレースで、さらに適応的であるといった利点を備えたソートアルゴリズムを見つけたいと考えます。しかし、ほかのデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たせるソートアルゴリズムは存在しません。実際の応用では、データの特性に応じて適切なソートアルゴリズムを選ぶ必要があります。
  • 下図では、主流のソートアルゴリズムについて、効率、安定性、インプレース性、適応性などを比較しています。

図 11-19   ソートアルゴリズムの比較

","path":["第 11 章   ソート","11.11   まとめ"],"tags":[]},{"location":"chapter_sorting/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:ソートアルゴリズムの安定性は、どのような場合に必須ですか?

現実には、オブジェクトのある属性に基づいて整列することがあります。たとえば、学生には氏名と身長という 2 つの属性があり、多段階のソートを行いたいとします。まず氏名で整列して (A, 180) (B, 185) (C, 170) (D, 170) を得て、その後に身長で整列します。ソートアルゴリズムが不安定である場合、結果は (D, 170) (C, 170) (A, 180) (B, 185) になる可能性があります。

このように、学生 D と C の位置が入れ替わり、氏名に関する順序性が壊れてしまいます。これは望ましくありません。

Q:番兵分割において、「右から左へ探索する」順序と「左から右へ探索する」順序は入れ替えられますか?

できません。最も左端の要素を基準値とする場合は、必ず先に「右から左へ探索する」を行い、その後に「左から右へ探索する」を行う必要があります。この結論はやや直感に反するので、理由を分析してみましょう。

番兵分割 partition() の最後の手順は、nums[left]nums[i] を交換することです。交換が終わると、基準値の左側にある要素はすべて基準値 <= になります。したがって、最後の交換の前に nums[left] >= nums[i] が必ず成り立っていなければなりません。仮に先に「左から右へ探索する」を行うと、基準値より大きい要素が見つからない場合、i == j の時点でループを抜け、このとき nums[j] == nums[i] > nums[left] となる可能性があります。つまり、この最後の交換によって、基準値より大きい要素が配列の最左端へ移されてしまい、番兵分割は失敗します。

たとえば、配列 [0, 0, 0, 0, 1] が与えられたとき、先に「左から右へ探索する」を行うと、番兵分割後の配列は [1, 0, 0, 0, 0] になります。これは誤った結果です。

さらに考えると、nums[right] を基準値に選ぶ場合はちょうど逆になり、必ず先に「左から右へ探索する」を行う必要があります。

Q:クイックソートの再帰深度最適化について、短い配列を選ぶとなぜ再帰深度が \\(\\log n\\) を超えないと保証できるのですか?

再帰深度とは、現在まだ戻っていない再帰呼び出しの数のことです。各ラウンドの番兵分割では、元の配列を 2 つの部分配列に分けます。再帰深度の最適化後は、下方向に再帰する部分配列の長さは最大でも元の配列長の半分です。最悪の場合でも毎回半分の長さになると仮定すれば、最終的な再帰深度は \\(\\log n\\) になります。

元のクイックソートを振り返ると、長いほうの配列に対して連続して再帰してしまう可能性があり、最悪の場合は \\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) と続き、再帰深度は \\(n\\) になります。再帰深度の最適化により、このような状況を避けられます。

Q:配列内のすべての要素が等しい場合、クイックソートの時間計算量は \\(O(n^2)\\) になりますか?このような退化はどう処理すべきですか?

はい。この場合は、番兵分割によって配列を「基準値より小さい」「基準値に等しい」「基準値より大きい」の 3 つの部分に分ける方法を検討できます。下方向に再帰するのは、小さい部分と大きい部分だけです。この方法では、入力要素がすべて等しい配列は、1 回の番兵分割だけで整列を完了できます。

Q:バケットソートの最悪時間計算量が \\(O(n^2)\\) なのはなぜですか?

最悪の場合、すべての要素が同じバケットに振り分けられます。その要素群を整列するのに \\(O(n^2)\\) のアルゴリズムを使えば、時間計算量は \\(O(n^2)\\) になります。

","path":["第 11 章   ソート","11.11   まとめ"],"tags":[]},{"location":"chapter_stack_and_queue/","level":1,"title":"第 5 章   スタックとキュー","text":"

Abstract

スタックは猫を積み重ねるようなもので、キューは猫が列に並ぶようなものです。

両者はそれぞれ、後入れ先出しと先入れ先出しの論理関係を表します。

","path":["第 5 章   スタックとキュー"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"章の内容","text":"
  • 5.1   スタック
  • 5.2   キュー
  • 5.3   両端キュー
  • 5.4   まとめ
","path":["第 5 章   スタックとキュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   両端キュー","text":"

キューでは、先頭要素を削除するか末尾に要素を追加することしかできません。次の図に示すように、両端キュー(double-ended queue)はより高い柔軟性を備えており、先頭と末尾の両方で要素の追加や削除を行えます。

図 5-7   両端キューの操作

","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#531","level":2,"title":"5.3.1   両端キューの基本操作","text":"

両端キューの基本操作を次の表に示します。具体的なメソッド名は、使用するプログラミング言語によって異なります。

表 5-3   両端キューの操作効率

メソッド名 説明 時間計算量 push_first() 先頭に要素を追加 \\(O(1)\\) push_last() 末尾に要素を追加 \\(O(1)\\) pop_first() 先頭要素を削除 \\(O(1)\\) pop_last() 末尾要素を削除 \\(O(1)\\) peek_first() 先頭要素にアクセス \\(O(1)\\) peek_last() 末尾要素にアクセス \\(O(1)\\)

同様に、プログラミング言語に組み込み実装されている両端キューのクラスを直接使うこともできます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby deque.py
from collections import deque\n\n# 両端キューを初期化\ndeq: deque[int] = deque()\n\n# 要素をエンキュー\ndeq.append(2)      # 末尾に追加\ndeq.append(5)\ndeq.append(4)\ndeq.appendleft(3)  # 先頭に追加\ndeq.appendleft(1)\n\n# 要素にアクセス\nfront: int = deq[0]  # 先頭要素\nrear: int = deq[-1]  # 末尾要素\n\n# 要素をデキュー\npop_front: int = deq.popleft()  # 先頭要素をデキュー\npop_rear: int = deq.pop()       # 末尾要素をデキュー\n\n# 両端キューの長さを取得\nsize: int = len(deq)\n\n# 両端キューが空かどうかを判定\nis_empty: bool = len(deq) == 0\n
deque.cpp
/* 両端キューを初期化 */\ndeque<int> deque;\n\n/* 要素をエンキュー */\ndeque.push_back(2);   // 末尾に追加\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3);  // 先頭に追加\ndeque.push_front(1);\n\n/* 要素にアクセス */\nint front = deque.front(); // 先頭要素\nint back = deque.back();   // 末尾要素\n\n/* 要素をデキュー */\ndeque.pop_front();  // 先頭要素をデキュー\ndeque.pop_back();   // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.size();\n\n/* 両端キューが空かどうかを判定 */\nbool empty = deque.empty();\n
deque.java
/* 両端キューを初期化 */\nDeque<Integer> deque = new LinkedList<>();\n\n/* 要素をエンキュー */\ndeque.offerLast(2);   // 末尾に追加\ndeque.offerLast(5);\ndeque.offerLast(4);\ndeque.offerFirst(3);  // 先頭に追加\ndeque.offerFirst(1);\n\n/* 要素にアクセス */\nint peekFirst = deque.peekFirst();  // 先頭要素\nint peekLast = deque.peekLast();    // 末尾要素\n\n/* 要素をデキュー */\nint popFirst = deque.pollFirst();  // 先頭要素をデキュー\nint popLast = deque.pollLast();    // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.size();\n\n/* 両端キューが空かどうかを判定 */\nboolean isEmpty = deque.isEmpty();\n
deque.cs
/* 両端キューを初期化 */\n// C# では、連結リスト LinkedList を両端キューとして使用する\nLinkedList<int> deque = new();\n\n/* 要素をエンキュー */\ndeque.AddLast(2);   // 末尾に追加\ndeque.AddLast(5);\ndeque.AddLast(4);\ndeque.AddFirst(3);  // 先頭に追加\ndeque.AddFirst(1);\n\n/* 要素にアクセス */\nint peekFirst = deque.First.Value;  // 先頭要素\nint peekLast = deque.Last.Value;    // 末尾要素\n\n/* 要素をデキュー */\ndeque.RemoveFirst();  // 先頭要素をデキュー\ndeque.RemoveLast();   // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.Count;\n\n/* 両端キューが空かどうかを判定 */\nbool isEmpty = deque.Count == 0;\n
deque_test.go
/* 両端キューを初期化 */\n// Go では、list を両端キューとして使用する\ndeque := list.New()\n\n/* 要素をエンキュー */\ndeque.PushBack(2)      // 末尾に追加\ndeque.PushBack(5)\ndeque.PushBack(4)\ndeque.PushFront(3)     // 先頭に追加\ndeque.PushFront(1)\n\n/* 要素にアクセス */\nfront := deque.Front() // 先頭要素\nrear := deque.Back()   // 末尾要素\n\n/* 要素をデキュー */\ndeque.Remove(front)    // 先頭要素をデキュー\ndeque.Remove(rear)     // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nsize := deque.Len()\n\n/* 両端キューが空かどうかを判定 */\nisEmpty := deque.Len() == 0\n
deque.swift
/* 両端キューを初期化 */\n// Swift には組み込みの両端キュークラスがないため、Array を両端キューとして使用する\nvar deque: [Int] = []\n\n/* 要素をエンキュー */\ndeque.append(2) // 末尾に追加\ndeque.append(5)\ndeque.append(4)\ndeque.insert(3, at: 0) // 先頭に追加\ndeque.insert(1, at: 0)\n\n/* 要素にアクセス */\nlet peekFirst = deque.first! // 先頭要素\nlet peekLast = deque.last! // 末尾要素\n\n/* 要素をデキュー */\n// Array で模擬する場合、popFirst の計算量は O(n)\nlet popFirst = deque.removeFirst() // 先頭要素をデキュー\nlet popLast = deque.removeLast() // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nlet size = deque.count\n\n/* 両端キューが空かどうかを判定 */\nlet isEmpty = deque.isEmpty\n
deque.js
/* 両端キューを初期化 */\n// JavaScript には組み込みの両端キューがないため、Array を両端キューとして使用するしかない\nconst deque = [];\n\n/* 要素をエンキュー */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// 配列であるため、unshift() メソッドの時間計算量は O(n) です\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* 要素にアクセス */\nconst peekFirst = deque[0];\nconst peekLast = deque[deque.length - 1];\n\n/* 要素をデキュー */\n// 配列であるため、shift() メソッドの時間計算量は O(n) です\nconst popFront = deque.shift();\nconst popBack = deque.pop();\n\n/* 両端キューの長さを取得 */\nconst size = deque.length;\n\n/* 両端キューが空かどうかを判定 */\nconst isEmpty = size === 0;\n
deque.ts
/* 両端キューを初期化 */\n// TypeScript には組み込みの両端キューがないため、Array を両端キューとして使用するしかない\nconst deque: number[] = [];\n\n/* 要素をエンキュー */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// 配列であるため、unshift() メソッドの時間計算量は O(n) です\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* 要素にアクセス */\nconst peekFirst: number = deque[0];\nconst peekLast: number = deque[deque.length - 1];\n\n/* 要素をデキュー */\n// 配列であるため、shift() メソッドの時間計算量は O(n) です\nconst popFront: number = deque.shift() as number;\nconst popBack: number = deque.pop() as number;\n\n/* 両端キューの長さを取得 */\nconst size: number = deque.length;\n\n/* 両端キューが空かどうかを判定 */\nconst isEmpty: boolean = size === 0;\n
deque.dart
/* 両端キューを初期化 */\n// Dart では、Queue は両端キューとして定義されています\nQueue<int> deque = Queue<int>();\n\n/* 要素をエンキュー */\ndeque.addLast(2);  // 末尾に追加\ndeque.addLast(5);\ndeque.addLast(4);\ndeque.addFirst(3); // 先頭に追加\ndeque.addFirst(1);\n\n/* 要素にアクセス */\nint peekFirst = deque.first; // 先頭要素\nint peekLast = deque.last;   // 末尾要素\n\n/* 要素をデキュー */\nint popFirst = deque.removeFirst(); // 先頭要素をデキュー\nint popLast = deque.removeLast();   // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.length;\n\n/* 両端キューが空かどうかを判定 */\nbool isEmpty = deque.isEmpty;\n
deque.rs
/* 両端キューを初期化 */\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* 要素をエンキュー */\ndeque.push_back(2);  // 末尾に追加\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // 先頭に追加\ndeque.push_front(1);\n\n/* 要素にアクセス */\nif let Some(front) = deque.front() { // 先頭要素\n}\nif let Some(rear) = deque.back() {   // 末尾要素\n}\n\n/* 要素をデキュー */\nif let Some(pop_front) = deque.pop_front() { // 先頭要素をデキュー\n}\nif let Some(pop_rear) = deque.pop_back() {   // 末尾要素をデキュー\n}\n\n/* 両端キューの長さを取得 */\nlet size = deque.len();\n\n/* 両端キューが空かどうかを判定 */\nlet is_empty = deque.is_empty();\n
deque.c
// C には組み込みの両端キューがありません\n
deque.kt
/* 両端キューを初期化 */\nval deque = LinkedList<Int>()\n\n/* 要素をエンキュー */\ndeque.offerLast(2)  // 末尾に追加\ndeque.offerLast(5)\ndeque.offerLast(4)\ndeque.offerFirst(3) // 先頭に追加\ndeque.offerFirst(1)\n\n/* 要素にアクセス */\nval peekFirst = deque.peekFirst() // 先頭要素\nval peekLast = deque.peekLast()   // 末尾要素\n\n/* 要素をデキュー */\nval popFirst = deque.pollFirst() // 先頭要素をデキュー\nval popLast = deque.pollLast()   // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nval size = deque.size\n\n/* 両端キューが空かどうかを判定 */\nval isEmpty = deque.isEmpty()\n
deque.rb
# 両端キューを初期化\n# Ruby には組み込みの両端キューがないため、Array を両端キューとして使用するしかありません\ndeque = []\n\n# 要素をエンキュー\ndeque << 2\ndeque << 5\ndeque << 4\n# 配列であるため、Array#unshift メソッドの時間計算量は O(n) です\ndeque.unshift(3)\ndeque.unshift(1)\n\n# 要素にアクセス\npeek_first = deque.first\npeek_last = deque.last\n\n# 要素をデキュー\n# 配列であるため、 Array#shift メソッドの時間計算量は O(n) です\npop_front = deque.shift\npop_back = deque.pop\n\n# 両端キューの長さを取得\nsize = deque.length\n\n# 両端キューが空かどうかを判定\nis_empty = size.zero?\n
実行の可視化

https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%0A%20%20%20%20deq%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20deq.append%282%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E5%B0%BE%0A%20%20%20%20deq.append%285%29%0A%20%20%20%20deq.append%284%29%0A%20%20%20%20deq.appendleft%283%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E9%A6%96%0A%20%20%20%20deq.appendleft%281%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20deq%5B0%5D%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%20%20%20%20rear%20%3D%20deq%5B-1%5D%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%20rear%20%3D%22,%20rear%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop_front%20%3D%20deq.popleft%28%29%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_front%20%3D%22,%20pop_front%29%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%20%20%20%20pop_rear%20%3D%20deq.pop%28%29%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_rear%20%3D%22,%20pop_rear%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28deq%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28deq%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#532","level":2,"title":"5.3.2   両端キューの実装 *","text":"

両端キューの実装はキューと似ており、連結リストまたは配列を基盤となるデータ構造として選べます。

","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#1","level":3,"title":"1.   双方向連結リストに基づく実装","text":"

前節を振り返ると、通常の単方向連結リストを使ってキューを実装しました。これは、先頭ノードの削除(デキューに対応)と末尾ノードの後ろへの新規ノード追加(エンキューに対応)を容易に行えるためです。

両端キューでは、先頭と末尾のどちらでもエンキューとデキューを行えます。言い換えると、両端キューではもう一方の対称方向の操作も実装する必要があります。そのため、両端キューの基盤データ構造として「双方向連結リスト」を採用します。

次の図に示すように、双方向連結リストの先頭ノードと末尾ノードを両端キューの先頭と末尾と見なし、両端でノードを追加および削除する機能を実現します。

LinkedListDequepush_last()push_first()pop_last()pop_first()

図 5-8   連結リストによる両端キューのエンキューとデキュー

実装コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_deque.py
class ListNode:\n    \"\"\"双方向連結リストノード\"\"\"\n\n    def __init__(self, val: int):\n        \"\"\"コンストラクタ\"\"\"\n        self.val: int = val\n        self.next: ListNode | None = None  # 後続ノードへの参照\n        self.prev: ListNode | None = None  # 前駆ノードへの参照\n\nclass LinkedListDeque:\n    \"\"\"双方向連結リストベースの両端キュー\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self._front: ListNode | None = None  # 先頭ノード front\n        self._rear: ListNode | None = None  # 末尾ノード rear\n        self._size: int = 0  # 両端キューの長さ\n\n    def size(self) -> int:\n        \"\"\"両端キューの長さを取得\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"両端キューが空かどうかを判定\"\"\"\n        return self._size == 0\n\n    def push(self, num: int, is_front: bool):\n        \"\"\"エンキュー操作\"\"\"\n        node = ListNode(num)\n        # 連結リストが空なら、front と rear の両方を node に向ける\n        if self.is_empty():\n            self._front = self._rear = node\n        # 先頭へのエンキュー操作\n        elif is_front:\n            # node を連結リストの先頭に追加\n            self._front.prev = node\n            node.next = self._front\n            self._front = node  # 先頭ノードを更新する\n        # 末尾へのエンキュー操作\n        else:\n            # node を連結リストの末尾に追加\n            self._rear.next = node\n            node.prev = self._rear\n            self._rear = node  # 末尾ノードを更新する\n        self._size += 1  # キューの長さを更新\n\n    def push_first(self, num: int):\n        \"\"\"キュー先頭にエンキュー\"\"\"\n        self.push(num, True)\n\n    def push_last(self, num: int):\n        \"\"\"キュー末尾にエンキュー\"\"\"\n        self.push(num, False)\n\n    def pop(self, is_front: bool) -> int:\n        \"\"\"デキュー操作\"\"\"\n        if self.is_empty():\n            raise IndexError(\"両端キューが空です\")\n        # キュー先頭からの取り出し\n        if is_front:\n            val: int = self._front.val  # 先頭ノードの値を一時保存\n            # 先頭ノードを削除\n            fnext: ListNode | None = self._front.next\n            if fnext is not None:\n                fnext.prev = None\n                self._front.next = None\n            self._front = fnext  # 先頭ノードを更新する\n        # キュー末尾からの取り出し\n        else:\n            val: int = self._rear.val  # 末尾ノードの値を一時保存\n            # 末尾ノードを削除\n            rprev: ListNode | None = self._rear.prev\n            if rprev is not None:\n                rprev.next = None\n                self._rear.prev = None\n            self._rear = rprev  # 末尾ノードを更新する\n        self._size -= 1  # キューの長さを更新\n        return val\n\n    def pop_first(self) -> int:\n        \"\"\"キュー先頭からデキュー\"\"\"\n        return self.pop(True)\n\n    def pop_last(self) -> int:\n        \"\"\"キュー末尾からデキュー\"\"\"\n        return self.pop(False)\n\n    def peek_first(self) -> int:\n        \"\"\"キュー先頭の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"両端キューが空です\")\n        return self._front.val\n\n    def peek_last(self) -> int:\n        \"\"\"キュー末尾の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"両端キューが空です\")\n        return self._rear.val\n\n    def to_array(self) -> list[int]:\n        \"\"\"出力用の配列を返す\"\"\"\n        node = self._front\n        res = [0] * self.size()\n        for i in range(self.size()):\n            res[i] = node.val\n            node = node.next\n        return res\n
linkedlist_deque.cpp
/* 双方向連結リストノード */\nstruct DoublyListNode {\n    int val;              // ノード値\n    DoublyListNode *next; // 後継ノードへのポインタ\n    DoublyListNode *prev; // 前駆ノードへのポインタ\n    DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {\n    }\n};\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n  private:\n    DoublyListNode *front, *rear; // 先頭ノード front、末尾ノード rear\n    int queSize = 0;              // 両端キューの長さ\n\n  public:\n    /* コンストラクタ */\n    LinkedListDeque() : front(nullptr), rear(nullptr) {\n    }\n\n    /* デストラクタメソッド */\n    ~LinkedListDeque() {\n        // 連結リストを走査してノードを削除し、メモリを解放する\n        DoublyListNode *pre, *cur = front;\n        while (cur != nullptr) {\n            pre = cur;\n            cur = cur->next;\n            delete pre;\n        }\n    }\n\n    /* 両端キューの長さを取得 */\n    int size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* エンキュー操作 */\n    void push(int num, bool isFront) {\n        DoublyListNode *node = new DoublyListNode(num);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (isEmpty())\n            front = rear = node;\n        // 先頭へのエンキュー操作\n        else if (isFront) {\n            // node を連結リストの先頭に追加\n            front->prev = node;\n            node->next = front;\n            front = node; // 先頭ノードを更新する\n        // 末尾へのエンキュー操作\n        } else {\n            // node を連結リストの末尾に追加\n            rear->next = node;\n            node->prev = rear;\n            rear = node; // 末尾ノードを更新する\n        }\n        queSize++; // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* キュー末尾にエンキュー */\n    void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* デキュー操作 */\n    int pop(bool isFront) {\n        if (isEmpty())\n            throw out_of_range(\"キューが空です\");\n        int val;\n        // キュー先頭からの取り出し\n        if (isFront) {\n            val = front->val; // 先頭ノードの値を一時保存\n            // 先頭ノードを削除\n            DoublyListNode *fNext = front->next;\n            if (fNext != nullptr) {\n                fNext->prev = nullptr;\n                front->next = nullptr;\n            }\n            delete front;\n            front = fNext; // 先頭ノードを更新する\n        // キュー末尾からの取り出し\n        } else {\n            val = rear->val; // 末尾ノードの値を一時保存\n            // 末尾ノードを削除\n            DoublyListNode *rPrev = rear->prev;\n            if (rPrev != nullptr) {\n                rPrev->next = nullptr;\n                rear->prev = nullptr;\n            }\n            delete rear;\n            rear = rPrev; // 末尾ノードを更新する\n        }\n        queSize--; // キューの長さを更新\n        return val;\n    }\n\n    /* キュー先頭からデキュー */\n    int popFirst() {\n        return pop(true);\n    }\n\n    /* キュー末尾からデキュー */\n    int popLast() {\n        return pop(false);\n    }\n\n    /* キュー先頭の要素にアクセス */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"両端キューが空です\");\n        return front->val;\n    }\n\n    /* キュー末尾の要素にアクセス */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"両端キューが空です\");\n        return rear->val;\n    }\n\n    /* 出力用の配列を返す */\n    vector<int> toVector() {\n        DoublyListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_deque.java
/* 双方向連結リストノード */\nclass ListNode {\n    int val; // ノード値\n    ListNode next; // 後続ノードへの参照\n    ListNode prev; // 前駆ノードへの参照\n\n    ListNode(int val) {\n        this.val = val;\n        prev = next = null;\n    }\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    private ListNode front, rear; // 先頭ノード front、末尾ノード rear\n    private int queSize = 0; // 両端キューの長さ\n\n    public LinkedListDeque() {\n        front = rear = null;\n    }\n\n    /* 両端キューの長さを取得 */\n    public int size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* エンキュー操作 */\n    private void push(int num, boolean isFront) {\n        ListNode node = new ListNode(num);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (isEmpty())\n            front = rear = node;\n        // 先頭へのエンキュー操作\n        else if (isFront) {\n            // node を連結リストの先頭に追加\n            front.prev = node;\n            node.next = front;\n            front = node; // 先頭ノードを更新する\n        // 末尾へのエンキュー操作\n        } else {\n            // node を連結リストの末尾に追加\n            rear.next = node;\n            node.prev = rear;\n            rear = node; // 末尾ノードを更新する\n        }\n        queSize++; // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    public void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* キュー末尾にエンキュー */\n    public void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* デキュー操作 */\n    private int pop(boolean isFront) {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        int val;\n        // キュー先頭からの取り出し\n        if (isFront) {\n            val = front.val; // 先頭ノードの値を一時保存\n            // 先頭ノードを削除\n            ListNode fNext = front.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front.next = null;\n            }\n            front = fNext; // 先頭ノードを更新する\n        // キュー末尾からの取り出し\n        } else {\n            val = rear.val; // 末尾ノードの値を一時保存\n            // 末尾ノードを削除\n            ListNode rPrev = rear.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear.prev = null;\n            }\n            rear = rPrev; // 末尾ノードを更新する\n        }\n        queSize--; // キューの長さを更新\n        return val;\n    }\n\n    /* キュー先頭からデキュー */\n    public int popFirst() {\n        return pop(true);\n    }\n\n    /* キュー末尾からデキュー */\n    public int popLast() {\n        return pop(false);\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* キュー末尾の要素にアクセス */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return rear.val;\n    }\n\n    /* 出力用の配列を返す */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_deque.cs
/* 双方向連結リストノード */\nclass ListNode(int val) {\n    public int val = val;       // ノード値\n    public ListNode? next = null; // 後続ノードへの参照\n    public ListNode? prev = null; // 前駆ノードへの参照\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    ListNode? front, rear; // 先頭ノード front、末尾ノード rear\n    int queSize = 0;      // 両端キューの長さ\n\n    public LinkedListDeque() {\n        front = null;\n        rear = null;\n    }\n\n    /* 両端キューの長さを取得 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* エンキュー操作 */\n    void Push(int num, bool isFront) {\n        ListNode node = new(num);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (IsEmpty()) {\n            front = node;\n            rear = node;\n        }\n        // 先頭へのエンキュー操作\n        else if (isFront) {\n            // node を連結リストの先頭に追加\n            front!.prev = node;\n            node.next = front;\n            front = node; // 先頭ノードを更新する\n        }\n        // 末尾へのエンキュー操作\n        else {\n            // node を連結リストの末尾に追加\n            rear!.next = node;\n            node.prev = rear;\n            rear = node;  // 末尾ノードを更新する\n        }\n\n        queSize++; // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    public void PushFirst(int num) {\n        Push(num, true);\n    }\n\n    /* キュー末尾にエンキュー */\n    public void PushLast(int num) {\n        Push(num, false);\n    }\n\n    /* デキュー操作 */\n    int? Pop(bool isFront) {\n        if (IsEmpty())\n            throw new Exception();\n        int? val;\n        // キュー先頭からの取り出し\n        if (isFront) {\n            val = front?.val; // 先頭ノードの値を一時保存\n            // 先頭ノードを削除\n            ListNode? fNext = front?.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front!.next = null;\n            }\n            front = fNext;   // 先頭ノードを更新する\n        }\n        // キュー末尾からの取り出し\n        else {\n            val = rear?.val;  // 末尾ノードの値を一時保存\n            // 末尾ノードを削除\n            ListNode? rPrev = rear?.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear!.prev = null;\n            }\n            rear = rPrev;    // 末尾ノードを更新する\n        }\n\n        queSize--; // キューの長さを更新\n        return val;\n    }\n\n    /* キュー先頭からデキュー */\n    public int? PopFirst() {\n        return Pop(true);\n    }\n\n    /* キュー末尾からデキュー */\n    public int? PopLast() {\n        return Pop(false);\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int? PeekFirst() {\n        if (IsEmpty())\n            throw new Exception();\n        return front?.val;\n    }\n\n    /* キュー末尾の要素にアクセス */\n    public int? PeekLast() {\n        if (IsEmpty())\n            throw new Exception();\n        return rear?.val;\n    }\n\n    /* 出力用の配列を返す */\n    public int?[] ToArray() {\n        ListNode? node = front;\n        int?[] res = new int?[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node?.val;\n            node = node?.next;\n        }\n\n        return res;\n    }\n}\n
linkedlist_deque.go
/* 双方向連結リストベースの両端キュー */\ntype linkedListDeque struct {\n    // 組み込みパッケージ list を使う\n    data *list.List\n}\n\n/* 両端キューを初期化する */\nfunc newLinkedListDeque() *linkedListDeque {\n    return &linkedListDeque{\n        data: list.New(),\n    }\n}\n\n/* キュー先頭に要素を追加する */\nfunc (s *linkedListDeque) pushFirst(value any) {\n    s.data.PushFront(value)\n}\n\n/* キュー末尾に要素を追加する */\nfunc (s *linkedListDeque) pushLast(value any) {\n    s.data.PushBack(value)\n}\n\n/* 先頭要素を取り出す */\nfunc (s *linkedListDeque) popFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* 末尾要素を取り出す */\nfunc (s *linkedListDeque) popLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* キュー先頭の要素にアクセス */\nfunc (s *linkedListDeque) peekFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* キュー末尾の要素にアクセス */\nfunc (s *linkedListDeque) peekLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* キューの長さを取得 */\nfunc (s *linkedListDeque) size() int {\n    return s.data.Len()\n}\n\n/* キューが空かどうかを判定 */\nfunc (s *linkedListDeque) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* 表示用に List を取得 */\nfunc (s *linkedListDeque) toList() *list.List {\n    return s.data\n}\n
linkedlist_deque.swift
/* 双方向連結リストノード */\nclass ListNode {\n    var val: Int // ノード値\n    var next: ListNode? // 後続ノードへの参照\n    weak var prev: ListNode? // 前駆ノードへの参照\n\n    init(val: Int) {\n        self.val = val\n    }\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    private var front: ListNode? // 先頭ノード front\n    private var rear: ListNode? // 末尾ノード rear\n    private var _size: Int // 両端キューの長さ\n\n    init() {\n        _size = 0\n    }\n\n    /* 両端キューの長さを取得 */\n    func size() -> Int {\n        _size\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* エンキュー操作 */\n    private func push(num: Int, isFront: Bool) {\n        let node = ListNode(val: num)\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if isEmpty() {\n            front = node\n            rear = node\n        }\n        // 先頭へのエンキュー操作\n        else if isFront {\n            // node を連結リストの先頭に追加\n            front?.prev = node\n            node.next = front\n            front = node // 先頭ノードを更新する\n        }\n        // 末尾へのエンキュー操作\n        else {\n            // node を連結リストの末尾に追加\n            rear?.next = node\n            node.prev = rear\n            rear = node // 末尾ノードを更新する\n        }\n        _size += 1 // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    func pushFirst(num: Int) {\n        push(num: num, isFront: true)\n    }\n\n    /* キュー末尾にエンキュー */\n    func pushLast(num: Int) {\n        push(num: num, isFront: false)\n    }\n\n    /* デキュー操作 */\n    private func pop(isFront: Bool) -> Int {\n        if isEmpty() {\n            fatalError(\"両端キューが空です\")\n        }\n        let val: Int\n        // キュー先頭からの取り出し\n        if isFront {\n            val = front!.val // 先頭ノードの値を一時保存\n            // 先頭ノードを削除\n            let fNext = front?.next\n            if fNext != nil {\n                fNext?.prev = nil\n                front?.next = nil\n            }\n            front = fNext // 先頭ノードを更新する\n        }\n        // キュー末尾からの取り出し\n        else {\n            val = rear!.val // 末尾ノードの値を一時保存\n            // 末尾ノードを削除\n            let rPrev = rear?.prev\n            if rPrev != nil {\n                rPrev?.next = nil\n                rear?.prev = nil\n            }\n            rear = rPrev // 末尾ノードを更新する\n        }\n        _size -= 1 // キューの長さを更新\n        return val\n    }\n\n    /* キュー先頭からデキュー */\n    func popFirst() -> Int {\n        pop(isFront: true)\n    }\n\n    /* キュー末尾からデキュー */\n    func popLast() -> Int {\n        pop(isFront: false)\n    }\n\n    /* キュー先頭の要素にアクセス */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"両端キューが空です\")\n        }\n        return front!.val\n    }\n\n    /* キュー末尾の要素にアクセス */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"両端キューが空です\")\n        }\n        return rear!.val\n    }\n\n    /* 出力用の配列を返す */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_deque.js
/* 双方向連結リストノード */\nclass ListNode {\n    prev; // 前駆ノードへの参照(ポインタ)\n    next; // 後継ノードへの参照(ポインタ)\n    val; // ノード値\n\n    constructor(val) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    #front; // 先頭ノード front\n    #rear; // 末尾ノード rear\n    #queSize; // 両端キューの長さ\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n        this.#queSize = 0;\n    }\n\n    /* 末尾へのエンキュー操作 */\n    pushLast(val) {\n        const node = new ListNode(val);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // node を連結リストの末尾に追加\n            this.#rear.next = node;\n            node.prev = this.#rear;\n            this.#rear = node; // 末尾ノードを更新する\n        }\n        this.#queSize++;\n    }\n\n    /* 先頭へのエンキュー操作 */\n    pushFirst(val) {\n        const node = new ListNode(val);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // node を連結リストの先頭に追加\n            this.#front.prev = node;\n            node.next = this.#front;\n            this.#front = node; // 先頭ノードを更新する\n        }\n        this.#queSize++;\n    }\n\n    /* キュー末尾からの取り出し */\n    popLast() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#rear.val; // 末尾ノードの値を保存する\n        // 末尾ノードを削除\n        let temp = this.#rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.#rear.prev = null;\n        }\n        this.#rear = temp; // 末尾ノードを更新する\n        this.#queSize--;\n        return value;\n    }\n\n    /* キュー先頭からの取り出し */\n    popFirst() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#front.val; // 末尾ノードの値を保存する\n        // 先頭ノードを削除\n        let temp = this.#front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.#front.next = null;\n        }\n        this.#front = temp; // 先頭ノードを更新する\n        this.#queSize--;\n        return value;\n    }\n\n    /* キュー末尾の要素にアクセス */\n    peekLast() {\n        return this.#queSize === 0 ? null : this.#rear.val;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peekFirst() {\n        return this.#queSize === 0 ? null : this.#front.val;\n    }\n\n    /* 両端キューの長さを取得 */\n    size() {\n        return this.#queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* 両端キューを出力する */\n    print() {\n        const arr = [];\n        let temp = this.#front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
linkedlist_deque.ts
/* 双方向連結リストノード */\nclass ListNode {\n    prev: ListNode; // 前駆ノードへの参照(ポインタ)\n    next: ListNode; // 後継ノードへの参照(ポインタ)\n    val: number; // ノード値\n\n    constructor(val: number) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    private front: ListNode; // 先頭ノード front\n    private rear: ListNode; // 末尾ノード rear\n    private queSize: number; // 両端キューの長さ\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n        this.queSize = 0;\n    }\n\n    /* 末尾へのエンキュー操作 */\n    pushLast(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // node を連結リストの末尾に追加\n            this.rear.next = node;\n            node.prev = this.rear;\n            this.rear = node; // 末尾ノードを更新する\n        }\n        this.queSize++;\n    }\n\n    /* 先頭へのエンキュー操作 */\n    pushFirst(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // node を連結リストの先頭に追加\n            this.front.prev = node;\n            node.next = this.front;\n            this.front = node; // 先頭ノードを更新する\n        }\n        this.queSize++;\n    }\n\n    /* キュー末尾からの取り出し */\n    popLast(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.rear.val; // 末尾ノードの値を保存する\n        // 末尾ノードを削除\n        let temp: ListNode = this.rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.rear.prev = null;\n        }\n        this.rear = temp; // 末尾ノードを更新する\n        this.queSize--;\n        return value;\n    }\n\n    /* キュー先頭からの取り出し */\n    popFirst(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.front.val; // 末尾ノードの値を保存する\n        // 先頭ノードを削除\n        let temp: ListNode = this.front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.front.next = null;\n        }\n        this.front = temp; // 先頭ノードを更新する\n        this.queSize--;\n        return value;\n    }\n\n    /* キュー末尾の要素にアクセス */\n    peekLast(): number {\n        return this.queSize === 0 ? null : this.rear.val;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peekFirst(): number {\n        return this.queSize === 0 ? null : this.front.val;\n    }\n\n    /* 両端キューの長さを取得 */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* 両端キューを出力する */\n    print(): void {\n        const arr: number[] = [];\n        let temp: ListNode = this.front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
linkedlist_deque.dart
/* 双方向連結リストノード */\nclass ListNode {\n  int val; // ノード値\n  ListNode? next; // 後続ノードへの参照\n  ListNode? prev; // 前駆ノードへの参照\n\n  ListNode(this.val, {this.next, this.prev});\n}\n\n/* 双方向連結リストに基づく双方向キュー */\nclass LinkedListDeque {\n  late ListNode? _front; // 先頭ノード _front\n  late ListNode? _rear; // 末尾ノード _rear\n  int _queSize = 0; // 両端キューの長さ\n\n  LinkedListDeque() {\n    this._front = null;\n    this._rear = null;\n  }\n\n  /* 両端キューの長さを取得 */\n  int size() {\n    return this._queSize;\n  }\n\n  /* 両端キューが空かどうかを判定 */\n  bool isEmpty() {\n    return size() == 0;\n  }\n\n  /* エンキュー操作 */\n  void push(int _num, bool isFront) {\n    final ListNode node = ListNode(_num);\n    if (isEmpty()) {\n      // 連結リストが空なら、`_front` と `_rear` の両方を `node` に向ける\n      _front = _rear = node;\n    } else if (isFront) {\n      // 先頭へのエンキュー操作\n      // node を連結リストの先頭に追加する\n      _front!.prev = node;\n      node.next = _front;\n      _front = node; // 先頭ノードを更新する\n    } else {\n      // 末尾へのエンキュー操作\n      // node を連結リストの末尾に追加する\n      _rear!.next = node;\n      node.prev = _rear;\n      _rear = node; // 末尾ノードを更新する\n    }\n    _queSize++; // キューの長さを更新\n  }\n\n  /* キュー先頭にエンキュー */\n  void pushFirst(int _num) {\n    push(_num, true);\n  }\n\n  /* キュー末尾にエンキュー */\n  void pushLast(int _num) {\n    push(_num, false);\n  }\n\n  /* デキュー操作 */\n  int? pop(bool isFront) {\n    // キューが空なら、そのまま `null` を返す\n    if (isEmpty()) {\n      return null;\n    }\n    final int val;\n    if (isFront) {\n      // キュー先頭からの取り出し\n      val = _front!.val; // 先頭ノードの値を一時保存\n      // 先頭ノードを削除\n      ListNode? fNext = _front!.next;\n      if (fNext != null) {\n        fNext.prev = null;\n        _front!.next = null;\n      }\n      _front = fNext; // 先頭ノードを更新する\n    } else {\n      // キュー末尾からの取り出し\n      val = _rear!.val; // 末尾ノードの値を一時保存\n      // 末尾ノードを削除\n      ListNode? rPrev = _rear!.prev;\n      if (rPrev != null) {\n        rPrev.next = null;\n        _rear!.prev = null;\n      }\n      _rear = rPrev; // 末尾ノードを更新する\n    }\n    _queSize--; // キューの長さを更新\n    return val;\n  }\n\n  /* キュー先頭からデキュー */\n  int? popFirst() {\n    return pop(true);\n  }\n\n  /* キュー末尾からデキュー */\n  int? popLast() {\n    return pop(false);\n  }\n\n  /* キュー先頭の要素にアクセス */\n  int? peekFirst() {\n    return _front?.val;\n  }\n\n  /* キュー末尾の要素にアクセス */\n  int? peekLast() {\n    return _rear?.val;\n  }\n\n  /* 出力用の配列を返す */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> res = [];\n    for (int i = 0; i < _queSize; i++) {\n      res.add(node!.val);\n      node = node.next;\n    }\n    return res;\n  }\n}\n
linkedlist_deque.rs
/* 双方向連結リストノード */\npub struct ListNode<T> {\n    pub val: T,                                 // ノード値\n    pub next: Option<Rc<RefCell<ListNode<T>>>>, // 後継ノードへのポインタ\n    pub prev: Option<Rc<RefCell<ListNode<T>>>>, // 前駆ノードへのポインタ\n}\n\nimpl<T> ListNode<T> {\n    pub fn new(val: T) -> Rc<RefCell<ListNode<T>>> {\n        Rc::new(RefCell::new(ListNode {\n            val,\n            next: None,\n            prev: None,\n        }))\n    }\n}\n\n/* 双方向連結リストベースの両端キュー */\n#[allow(dead_code)]\npub struct LinkedListDeque<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // 先頭ノード front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // 末尾ノード rear\n    que_size: usize,                         // 両端キューの長さ\n}\n\nimpl<T: Copy> LinkedListDeque<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* 両端キューの長さを取得 */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* エンキュー操作 */\n    fn push(&mut self, num: T, is_front: bool) {\n        let node = ListNode::new(num);\n        // 先頭へのエンキュー操作\n        if is_front {\n            match self.front.take() {\n                // 連結リストが空なら、front と rear の両方を node に向ける\n                None => {\n                    self.rear = Some(node.clone());\n                    self.front = Some(node);\n                }\n                // node を連結リストの先頭に追加\n                Some(old_front) => {\n                    old_front.borrow_mut().prev = Some(node.clone());\n                    node.borrow_mut().next = Some(old_front);\n                    self.front = Some(node); // 先頭ノードを更新する\n                }\n            }\n        }\n        // 末尾へのエンキュー操作\n        else {\n            match self.rear.take() {\n                // 連結リストが空なら、front と rear の両方を node に向ける\n                None => {\n                    self.front = Some(node.clone());\n                    self.rear = Some(node);\n                }\n                // node を連結リストの末尾に追加\n                Some(old_rear) => {\n                    old_rear.borrow_mut().next = Some(node.clone());\n                    node.borrow_mut().prev = Some(old_rear);\n                    self.rear = Some(node); // 末尾ノードを更新する\n                }\n            }\n        }\n        self.que_size += 1; // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    pub fn push_first(&mut self, num: T) {\n        self.push(num, true);\n    }\n\n    /* キュー末尾にエンキュー */\n    pub fn push_last(&mut self, num: T) {\n        self.push(num, false);\n    }\n\n    /* デキュー操作 */\n    fn pop(&mut self, is_front: bool) -> Option<T> {\n        // キューが空なら、そのまま `None` を返す\n        if self.is_empty() {\n            return None;\n        };\n        // キュー先頭からの取り出し\n        if is_front {\n            self.front.take().map(|old_front| {\n                match old_front.borrow_mut().next.take() {\n                    Some(new_front) => {\n                        new_front.borrow_mut().prev.take();\n                        self.front = Some(new_front); // 先頭ノードを更新する\n                    }\n                    None => {\n                        self.rear.take();\n                    }\n                }\n                self.que_size -= 1; // キューの長さを更新\n                old_front.borrow().val\n            })\n        }\n        // キュー末尾からの取り出し\n        else {\n            self.rear.take().map(|old_rear| {\n                match old_rear.borrow_mut().prev.take() {\n                    Some(new_rear) => {\n                        new_rear.borrow_mut().next.take();\n                        self.rear = Some(new_rear); // 末尾ノードを更新する\n                    }\n                    None => {\n                        self.front.take();\n                    }\n                }\n                self.que_size -= 1; // キューの長さを更新\n                old_rear.borrow().val\n            })\n        }\n    }\n\n    /* キュー先頭からデキュー */\n    pub fn pop_first(&mut self) -> Option<T> {\n        return self.pop(true);\n    }\n\n    /* キュー末尾からデキュー */\n    pub fn pop_last(&mut self) -> Option<T> {\n        return self.pop(false);\n    }\n\n    /* キュー先頭の要素にアクセス */\n    pub fn peek_first(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* キュー末尾の要素にアクセス */\n    pub fn peek_last(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.rear.as_ref()\n    }\n\n    /* 出力用の配列を返す */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n        res\n    }\n}\n
linkedlist_deque.c
/* 双方向連結リストノード */\ntypedef struct DoublyListNode {\n    int val;                     // ノード値\n    struct DoublyListNode *next; // 後続ノード\n    struct DoublyListNode *prev; // 前駆ノード\n} DoublyListNode;\n\n/* コンストラクタ */\nDoublyListNode *newDoublyListNode(int num) {\n    DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode));\n    new->val = num;\n    new->next = NULL;\n    new->prev = NULL;\n    return new;\n}\n\n/* デストラクタ */\nvoid delDoublyListNode(DoublyListNode *node) {\n    free(node);\n}\n\n/* 双方向連結リストベースの両端キュー */\ntypedef struct {\n    DoublyListNode *front, *rear; // 先頭ノード front、末尾ノード rear\n    int queSize;                  // 両端キューの長さ\n} LinkedListDeque;\n\n/* コンストラクタ */\nLinkedListDeque *newLinkedListDeque() {\n    LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque));\n    deque->front = NULL;\n    deque->rear = NULL;\n    deque->queSize = 0;\n    return deque;\n}\n\n/* デストラクタ */\nvoid delLinkedListdeque(LinkedListDeque *deque) {\n    // すべてのノードを解放\n    for (int i = 0; i < deque->queSize && deque->front != NULL; i++) {\n        DoublyListNode *tmp = deque->front;\n        deque->front = deque->front->next;\n        free(tmp);\n    }\n    // deque 構造体を解放する\n    free(deque);\n}\n\n/* キューの長さを取得 */\nint size(LinkedListDeque *deque) {\n    return deque->queSize;\n}\n\n/* キューが空かどうかを判定 */\nbool empty(LinkedListDeque *deque) {\n    return (size(deque) == 0);\n}\n\n/* エンキュー */\nvoid push(LinkedListDeque *deque, int num, bool isFront) {\n    DoublyListNode *node = newDoublyListNode(num);\n    // 連結リストが空なら、`front` と `rear` の両方を `node` に向ける\n    if (empty(deque)) {\n        deque->front = deque->rear = node;\n    }\n    // 先頭へのエンキュー操作\n    else if (isFront) {\n        // node を連結リストの先頭に追加\n        deque->front->prev = node;\n        node->next = deque->front;\n        deque->front = node; // 先頭ノードを更新する\n    }\n    // 末尾へのエンキュー操作\n    else {\n        // node を連結リストの末尾に追加\n        deque->rear->next = node;\n        node->prev = deque->rear;\n        deque->rear = node;\n    }\n    deque->queSize++; // キューの長さを更新\n}\n\n/* キュー先頭にエンキュー */\nvoid pushFirst(LinkedListDeque *deque, int num) {\n    push(deque, num, true);\n}\n\n/* キュー末尾にエンキュー */\nvoid pushLast(LinkedListDeque *deque, int num) {\n    push(deque, num, false);\n}\n\n/* キュー先頭の要素にアクセス */\nint peekFirst(LinkedListDeque *deque) {\n    assert(size(deque) && deque->front);\n    return deque->front->val;\n}\n\n/* キュー末尾の要素にアクセス */\nint peekLast(LinkedListDeque *deque) {\n    assert(size(deque) && deque->rear);\n    return deque->rear->val;\n}\n\n/* デキュー */\nint pop(LinkedListDeque *deque, bool isFront) {\n    if (empty(deque))\n        return -1;\n    int val;\n    // キュー先頭からの取り出し\n    if (isFront) {\n        val = peekFirst(deque); // 先頭ノードの値を一時保存\n        DoublyListNode *fNext = deque->front->next;\n        if (fNext) {\n            fNext->prev = NULL;\n            deque->front->next = NULL;\n        }\n        delDoublyListNode(deque->front);\n        deque->front = fNext; // 先頭ノードを更新する\n    }\n    // キュー末尾からの取り出し\n    else {\n        val = peekLast(deque); // 末尾ノードの値を一時保存\n        DoublyListNode *rPrev = deque->rear->prev;\n        if (rPrev) {\n            rPrev->next = NULL;\n            deque->rear->prev = NULL;\n        }\n        delDoublyListNode(deque->rear);\n        deque->rear = rPrev; // 末尾ノードを更新する\n    }\n    deque->queSize--; // キューの長さを更新\n    return val;\n}\n\n/* キュー先頭からデキュー */\nint popFirst(LinkedListDeque *deque) {\n    return pop(deque, true);\n}\n\n/* キュー末尾からデキュー */\nint popLast(LinkedListDeque *deque) {\n    return pop(deque, false);\n}\n\n/* キューを出力する */\nvoid printLinkedListDeque(LinkedListDeque *deque) {\n    int *arr = malloc(sizeof(int) * deque->queSize);\n    // 連結リスト内のデータを配列にコピー\n    int i;\n    DoublyListNode *node;\n    for (i = 0, node = deque->front; i < deque->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, deque->queSize);\n    free(arr);\n}\n
linkedlist_deque.kt
/* 双方向連結リストノード */\nclass ListNode(var _val: Int) {\n    // ノード値\n    var next: ListNode? = null // 後続ノードへの参照\n    var prev: ListNode? = null // 前駆ノードへの参照\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    private var front: ListNode? = null // 先頭ノード front\n    private var rear: ListNode? = null // 末尾ノード rear\n    private var queSize: Int = 0 // 両端キューの長さ\n\n    /* 両端キューの長さを取得 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* エンキュー操作 */\n    fun push(num: Int, isFront: Boolean) {\n        val node = ListNode(num)\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (isEmpty()) {\n            rear = node\n            front = rear\n            // 先頭へのエンキュー操作\n        } else if (isFront) {\n            // node を連結リストの先頭に追加\n            front?.prev = node\n            node.next = front\n            front = node // 先頭ノードを更新する\n            // 末尾へのエンキュー操作\n        } else {\n            // node を連結リストの末尾に追加\n            rear?.next = node\n            node.prev = rear\n            rear = node // 末尾ノードを更新する\n        }\n        queSize++ // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    fun pushFirst(num: Int) {\n        push(num, true)\n    }\n\n    /* キュー末尾にエンキュー */\n    fun pushLast(num: Int) {\n        push(num, false)\n    }\n\n    /* デキュー操作 */\n    fun pop(isFront: Boolean): Int {\n        if (isEmpty()) \n            throw IndexOutOfBoundsException()\n        val _val: Int\n        // キュー先頭からの取り出し\n        if (isFront) {\n            _val = front!!._val // 先頭ノードの値を一時保存\n            // 先頭ノードを削除\n            val fNext = front!!.next\n            if (fNext != null) {\n                fNext.prev = null\n                front!!.next = null\n            }\n            front = fNext // 先頭ノードを更新する\n            // キュー末尾からの取り出し\n        } else {\n            _val = rear!!._val // 末尾ノードの値を一時保存\n            // 末尾ノードを削除\n            val rPrev = rear!!.prev\n            if (rPrev != null) {\n                rPrev.next = null\n                rear!!.prev = null\n            }\n            rear = rPrev // 末尾ノードを更新する\n        }\n        queSize-- // キューの長さを更新\n        return _val\n    }\n\n    /* キュー先頭からデキュー */\n    fun popFirst(): Int {\n        return pop(true)\n    }\n\n    /* キュー末尾からデキュー */\n    fun popLast(): Int {\n        return pop(false)\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* キュー末尾の要素にアクセス */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return rear!!._val\n    }\n\n    /* 出力用の配列を返す */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_deque.rb
=begin\nFile: linkedlist_deque.rb\nCreated Time: 2024-04-06\nAuthor: Xuan Khoa Tu Nguyen (ngxktuzkai2000@gmail.com)\n=end\n\n# ## 双方向連結リストノード\nclass ListNode\n  attr_accessor :val\n  attr_accessor :next # 後続ノードへの参照\n  attr_accessor :prev # 前ノードへの参照\n\n  ### コンストラクタ ###\n  def initialize(val)\n    @val = val\n  end\nend\n\n### 双方向連結リストで実装した両端キュー ###\nclass LinkedListDeque\n  ### 両端キューの長さを取得 ###\n  attr_reader :size\n\n  ### コンストラクタ ###\n  def initialize\n    @front = nil  # 先頭ノード front\n    @rear = nil   # 末尾ノード rear\n    @size = 0     # 両端キューの長さ\n  end\n\n  ### 両端キューが空か判定 ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### エンキュー操作 ###\n  def push(num, is_front)\n    node = ListNode.new(num)\n    # 連結リストが空なら、`front` と `rear` の両方を `node` に向ける\n    if is_empty?\n      @front = @rear = node\n    # 先頭へのエンキュー操作\n    elsif is_front\n      # node を連結リストの先頭に追加\n      @front.prev = node\n      node.next = @front\n      @front = node # 先頭ノードを更新する\n    # 末尾へのエンキュー操作\n    else\n      # node を連結リストの末尾に追加\n      @rear.next = node\n      node.prev = @rear\n      @rear = node # 末尾ノードを更新する\n    end\n    @size += 1 # キューの長さを更新\n  end\n\n  ### キュー先頭に追加 ###\n  def push_first(num)\n    push(num, true)\n  end\n\n  ### キュー末尾に追加 ###\n  def push_last(num)\n    push(num, false)\n  end\n\n  ### デキュー操作 ###\n  def pop(is_front)\n    raise IndexError, '両端キューは空です' if is_empty?\n\n    # キュー先頭からの取り出し\n    if is_front\n      val = @front.val # 先頭ノードの値を一時保存\n      # 先頭ノードを削除\n      fnext = @front.next\n      unless fnext.nil?\n        fnext.prev = nil\n        @front.next = nil\n      end\n      @front = fnext # 先頭ノードを更新する\n    # キュー末尾からの取り出し\n    else\n      val = @rear.val # 末尾ノードの値を一時保存\n      # 末尾ノードを削除\n      rprev = @rear.prev\n      unless rprev.nil?\n        rprev.next = nil\n        @rear.prev = nil\n      end\n      @rear = rprev # 末尾ノードを更新する\n    end\n    @size -= 1 # キューの長さを更新\n\n    val\n  end\n\n  ### キュー先頭から取り出す ###\n  def pop_first\n    pop(true)\n  end\n\n  ### キュー先頭から取り出す ###\n  def pop_last\n    pop(false)\n  end\n\n  ### 先頭要素にアクセス ###\n  def peek_first\n    raise IndexError, '両端キューは空です' if is_empty?\n\n    @front.val\n  end\n\n  ### キュー末尾要素を参照 ###\n  def peek_last\n    raise IndexError, '両端キューは空です' if is_empty?\n\n    @rear.val\n  end\n\n  ### 表示用の配列を返す ###\n  def to_array\n    node = @front\n    res = Array.new(size, 0)\n    for i in 0...size\n      res[i] = node.val\n      node = node.next\n    end\n    res\n  end\nend\n
","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#2","level":3,"title":"2.   配列に基づく実装","text":"

次の図に示すように、配列によるキュー実装と同様に、循環配列を使って両端キューを実装することもできます。

ArrayDequepush_last()push_first()pop_last()pop_first()

図 5-9   配列による両端キューのエンキューとデキュー

キュー実装を土台として、「先頭へのエンキュー」と「末尾からのデキュー」のメソッドを追加するだけで済みます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_deque.py
class ArrayDeque:\n    \"\"\"循環配列ベースの両端キュー\"\"\"\n\n    def __init__(self, capacity: int):\n        \"\"\"コンストラクタ\"\"\"\n        self._nums: list[int] = [0] * capacity\n        self._front: int = 0\n        self._size: int = 0\n\n    def capacity(self) -> int:\n        \"\"\"両端キューの容量を取得\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"両端キューの長さを取得\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"両端キューが空かどうかを判定\"\"\"\n        return self._size == 0\n\n    def index(self, i: int) -> int:\n        \"\"\"循環配列のインデックスを計算\"\"\"\n        # 剰余演算により配列の先頭と末尾をつなげる\n        # i が配列の末尾を越えたら先頭に戻る\n        # i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + self.capacity()) % self.capacity()\n\n    def push_first(self, num: int):\n        \"\"\"キュー先頭にエンキュー\"\"\"\n        if self._size == self.capacity():\n            print(\"両端キューがいっぱいです\")\n            return\n        # 先頭ポインタを左に 1 つ移動する\n        # 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        self._front = self.index(self._front - 1)\n        # num をキュー先頭に追加\n        self._nums[self._front] = num\n        self._size += 1\n\n    def push_last(self, num: int):\n        \"\"\"キュー末尾にエンキュー\"\"\"\n        if self._size == self.capacity():\n            print(\"両端キューがいっぱいです\")\n            return\n        # キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        rear = self.index(self._front + self._size)\n        # num をキュー末尾に追加\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop_first(self) -> int:\n        \"\"\"キュー先頭からデキュー\"\"\"\n        num = self.peek_first()\n        # 先頭ポインタを 1 つ後ろへ進める\n        self._front = self.index(self._front + 1)\n        self._size -= 1\n        return num\n\n    def pop_last(self) -> int:\n        \"\"\"キュー末尾からデキュー\"\"\"\n        num = self.peek_last()\n        self._size -= 1\n        return num\n\n    def peek_first(self) -> int:\n        \"\"\"キュー先頭の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"両端キューが空です\")\n        return self._nums[self._front]\n\n    def peek_last(self) -> int:\n        \"\"\"キュー末尾の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"両端キューが空です\")\n        # 末尾要素のインデックスを計算\n        last = self.index(self._front + self._size - 1)\n        return self._nums[last]\n\n    def to_array(self) -> list[int]:\n        \"\"\"出力用の配列を返す\"\"\"\n        # 有効長の範囲内のリスト要素のみを変換\n        res = []\n        for i in range(self._size):\n            res.append(self._nums[self.index(self._front + i)])\n        return res\n
array_deque.cpp
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n  private:\n    vector<int> nums; // 両端キューの要素を格納する配列\n    int front;        // 先頭ポインタ。先頭要素を指す\n    int queSize;      // 両端キューの長さ\n\n  public:\n    /* コンストラクタ */\n    ArrayDeque(int capacity) {\n        nums.resize(capacity);\n        front = queSize = 0;\n    }\n\n    /* 両端キューの容量を取得 */\n    int capacity() {\n        return nums.size();\n    }\n\n    /* 両端キューの長さを取得 */\n    int size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* 循環配列のインデックスを計算 */\n    int index(int i) {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + capacity()) % capacity();\n    }\n\n    /* キュー先頭にエンキュー */\n    void pushFirst(int num) {\n        if (queSize == capacity()) {\n            cout << \"両端キューがいっぱいです\" << endl;\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        front = index(front - 1);\n        // num をキュー先頭に追加\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* キュー末尾にエンキュー */\n    void pushLast(int num) {\n        if (queSize == capacity()) {\n            cout << \"両端キューがいっぱいです\" << endl;\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        int rear = index(front + queSize);\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* キュー先頭からデキュー */\n    int popFirst() {\n        int num = peekFirst();\n        // 先頭ポインタを 1 つ後ろへ進める\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* キュー末尾からデキュー */\n    int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"両端キューが空です\");\n        return nums[front];\n    }\n\n    /* キュー末尾の要素にアクセス */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"両端キューが空です\");\n        // 末尾要素のインデックスを計算\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* 出力用の配列を返す */\n    vector<int> toVector() {\n        // 有効長の範囲内のリスト要素のみを変換\n        vector<int> res(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n};\n
array_deque.java
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n    private int[] nums; // 両端キューの要素を格納する配列\n    private int front; // 先頭ポインタ。先頭要素を指す\n    private int queSize; // 両端キューの長さ\n\n    /* コンストラクタ */\n    public ArrayDeque(int capacity) {\n        this.nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* 両端キューの容量を取得 */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* 両端キューの長さを取得 */\n    public int size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* 循環配列のインデックスを計算 */\n    private int index(int i) {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + capacity()) % capacity();\n    }\n\n    /* キュー先頭にエンキュー */\n    public void pushFirst(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"双方向キューは満杯です\");\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        front = index(front - 1);\n        // num をキュー先頭に追加\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* キュー末尾にエンキュー */\n    public void pushLast(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"双方向キューは満杯です\");\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        int rear = index(front + queSize);\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* キュー先頭からデキュー */\n    public int popFirst() {\n        int num = peekFirst();\n        // 先頭ポインタを 1 つ後ろへ進める\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* キュー末尾からデキュー */\n    public int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* キュー末尾の要素にアクセス */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        // 末尾要素のインデックスを計算\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* 出力用の配列を返す */\n    public int[] toArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.cs
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n    int[] nums;  // 両端キューの要素を格納する配列\n    int front;   // 先頭ポインタ。先頭要素を指す\n    int queSize; // 両端キューの長さ\n\n    /* コンストラクタ */\n    public ArrayDeque(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* 両端キューの容量を取得 */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* 両端キューの長さを取得 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* 循環配列のインデックスを計算 */\n    int Index(int i) {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + Capacity()) % Capacity();\n    }\n\n    /* キュー先頭にエンキュー */\n    public void PushFirst(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"両端キューは満杯です\");\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        front = Index(front - 1);\n        // num をキュー先頭に追加\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* キュー末尾にエンキュー */\n    public void PushLast(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"両端キューは満杯です\");\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        int rear = Index(front + queSize);\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* キュー先頭からデキュー */\n    public int PopFirst() {\n        int num = PeekFirst();\n        // 先頭ポインタを 1 つ後ろへ進める\n        front = Index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* キュー末尾からデキュー */\n    public int PopLast() {\n        int num = PeekLast();\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int PeekFirst() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        return nums[front];\n    }\n\n    /* キュー末尾の要素にアクセス */\n    public int PeekLast() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        // 末尾要素のインデックスを計算\n        int last = Index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* 出力用の配列を返す */\n    public int[] ToArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[Index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.go
/* 循環配列ベースの両端キュー */\ntype arrayDeque struct {\n    nums        []int // 両端キューの要素を格納する配列\n    front       int   // 先頭ポインタ。先頭要素を指す\n    queSize     int   // 両端キューの長さ\n    queCapacity int   // キュー容量(格納できる要素数の上限)\n}\n\n/* キューを初期化 */\nfunc newArrayDeque(queCapacity int) *arrayDeque {\n    return &arrayDeque{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* 両端キューの長さを取得 */\nfunc (q *arrayDeque) size() int {\n    return q.queSize\n}\n\n/* 両端キューが空かどうかを判定 */\nfunc (q *arrayDeque) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* 循環配列のインデックスを計算 */\nfunc (q *arrayDeque) index(i int) int {\n    // 剰余演算により配列の先頭と末尾をつなげる\n    // i が配列の末尾を越えたら先頭に戻る\n    // i が配列の先頭を越えて前に出たら末尾に戻る\n    return (i + q.queCapacity) % q.queCapacity\n}\n\n/* キュー先頭にエンキュー */\nfunc (q *arrayDeque) pushFirst(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"両端キューは満杯です\")\n        return\n    }\n    // 先頭ポインタを左に 1 つ移動する\n    // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n    q.front = q.index(q.front - 1)\n    // num をキュー先頭に追加\n    q.nums[q.front] = num\n    q.queSize++\n}\n\n/* キュー末尾にエンキュー */\nfunc (q *arrayDeque) pushLast(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"両端キューは満杯です\")\n        return\n    }\n    // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n    rear := q.index(q.front + q.queSize)\n    // num をキュー末尾に追加\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* キュー先頭からデキュー */\nfunc (q *arrayDeque) popFirst() any {\n    num := q.peekFirst()\n    if num == nil {\n        return nil\n    }\n    // 先頭ポインタを 1 つ後ろへ進める\n    q.front = q.index(q.front + 1)\n    q.queSize--\n    return num\n}\n\n/* キュー末尾からデキュー */\nfunc (q *arrayDeque) popLast() any {\n    num := q.peekLast()\n    if num == nil {\n        return nil\n    }\n    q.queSize--\n    return num\n}\n\n/* キュー先頭の要素にアクセス */\nfunc (q *arrayDeque) peekFirst() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* キュー末尾の要素にアクセス */\nfunc (q *arrayDeque) peekLast() any {\n    if q.isEmpty() {\n        return nil\n    }\n    // 末尾要素のインデックスを計算\n    last := q.index(q.front + q.queSize - 1)\n    return q.nums[last]\n}\n\n/* 表示用に Slice を取得 */\nfunc (q *arrayDeque) toSlice() []int {\n    // 有効長の範囲内のリスト要素のみを変換\n    res := make([]int, q.queSize)\n    for i, j := 0, q.front; i < q.queSize; i++ {\n        res[i] = q.nums[q.index(j)]\n        j++\n    }\n    return res\n}\n
array_deque.swift
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n    private var nums: [Int] // 両端キューの要素を格納する配列\n    private var front: Int // 先頭ポインタ。先頭要素を指す\n    private var _size: Int // 両端キューの長さ\n\n    /* コンストラクタ */\n    init(capacity: Int) {\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* 両端キューの容量を取得 */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* 両端キューの長さを取得 */\n    func size() -> Int {\n        _size\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* 循環配列のインデックスを計算 */\n    private func index(i: Int) -> Int {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        (i + capacity()) % capacity()\n    }\n\n    /* キュー先頭にエンキュー */\n    func pushFirst(num: Int) {\n        if size() == capacity() {\n            print(\"両端キューがいっぱいです\")\n            return\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        front = index(i: front - 1)\n        // num をキュー先頭に追加\n        nums[front] = num\n        _size += 1\n    }\n\n    /* キュー末尾にエンキュー */\n    func pushLast(num: Int) {\n        if size() == capacity() {\n            print(\"両端キューがいっぱいです\")\n            return\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        let rear = index(i: front + size())\n        // num をキュー末尾に追加\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* キュー先頭からデキュー */\n    func popFirst() -> Int {\n        let num = peekFirst()\n        // 先頭ポインタを 1 つ後ろへ進める\n        front = index(i: front + 1)\n        _size -= 1\n        return num\n    }\n\n    /* キュー末尾からデキュー */\n    func popLast() -> Int {\n        let num = peekLast()\n        _size -= 1\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"両端キューが空です\")\n        }\n        return nums[front]\n    }\n\n    /* キュー末尾の要素にアクセス */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"両端キューが空です\")\n        }\n        // 末尾要素のインデックスを計算\n        let last = index(i: front + size() - 1)\n        return nums[last]\n    }\n\n    /* 出力用の配列を返す */\n    func toArray() -> [Int] {\n        // 有効長の範囲内のリスト要素のみを変換\n        (front ..< front + size()).map { nums[index(i: $0)] }\n    }\n}\n
array_deque.js
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n    #nums; // 両端キューの要素を格納する配列\n    #front; // 先頭ポインタ。先頭要素を指す\n    #queSize; // 両端キューの長さ\n\n    /* コンストラクタ */\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n        this.#front = 0;\n        this.#queSize = 0;\n    }\n\n    /* 両端キューの容量を取得 */\n    capacity() {\n        return this.#nums.length;\n    }\n\n    /* 両端キューの長さを取得 */\n    size() {\n        return this.#queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* 循環配列のインデックスを計算 */\n    index(i) {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* キュー先頭にエンキュー */\n    pushFirst(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('両端キューがいっぱいです');\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        this.#front = this.index(this.#front - 1);\n        // num をキュー先頭に追加\n        this.#nums[this.#front] = num;\n        this.#queSize++;\n    }\n\n    /* キュー末尾にエンキュー */\n    pushLast(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('両端キューがいっぱいです');\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        const rear = this.index(this.#front + this.#queSize);\n        // num をキュー末尾に追加\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* キュー先頭からデキュー */\n    popFirst() {\n        const num = this.peekFirst();\n        // 先頭ポインタを 1 つ後ろへ進める\n        this.#front = this.index(this.#front + 1);\n        this.#queSize--;\n        return num;\n    }\n\n    /* キュー末尾からデキュー */\n    popLast() {\n        const num = this.peekLast();\n        this.#queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peekFirst() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.#nums[this.#front];\n    }\n\n    /* キュー末尾の要素にアクセス */\n    peekLast() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // 末尾要素のインデックスを計算\n        const last = this.index(this.#front + this.#queSize - 1);\n        return this.#nums[last];\n    }\n\n    /* 出力用の配列を返す */\n    toArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        const res = [];\n        for (let i = 0, j = this.#front; i < this.#queSize; i++, j++) {\n            res[i] = this.#nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.ts
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n    private nums: number[]; // 両端キューの要素を格納する配列\n    private front: number; // 先頭ポインタ。先頭要素を指す\n    private queSize: number; // 両端キューの長さ\n\n    /* コンストラクタ */\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = 0;\n        this.queSize = 0;\n    }\n\n    /* 両端キューの容量を取得 */\n    capacity(): number {\n        return this.nums.length;\n    }\n\n    /* 両端キューの長さを取得 */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* 循環配列のインデックスを計算 */\n    index(i: number): number {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* キュー先頭にエンキュー */\n    pushFirst(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('両端キューは満杯です');\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        this.front = this.index(this.front - 1);\n        // num をキュー先頭に追加\n        this.nums[this.front] = num;\n        this.queSize++;\n    }\n\n    /* キュー末尾にエンキュー */\n    pushLast(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('両端キューは満杯です');\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        const rear: number = this.index(this.front + this.queSize);\n        // num をキュー末尾に追加\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* キュー先頭からデキュー */\n    popFirst(): number {\n        const num: number = this.peekFirst();\n        // 先頭ポインタを 1 つ後ろへ進める\n        this.front = this.index(this.front + 1);\n        this.queSize--;\n        return num;\n    }\n\n    /* キュー末尾からデキュー */\n    popLast(): number {\n        const num: number = this.peekLast();\n        this.queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peekFirst(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.nums[this.front];\n    }\n\n    /* キュー末尾の要素にアクセス */\n    peekLast(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // 末尾要素のインデックスを計算\n        const last = this.index(this.front + this.queSize - 1);\n        return this.nums[last];\n    }\n\n    /* 出力用の配列を返す */\n    toArray(): number[] {\n        // 有効長の範囲内のリスト要素のみを変換\n        const res: number[] = [];\n        for (let i = 0, j = this.front; i < this.queSize; i++, j++) {\n            res[i] = this.nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.dart
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n  late List<int> _nums; // 両端キューの要素を格納する配列\n  late int _front; // 先頭ポインタ。先頭要素を指す\n  late int _queSize; // 両端キューの長さ\n\n  /* コンストラクタ */\n  ArrayDeque(int capacity) {\n    this._nums = List.filled(capacity, 0);\n    this._front = this._queSize = 0;\n  }\n\n  /* 両端キューの容量を取得 */\n  int capacity() {\n    return _nums.length;\n  }\n\n  /* 両端キューの長さを取得 */\n  int size() {\n    return _queSize;\n  }\n\n  /* 両端キューが空かどうかを判定 */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* 循環配列のインデックスを計算 */\n  int index(int i) {\n    // 剰余演算により配列の先頭と末尾をつなげる\n    // i が配列の末尾を越えたら先頭に戻る\n    // i が配列の先頭を越えて前に出たら末尾に戻る\n    return (i + capacity()) % capacity();\n  }\n\n  /* キュー先頭にエンキュー */\n  void pushFirst(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"両端キューがいっぱいです\");\n    }\n    // 先頭ポインタを左に 1 つ移動する\n    // 剰余演算により _front が配列の先頭を越えたあと末尾に戻るようにする\n    _front = index(_front - 1);\n    // _num をキューの先頭に追加\n    _nums[_front] = _num;\n    _queSize++;\n  }\n\n  /* キュー末尾にエンキュー */\n  void pushLast(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"両端キューがいっぱいです\");\n    }\n    // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n    int rear = index(_front + _queSize);\n    // _num をキュー末尾に追加\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* キュー先頭からデキュー */\n  int popFirst() {\n    int _num = peekFirst();\n    // 先頭ポインタを右に 1 つ移動する\n    _front = index(_front + 1);\n    _queSize--;\n    return _num;\n  }\n\n  /* キュー末尾からデキュー */\n  int popLast() {\n    int _num = peekLast();\n    _queSize--;\n    return _num;\n  }\n\n  /* キュー先頭の要素にアクセス */\n  int peekFirst() {\n    if (isEmpty()) {\n      throw Exception(\"両端キューが空です\");\n    }\n    return _nums[_front];\n  }\n\n  /* キュー末尾の要素にアクセス */\n  int peekLast() {\n    if (isEmpty()) {\n      throw Exception(\"両端キューが空です\");\n    }\n    // 末尾要素のインデックスを計算\n    int last = index(_front + _queSize - 1);\n    return _nums[last];\n  }\n\n  /* 出力用の配列を返す */\n  List<int> toArray() {\n    // 有効長の範囲内のリスト要素のみを変換\n    List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[index(j)];\n    }\n    return res;\n  }\n}\n
array_deque.rs
/* 循環配列ベースの両端キュー */\nstruct ArrayDeque<T> {\n    nums: Vec<T>,    // 両端キューの要素を格納する配列\n    front: usize,    // 先頭ポインタ。先頭要素を指す\n    que_size: usize, // 両端キューの長さ\n}\n\nimpl<T: Copy + Default> ArrayDeque<T> {\n    /* コンストラクタ */\n    pub fn new(capacity: usize) -> Self {\n        Self {\n            nums: vec![T::default(); capacity],\n            front: 0,\n            que_size: 0,\n        }\n    }\n\n    /* 両端キューの容量を取得 */\n    pub fn capacity(&self) -> usize {\n        self.nums.len()\n    }\n\n    /* 両端キューの長さを取得 */\n    pub fn size(&self) -> usize {\n        self.que_size\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    pub fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* 循環配列のインデックスを計算 */\n    fn index(&self, i: i32) -> usize {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        ((i + self.capacity() as i32) % self.capacity() as i32) as usize\n    }\n\n    /* キュー先頭にエンキュー */\n    pub fn push_first(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"両端キューがいっぱいです\");\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        self.front = self.index(self.front as i32 - 1);\n        // num をキュー先頭に追加\n        self.nums[self.front] = num;\n        self.que_size += 1;\n    }\n\n    /* キュー末尾にエンキュー */\n    pub fn push_last(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"両端キューがいっぱいです\");\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        let rear = self.index(self.front as i32 + self.que_size as i32);\n        // num をキュー末尾に追加\n        self.nums[rear] = num;\n        self.que_size += 1;\n    }\n\n    /* キュー先頭からデキュー */\n    fn pop_first(&mut self) -> T {\n        let num = self.peek_first();\n        // 先頭ポインタを 1 つ後ろへ進める\n        self.front = self.index(self.front as i32 + 1);\n        self.que_size -= 1;\n        num\n    }\n\n    /* キュー末尾からデキュー */\n    fn pop_last(&mut self) -> T {\n        let num = self.peek_last();\n        self.que_size -= 1;\n        num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fn peek_first(&self) -> T {\n        if self.is_empty() {\n            panic!(\"両端キューが空です\")\n        };\n        self.nums[self.front]\n    }\n\n    /* キュー末尾の要素にアクセス */\n    fn peek_last(&self) -> T {\n        if self.is_empty() {\n            panic!(\"両端キューが空です\")\n        };\n        // 末尾要素のインデックスを計算\n        let last = self.index(self.front as i32 + self.que_size as i32 - 1);\n        self.nums[last]\n    }\n\n    /* 出力用の配列を返す */\n    fn to_array(&self) -> Vec<T> {\n        // 有効長の範囲内のリスト要素のみを変換\n        let mut res = vec![T::default(); self.que_size];\n        let mut j = self.front;\n        for i in 0..self.que_size {\n            res[i] = self.nums[self.index(j as i32)];\n            j += 1;\n        }\n        res\n    }\n}\n
array_deque.c
/* 循環配列ベースの両端キュー */\ntypedef struct {\n    int *nums;       // キュー要素を格納する配列\n    int front;       // 先頭ポインタ。先頭要素を指す\n    int queSize;     // 末尾ポインタ。キューの末尾 + 1 を指す\n    int queCapacity; // キューの容量\n} ArrayDeque;\n\n/* コンストラクタ */\nArrayDeque *newArrayDeque(int capacity) {\n    ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque));\n    // 配列を初期化\n    deque->queCapacity = capacity;\n    deque->nums = (int *)malloc(sizeof(int) * deque->queCapacity);\n    deque->front = deque->queSize = 0;\n    return deque;\n}\n\n/* デストラクタ */\nvoid delArrayDeque(ArrayDeque *deque) {\n    free(deque->nums);\n    free(deque);\n}\n\n/* 両端キューの容量を取得 */\nint capacity(ArrayDeque *deque) {\n    return deque->queCapacity;\n}\n\n/* 両端キューの長さを取得 */\nint size(ArrayDeque *deque) {\n    return deque->queSize;\n}\n\n/* 両端キューが空かどうかを判定 */\nbool empty(ArrayDeque *deque) {\n    return deque->queSize == 0;\n}\n\n/* 循環配列のインデックスを計算 */\nint dequeIndex(ArrayDeque *deque, int i) {\n    // 剰余演算により配列の先頭と末尾をつなげる\n    // i が配列の末尾を越えたら先頭に戻る\n    // i が配列の先頭を越えたら末尾に戻る\n    return ((i + capacity(deque)) % capacity(deque));\n}\n\n/* キュー先頭にエンキュー */\nvoid pushFirst(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"両端キューがいっぱいです\\r\\n\");\n        return;\n    }\n    // 先頭ポインタを左に 1 つ移動する\n    // 剰余演算により front が配列の先頭を越えたあと末尾に戻るようにする\n    deque->front = dequeIndex(deque, deque->front - 1);\n    // num をキューの先頭に追加\n    deque->nums[deque->front] = num;\n    deque->queSize++;\n}\n\n/* キュー末尾にエンキュー */\nvoid pushLast(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"両端キューがいっぱいです\\r\\n\");\n        return;\n    }\n    // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n    int rear = dequeIndex(deque, deque->front + deque->queSize);\n    // num をキュー末尾に追加\n    deque->nums[rear] = num;\n    deque->queSize++;\n}\n\n/* キュー先頭の要素にアクセス */\nint peekFirst(ArrayDeque *deque) {\n    // アクセス例外:双方向キューが空です\n    assert(empty(deque) == 0);\n    return deque->nums[deque->front];\n}\n\n/* キュー末尾の要素にアクセス */\nint peekLast(ArrayDeque *deque) {\n    // アクセス例外:双方向キューが空です\n    assert(empty(deque) == 0);\n    int last = dequeIndex(deque, deque->front + deque->queSize - 1);\n    return deque->nums[last];\n}\n\n/* キュー先頭からデキュー */\nint popFirst(ArrayDeque *deque) {\n    int num = peekFirst(deque);\n    // 先頭ポインタを 1 つ後ろへ進める\n    deque->front = dequeIndex(deque, deque->front + 1);\n    deque->queSize--;\n    return num;\n}\n\n/* キュー末尾からデキュー */\nint popLast(ArrayDeque *deque) {\n    int num = peekLast(deque);\n    deque->queSize--;\n    return num;\n}\n\n/* 出力用の配列を返す */\nint *toArray(ArrayDeque *deque, int *queSize) {\n    *queSize = deque->queSize;\n    int *res = (int *)calloc(deque->queSize, sizeof(int));\n    int j = deque->front;\n    for (int i = 0; i < deque->queSize; i++) {\n        res[i] = deque->nums[j % deque->queCapacity];\n        j++;\n    }\n    return res;\n}\n
array_deque.kt
/* コンストラクタ */\nclass ArrayDeque(capacity: Int) {\n    private var nums: IntArray = IntArray(capacity) // 両端キューの要素を格納する配列\n    private var front: Int = 0 // 先頭ポインタ。先頭要素を指す\n    private var queSize: Int = 0 // 両端キューの長さ\n\n    /* 両端キューの容量を取得 */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* 両端キューの長さを取得 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* 循環配列のインデックスを計算 */\n    private fun index(i: Int): Int {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + capacity()) % capacity()\n    }\n\n    /* キュー先頭にエンキュー */\n    fun pushFirst(num: Int) {\n        if (queSize == capacity()) {\n            println(\"双方向キューは満杯です\")\n            return\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        front = index(front - 1)\n        // num をキュー先頭に追加\n        nums[front] = num\n        queSize++\n    }\n\n    /* キュー末尾にエンキュー */\n    fun pushLast(num: Int) {\n        if (queSize == capacity()) {\n            println(\"双方向キューは満杯です\")\n            return\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        val rear = index(front + queSize)\n        // num をキュー末尾に追加\n        nums[rear] = num\n        queSize++\n    }\n\n    /* キュー先頭からデキュー */\n    fun popFirst(): Int {\n        val num = peekFirst()\n        // 先頭ポインタを 1 つ後ろへ進める\n        front = index(front + 1)\n        queSize--\n        return num\n    }\n\n    /* キュー末尾からデキュー */\n    fun popLast(): Int {\n        val num = peekLast()\n        queSize--\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* キュー末尾の要素にアクセス */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // 末尾要素のインデックスを計算\n        val last = index(front + queSize - 1)\n        return nums[last]\n    }\n\n    /* 出力用の配列を返す */\n    fun toArray(): IntArray {\n        // 有効長の範囲内のリスト要素のみを変換\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[index(j)]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
array_deque.rb
### 循環配列で実装した両端キュー ###\nclass ArrayDeque\n  ### 両端キューの長さを取得 ###\n  attr_reader :size\n\n  ### コンストラクタ ###\n  def initialize(capacity)\n    @nums = Array.new(capacity, 0)\n    @front = 0\n    @size = 0\n  end\n\n  ### 両端キューの容量を取得 ###\n  def capacity\n    @nums.length\n  end\n\n  ### 両端キューが空か判定 ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### キュー先頭に追加 ###\n  def push_first(num)\n    if size == capacity\n      puts '両端キューがいっぱいです'\n      return\n    end\n\n    # 先頭ポインタを左に 1 つ移動する\n    # 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n    @front = index(@front - 1)\n    # num をキュー先頭に追加\n    @nums[@front] = num\n    @size += 1\n  end\n\n  ### キュー末尾に追加 ###\n  def push_last(num)\n    if size == capacity\n      puts '両端キューがいっぱいです'\n      return\n    end\n\n    # キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n    rear = index(@front + size)\n    # num をキュー末尾に追加\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### キュー先頭から取り出す ###\n  def pop_first\n    num = peek_first\n    # 先頭ポインタを 1 つ後ろへ進める\n    @front = index(@front + 1)\n    @size -= 1\n    num\n  end\n\n  ### キューの末尾から取り出す ###\n  def pop_last\n    num = peek_last\n    @size -= 1\n    num\n  end\n\n  ### 先頭要素にアクセス ###\n  def peek_first\n    raise IndexError, '両端キューは空です' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### キュー末尾要素を参照 ###\n  def peek_last\n    raise IndexError, '両端キューは空です' if is_empty?\n\n    # 末尾要素のインデックスを計算\n    last = index(@front + size - 1)\n    @nums[last]\n  end\n\n  ### 表示用の配列を返す ###\n  def to_array\n    # 有効長の範囲内のリスト要素のみを変換\n    res = []\n    for i in 0...size\n      res << @nums[index(@front + i)]\n    end\n    res\n  end\n\n  private\n\n  ### 循環配列のインデックスを計算 ###\n  def index(i)\n    # 剰余演算により配列の先頭と末尾をつなげる\n    # i が配列の末尾を越えたら先頭に戻る\n    # i が配列の先頭を越えて前に出たら末尾に戻る\n    (i + capacity) % capacity\n  end\nend\n
","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#533","level":2,"title":"5.3.3   両端キューの応用","text":"

両端キューはスタックとキューの両方の論理を備えているため、これら 2 つのすべての応用場面を実現でき、さらに高い自由度を提供します。

私たちが知っているように、ソフトウェアの「元に戻す」機能は通常スタックを使って実装されます。システムは変更操作を毎回スタックに push し、その後 pop によって取り消しを実現します。しかし、システム資源の制約を考慮すると、通常ソフトウェアは取り消し可能な手数を制限します(たとえば \\(50\\) 手まで保存可能)。スタックの長さが \\(50\\) を超えると、ソフトウェアはスタックの底部(先頭)で削除操作を行う必要があります。しかしスタックではこの機能を実現できないため、この場合はスタックの代わりに両端キューを使用する必要があります。なお、「元に戻す」の中核ロジック自体は依然としてスタックの後入れ先出し原則に従っており、両端キューは追加のロジックをより柔軟に実装できるだけです。

","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   キュー","text":"

キュー(queue)は、先入れ先出しの規則に従う線形データ構造です。名前のとおり、キューは順番待ちの現象を模したもので、新しく来た人は絶えずキュー末尾に加わり、キュー先頭にいる人から順に離れていきます。

下図のように、キューの先頭を「キュー先頭」、末尾を「キュー末尾」と呼びます。要素をキュー末尾に加える操作を「エンキュー」、キュー先頭の要素を削除する操作を「デキュー」と呼びます。

図 5-4   キューの先入れ先出し規則

","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521","level":2,"title":"5.2.1   キューの基本操作","text":"

キューの基本操作を以下の表に示します。なお、メソッド名はプログラミング言語によって異なる場合があります。ここではスタックと同じ命名を採用します。

表 5-2   キュー操作の効率

メソッド名 説明 時間計算量 push() 要素をエンキューし、キュー末尾に追加する \\(O(1)\\) pop() キュー先頭の要素をデキューする \\(O(1)\\) peek() キュー先頭の要素にアクセスする \\(O(1)\\)

プログラミング言語に用意された既存のキュークラスをそのまま利用できます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby queue.py
from collections import deque\n\n# キューを初期化\n# Python では、通常は双方向キュークラス deque をキューとして使用する\n# queue.Queue() は純粋なキュークラスだが、やや使いにくいため非推奨\nque: deque[int] = deque()\n\n# 要素をエンキュー\nque.append(1)\nque.append(3)\nque.append(2)\nque.append(5)\nque.append(4)\n\n# キュー先頭の要素にアクセス\nfront: int = que[0]\n\n# 要素をデキュー\npop: int = que.popleft()\n\n# キューの長さを取得\nsize: int = len(que)\n\n# キューが空かどうかを判定\nis_empty: bool = len(que) == 0\n
queue.cpp
/* キューを初期化 */\nqueue<int> queue;\n\n/* 要素をエンキュー */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* キュー先頭の要素にアクセス */\nint front = queue.front();\n\n/* 要素をデキュー */\nqueue.pop();\n\n/* キューの長さを取得 */\nint size = queue.size();\n\n/* キューが空かどうかを判定 */\nbool empty = queue.empty();\n
queue.java
/* キューを初期化 */\nQueue<Integer> queue = new LinkedList<>();\n\n/* 要素をエンキュー */\nqueue.offer(1);\nqueue.offer(3);\nqueue.offer(2);\nqueue.offer(5);\nqueue.offer(4);\n\n/* キュー先頭の要素にアクセス */\nint peek = queue.peek();\n\n/* 要素をデキュー */\nint pop = queue.poll();\n\n/* キューの長さを取得 */\nint size = queue.size();\n\n/* キューが空かどうかを判定 */\nboolean isEmpty = queue.isEmpty();\n
queue.cs
/* キューを初期化 */\nQueue<int> queue = new();\n\n/* 要素をエンキュー */\nqueue.Enqueue(1);\nqueue.Enqueue(3);\nqueue.Enqueue(2);\nqueue.Enqueue(5);\nqueue.Enqueue(4);\n\n/* キュー先頭の要素にアクセス */\nint peek = queue.Peek();\n\n/* 要素をデキュー */\nint pop = queue.Dequeue();\n\n/* キューの長さを取得 */\nint size = queue.Count;\n\n/* キューが空かどうかを判定 */\nbool isEmpty = queue.Count == 0;\n
queue_test.go
/* キューを初期化 */\n// Go では、list をキューとして使用する\nqueue := list.New()\n\n/* 要素をエンキュー */\nqueue.PushBack(1)\nqueue.PushBack(3)\nqueue.PushBack(2)\nqueue.PushBack(5)\nqueue.PushBack(4)\n\n/* キュー先頭の要素にアクセス */\npeek := queue.Front()\n\n/* 要素をデキュー */\npop := queue.Front()\nqueue.Remove(pop)\n\n/* キューの長さを取得 */\nsize := queue.Len()\n\n/* キューが空かどうかを判定 */\nisEmpty := queue.Len() == 0\n
queue.swift
/* キューを初期化 */\n// Swift には組み込みのキュークラスがないため、Array をキューとして使える\nvar queue: [Int] = []\n\n/* 要素をエンキュー */\nqueue.append(1)\nqueue.append(3)\nqueue.append(2)\nqueue.append(5)\nqueue.append(4)\n\n/* キュー先頭の要素にアクセス */\nlet peek = queue.first!\n\n/* 要素をデキュー */\n// 配列であるため、removeFirst の計算量は O(n)\nlet pool = queue.removeFirst()\n\n/* キューの長さを取得 */\nlet size = queue.count\n\n/* キューが空かどうかを判定 */\nlet isEmpty = queue.isEmpty\n
queue.js
/* キューを初期化 */\n// JavaScript には組み込みのキューがないため、Array をキューとして使える\nconst queue = [];\n\n/* 要素をエンキュー */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* キュー先頭の要素にアクセス */\nconst peek = queue[0];\n\n/* 要素をデキュー */\n// 基盤は配列であるため、shift() メソッドの時間計算量は O(n)\nconst pop = queue.shift();\n\n/* キューの長さを取得 */\nconst size = queue.length;\n\n/* キューが空かどうかを判定 */\nconst empty = queue.length === 0;\n
queue.ts
/* キューを初期化 */\n// TypeScript には組み込みのキューがないため、Array をキューとして使える\nconst queue: number[] = [];\n\n/* 要素をエンキュー */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* キュー先頭の要素にアクセス */\nconst peek = queue[0];\n\n/* 要素をデキュー */\n// 基盤は配列であるため、shift() メソッドの時間計算量は O(n)\nconst pop = queue.shift();\n\n/* キューの長さを取得 */\nconst size = queue.length;\n\n/* キューが空かどうかを判定 */\nconst empty = queue.length === 0;\n
queue.dart
/* キューを初期化 */\n// Dart では、キュークラス Qeque は双方向キューであり、キューとしても使用できる\nQueue<int> queue = Queue();\n\n/* 要素をエンキュー */\nqueue.add(1);\nqueue.add(3);\nqueue.add(2);\nqueue.add(5);\nqueue.add(4);\n\n/* キュー先頭の要素にアクセス */\nint peek = queue.first;\n\n/* 要素をデキュー */\nint pop = queue.removeFirst();\n\n/* キューの長さを取得 */\nint size = queue.length;\n\n/* キューが空かどうかを判定 */\nbool isEmpty = queue.isEmpty;\n
queue.rs
/* 双方向キューを初期化 */\n// Rust では双方向キューを通常のキューとして使う\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* 要素をエンキュー */\ndeque.push_back(1);\ndeque.push_back(3);\ndeque.push_back(2);\ndeque.push_back(5);\ndeque.push_back(4);\n\n/* キュー先頭の要素にアクセス */\nif let Some(front) = deque.front() {\n}\n\n/* 要素をデキュー */\nif let Some(pop) = deque.pop_front() {\n}\n\n/* キューの長さを取得 */\nlet size = deque.len();\n\n/* キューが空かどうかを判定 */\nlet is_empty = deque.is_empty();\n
queue.c
// C には組み込みのキューがない\n
queue.kt
/* キューを初期化 */\nval queue = LinkedList<Int>()\n\n/* 要素をエンキュー */\nqueue.offer(1)\nqueue.offer(3)\nqueue.offer(2)\nqueue.offer(5)\nqueue.offer(4)\n\n/* キュー先頭の要素にアクセス */\nval peek = queue.peek()\n\n/* 要素をデキュー */\nval pop = queue.poll()\n\n/* キューの長さを取得 */\nval size = queue.size\n\n/* キューが空かどうかを判定 */\nval isEmpty = queue.isEmpty()\n
queue.rb
# キューを初期化\n# Ruby 組み込みのキュー(Thread::Queue) には peek と走査メソッドがないため、Array をキューとして使える\nqueue = []\n\n# 要素をエンキュー\nqueue.push(1)\nqueue.push(3)\nqueue.push(2)\nqueue.push(5)\nqueue.push(4)\n\n# キュー先頭の要素にアクセス\npeek = queue.first\n\n# 要素をデキュー\n# 注意:配列であるため、Array#shift メソッドの時間計算量は O(n)\npop = queue.shift\n\n# キューの長さを取得\nsize = queue.length\n\n# キューが空かどうかを判定\nis_empty = queue.empty?\n
可視化実行

https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%98%9F%E5%88%97%0A%20%20%20%20%23%20%E5%9C%A8%20Python%20%E4%B8%AD%EF%BC%8C%E6%88%91%E4%BB%AC%E4%B8%80%E8%88%AC%E5%B0%86%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%B1%BB%20deque%20%E7%9C%8B%E4%BD%9C%E9%98%9F%E5%88%97%E4%BD%BF%E7%94%A8%0A%20%20%20%20%23%20%E8%99%BD%E7%84%B6%20queue.Queue%28%29%20%E6%98%AF%E7%BA%AF%E6%AD%A3%E7%9A%84%E9%98%9F%E5%88%97%E7%B1%BB%EF%BC%8C%E4%BD%86%E4%B8%8D%E5%A4%AA%E5%A5%BD%E7%94%A8%0A%20%20%20%20que%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20que.append%281%29%0A%20%20%20%20que.append%283%29%0A%20%20%20%20que.append%282%29%0A%20%20%20%20que.append%285%29%0A%20%20%20%20que.append%284%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20que%5B0%5D%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop%20%3D%20que.popleft%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%90%8E%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28que%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28que%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#522","level":2,"title":"5.2.2   キューの実装","text":"

キューを実装するには、一方の端で要素を追加し、もう一方の端で要素を削除できるデータ構造が必要です。連結リストと配列はいずれもこの条件を満たします。

","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#1","level":3,"title":"1.   連結リストに基づく実装","text":"

下図のように、連結リストの「先頭ノード」と「末尾ノード」をそれぞれ「キュー先頭」と「キュー末尾」とみなし、キュー末尾ではノードの追加のみ、キュー先頭ではノードの削除のみを行うようにします。

LinkedListQueuepush()pop()

図 5-5   連結リストでキューを実装したエンキューとデキュー操作

以下は連結リストでキューを実装するコードです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_queue.py
class LinkedListQueue:\n    \"\"\"連結リストベースのキュー\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self._front: ListNode | None = None  # 先頭ノード front\n        self._rear: ListNode | None = None  # 末尾ノード rear\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"キューの長さを取得\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"キューが空かどうかを判定\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"エンキュー\"\"\"\n        # 末尾ノードの後ろに num を追加\n        node = ListNode(num)\n        # キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if self._front is None:\n            self._front = node\n            self._rear = node\n        # キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        else:\n            self._rear.next = node\n            self._rear = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"デキュー\"\"\"\n        num = self.peek()\n        # 先頭ノードを削除\n        self._front = self._front.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"キュー先頭の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"キューが空です\")\n        return self._front.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"表示用にリストへ変換\"\"\"\n        queue = []\n        temp = self._front\n        while temp:\n            queue.append(temp.val)\n            temp = temp.next\n        return queue\n
linkedlist_queue.cpp
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n  private:\n    ListNode *front, *rear; // 先頭ノード front、末尾ノード rear\n    int queSize;\n\n  public:\n    LinkedListQueue() {\n        front = nullptr;\n        rear = nullptr;\n        queSize = 0;\n    }\n\n    ~LinkedListQueue() {\n        // 連結リストを走査してノードを削除し、メモリを解放する\n        freeMemoryLinkedList(front);\n    }\n\n    /* キューの長さを取得 */\n    int size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* エンキュー */\n    void push(int num) {\n        // 末尾ノードの後ろに num を追加\n        ListNode *node = new ListNode(num);\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (front == nullptr) {\n            front = node;\n            rear = node;\n        }\n        // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        else {\n            rear->next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* デキュー */\n    int pop() {\n        int num = peek();\n        // 先頭ノードを削除\n        ListNode *tmp = front;\n        front = front->next;\n        // メモリを解放する\n        delete tmp;\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    int peek() {\n        if (size() == 0)\n            throw out_of_range(\"キューが空です\");\n        return front->val;\n    }\n\n    /* 連結リストを Vector に変換して返す */\n    vector<int> toVector() {\n        ListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_queue.java
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n    private ListNode front, rear; // 先頭ノード front、末尾ノード rear\n    private int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* キューの長さを取得 */\n    public int size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* エンキュー */\n    public void push(int num) {\n        // 末尾ノードの後ろに num を追加\n        ListNode node = new ListNode(num);\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (front == null) {\n            front = node;\n            rear = node;\n        // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        } else {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* デキュー */\n    public int pop() {\n        int num = peek();\n        // 先頭ノードを削除\n        front = front.next;\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.cs
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n    ListNode? front, rear;  // 先頭ノード front、末尾ノード rear\n    int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* キューの長さを取得 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* エンキュー */\n    public void Push(int num) {\n        // 末尾ノードの後ろに num を追加\n        ListNode node = new(num);\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (front == null) {\n            front = node;\n            rear = node;\n            // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        } else if (rear != null) {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* デキュー */\n    public int Pop() {\n        int num = Peek();\n        // 先頭ノードを削除\n        front = front?.next;\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return front!.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    public int[] ToArray() {\n        if (front == null)\n            return [];\n\n        ListNode? node = front;\n        int[] res = new int[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.go
/* 連結リストベースのキュー */\ntype linkedListQueue struct {\n    // 組み込みパッケージ list でキューを実装する\n    data *list.List\n}\n\n/* キューを初期化 */\nfunc newLinkedListQueue() *linkedListQueue {\n    return &linkedListQueue{\n        data: list.New(),\n    }\n}\n\n/* エンキュー */\nfunc (s *linkedListQueue) push(value any) {\n    s.data.PushBack(value)\n}\n\n/* デキュー */\nfunc (s *linkedListQueue) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* キュー先頭の要素にアクセス */\nfunc (s *linkedListQueue) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* キューの長さを取得 */\nfunc (s *linkedListQueue) size() int {\n    return s.data.Len()\n}\n\n/* キューが空かどうかを判定 */\nfunc (s *linkedListQueue) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* 表示用に List を取得 */\nfunc (s *linkedListQueue) toList() *list.List {\n    return s.data\n}\n
linkedlist_queue.swift
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n    private var front: ListNode? // 先頭ノード\n    private var rear: ListNode? // 末尾ノード\n    private var _size: Int\n\n    init() {\n        _size = 0\n    }\n\n    /* キューの長さを取得 */\n    func size() -> Int {\n        _size\n    }\n\n    /* キューが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* エンキュー */\n    func push(num: Int) {\n        // 末尾ノードの後ろに num を追加\n        let node = ListNode(x: num)\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if front == nil {\n            front = node\n            rear = node\n        }\n        // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        else {\n            rear?.next = node\n            rear = node\n        }\n        _size += 1\n    }\n\n    /* デキュー */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // 先頭ノードを削除\n        front = front?.next\n        _size -= 1\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"キューが空です\")\n        }\n        return front!.val\n    }\n\n    /* 連結リストを Array に変換して返す */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_queue.js
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n    #front; // 先頭ノード #front\n    #rear; // 末尾ノード #rear\n    #queSize = 0;\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n    }\n\n    /* キューの長さを取得 */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* エンキュー */\n    push(num) {\n        // 末尾ノードの後ろに num を追加\n        const node = new ListNode(num);\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (!this.#front) {\n            this.#front = node;\n            this.#rear = node;\n            // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        } else {\n            this.#rear.next = node;\n            this.#rear = node;\n        }\n        this.#queSize++;\n    }\n\n    /* デキュー */\n    pop() {\n        const num = this.peek();\n        // 先頭ノードを削除\n        this.#front = this.#front.next;\n        this.#queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peek() {\n        if (this.size === 0) throw new Error('キューが空');\n        return this.#front.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    toArray() {\n        let node = this.#front;\n        const res = new Array(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.ts
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n    private front: ListNode | null; // 先頭ノード front\n    private rear: ListNode | null; // 末尾ノード rear\n    private queSize: number = 0;\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n    }\n\n    /* キューの長さを取得 */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* エンキュー */\n    push(num: number): void {\n        // 末尾ノードの後ろに num を追加\n        const node = new ListNode(num);\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (!this.front) {\n            this.front = node;\n            this.rear = node;\n            // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        } else {\n            this.rear!.next = node;\n            this.rear = node;\n        }\n        this.queSize++;\n    }\n\n    /* デキュー */\n    pop(): number {\n        const num = this.peek();\n        if (!this.front) throw new Error('キューが空です');\n        // 先頭ノードを削除\n        this.front = this.front.next;\n        this.queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peek(): number {\n        if (this.size === 0) throw new Error('キューが空です');\n        return this.front!.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    toArray(): number[] {\n        let node = this.front;\n        const res = new Array<number>(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.dart
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n  ListNode? _front; // 先頭ノード _front\n  ListNode? _rear; // 末尾ノード _rear\n  int _queSize = 0; // キューの長さ\n\n  LinkedListQueue() {\n    _front = null;\n    _rear = null;\n  }\n\n  /* キューの長さを取得 */\n  int size() {\n    return _queSize;\n  }\n\n  /* キューが空かどうかを判定 */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* エンキュー */\n  void push(int _num) {\n    // 末尾ノードの後ろに _num を追加\n    final node = ListNode(_num);\n    // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n    if (_front == null) {\n      _front = node;\n      _rear = node;\n    } else {\n      // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n      _rear!.next = node;\n      _rear = node;\n    }\n    _queSize++;\n  }\n\n  /* デキュー */\n  int pop() {\n    final int _num = peek();\n    // 先頭ノードを削除\n    _front = _front!.next;\n    _queSize--;\n    return _num;\n  }\n\n  /* キュー先頭の要素にアクセス */\n  int peek() {\n    if (_queSize == 0) {\n      throw Exception('キューが空です');\n    }\n    return _front!.val;\n  }\n\n  /* 連結リストを Array に変換して返す */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> queue = [];\n    while (node != null) {\n      queue.add(node.val);\n      node = node.next;\n    }\n    return queue;\n  }\n}\n
linkedlist_queue.rs
/* 連結リストベースのキュー */\n#[allow(dead_code)]\npub struct LinkedListQueue<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // 先頭ノード front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // 末尾ノード rear\n    que_size: usize,                         // キューの長さ\n}\n\nimpl<T: Copy> LinkedListQueue<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* キューの長さを取得 */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* キューが空かどうかを判定 */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* エンキュー */\n    pub fn push(&mut self, num: T) {\n        // 末尾ノードの後ろに num を追加\n        let new_rear = ListNode::new(num);\n        match self.rear.take() {\n            // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n            Some(old_rear) => {\n                old_rear.borrow_mut().next = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n            // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n            None => {\n                self.front = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n        }\n        self.que_size += 1;\n    }\n\n    /* デキュー */\n    pub fn pop(&mut self) -> Option<T> {\n        self.front.take().map(|old_front| {\n            match old_front.borrow_mut().next.take() {\n                Some(new_front) => {\n                    self.front = Some(new_front);\n                }\n                None => {\n                    self.rear.take();\n                }\n            }\n            self.que_size -= 1;\n            old_front.borrow().val\n        })\n    }\n\n    /* キュー先頭の要素にアクセス */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* 連結リストを Array に変換して返す */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n\n        res\n    }\n}\n
linkedlist_queue.c
/* 連結リストベースのキュー */\ntypedef struct {\n    ListNode *front, *rear;\n    int queSize;\n} LinkedListQueue;\n\n/* コンストラクタ */\nLinkedListQueue *newLinkedListQueue() {\n    LinkedListQueue *queue = (LinkedListQueue *)malloc(sizeof(LinkedListQueue));\n    queue->front = NULL;\n    queue->rear = NULL;\n    queue->queSize = 0;\n    return queue;\n}\n\n/* デストラクタ */\nvoid delLinkedListQueue(LinkedListQueue *queue) {\n    // すべてのノードを解放\n    while (queue->front != NULL) {\n        ListNode *tmp = queue->front;\n        queue->front = queue->front->next;\n        free(tmp);\n    }\n    // queue 構造体を解放する\n    free(queue);\n}\n\n/* キューの長さを取得 */\nint size(LinkedListQueue *queue) {\n    return queue->queSize;\n}\n\n/* キューが空かどうかを判定 */\nbool empty(LinkedListQueue *queue) {\n    return (size(queue) == 0);\n}\n\n/* エンキュー */\nvoid push(LinkedListQueue *queue, int num) {\n    // 末尾ノードに node を追加\n    ListNode *node = newListNode(num);\n    // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n    if (queue->front == NULL) {\n        queue->front = node;\n        queue->rear = node;\n    }\n    // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n    else {\n        queue->rear->next = node;\n        queue->rear = node;\n    }\n    queue->queSize++;\n}\n\n/* キュー先頭の要素にアクセス */\nint peek(LinkedListQueue *queue) {\n    assert(size(queue) && queue->front);\n    return queue->front->val;\n}\n\n/* デキュー */\nint pop(LinkedListQueue *queue) {\n    int num = peek(queue);\n    ListNode *tmp = queue->front;\n    queue->front = queue->front->next;\n    free(tmp);\n    queue->queSize--;\n    return num;\n}\n\n/* キューを出力する */\nvoid printLinkedListQueue(LinkedListQueue *queue) {\n    int *arr = malloc(sizeof(int) * queue->queSize);\n    // 連結リスト内のデータを配列にコピー\n    int i;\n    ListNode *node;\n    for (i = 0, node = queue->front; i < queue->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, queue->queSize);\n    free(arr);\n}\n
linkedlist_queue.kt
/* 連結リストベースのキュー */\nclass LinkedListQueue(\n    // 先頭ノード front、末尾ノード rear\n    private var front: ListNode? = null,\n    private var rear: ListNode? = null,\n    private var queSize: Int = 0\n) {\n\n    /* キューの長さを取得 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* キューが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* エンキュー */\n    fun push(num: Int) {\n        // 末尾ノードの後ろに num を追加\n        val node = ListNode(num)\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (front == null) {\n            front = node\n            rear = node\n            // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        } else {\n            rear?.next = node\n            rear = node\n        }\n        queSize++\n    }\n\n    /* デキュー */\n    fun pop(): Int {\n        val num = peek()\n        // 先頭ノードを削除\n        front = front?.next\n        queSize--\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* 連結リストを Array に変換して返す */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_queue.rb
### 連結リストで実装したキュー ###\nclass LinkedListQueue\n  ### キューの長さを取得 ###\n  attr_reader :size\n\n  ### コンストラクタ ###\n  def initialize\n    @front = nil  # 先頭ノード front\n    @rear = nil   # 末尾ノード rear\n    @size = 0\n  end\n\n  ### キューが空か判定 ###\n  def is_empty?\n    @front.nil?\n  end\n\n  ### エンキュー ###\n  def push(num)\n    # 末尾ノードの後ろに num を追加\n    node = ListNode.new(num)\n\n    # キューが空なら、先頭ノードと末尾ノードの両方をそのノードに向ける\n    if @front.nil?\n      @front = node\n      @rear = node\n    # キューが空でなければ、そのノードを末尾ノードの後ろに追加する\n    else\n      @rear.next = node\n      @rear = node\n    end\n\n    @size += 1\n  end\n\n  ### デキュー ###\n  def pop\n    num = peek\n    # 先頭ノードを削除\n    @front = @front.next\n    @size -= 1\n    num\n  end\n\n  ### 先頭要素にアクセス ###\n  def peek\n    raise IndexError, 'キューは空です' if is_empty?\n\n    @front.val\n  end\n\n  ### 連結リストを Array に変換して返す ###\n  def to_array\n    queue = []\n    temp = @front\n    while temp\n      queue << temp.val\n      temp = temp.next\n    end\n    queue\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#2","level":3,"title":"2.   配列に基づく実装","text":"

配列で先頭要素を削除する時間計算量は \\(O(n)\\) であり、そのままではデキュー操作の効率が低くなります。しかし、次の巧妙な方法によってこの問題を回避できます。

変数 front を用いてキュー先頭要素のインデックスを指し、さらに変数 size でキューの長さを記録できます。rear = front + size と定義すると、この式で得られる rear はキュー末尾要素の次の位置を指します。

この設計に基づくと、配列内で要素を含む有効区間は [front, rear - 1] となります。各種操作の実装方法を下図に示します。

  • エンキュー操作:入力要素を rear の位置に代入し、size を 1 増やします。
  • デキュー操作:front を 1 増やし、size を 1 減らすだけです。

このように、エンキューとデキューはいずれも 1 回の操作だけで済み、時間計算量はともに \\(O(1)\\) です。

ArrayQueuepush()pop()

図 5-6   配列でキューを実装したエンキューとデキュー操作

ここで 1 つ問題があります。エンキューとデキューを繰り返すと、frontrear はどちらも右へ移動し続け、配列の末尾に達するとそれ以上進めなくなります。この問題を解決するために、配列を先頭と末尾がつながった「環状配列」とみなします。

環状配列では、front または rear が配列末尾を越えたときに、直ちに配列先頭へ戻って走査を続けられるようにする必要があります。この周期的な規則は「剰余演算」によって実現できます。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_queue.py
class ArrayQueue:\n    \"\"\"循環配列ベースのキュー\"\"\"\n\n    def __init__(self, size: int):\n        \"\"\"コンストラクタ\"\"\"\n        self._nums: list[int] = [0] * size  # キュー要素を格納する配列\n        self._front: int = 0  # 先頭ポインタ。先頭要素を指す\n        self._size: int = 0  # キューの長さ\n\n    def capacity(self) -> int:\n        \"\"\"キューの容量を取得\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"キューの長さを取得\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"キューが空かどうかを判定\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"エンキュー\"\"\"\n        if self._size == self.capacity():\n            raise IndexError(\"キューがいっぱいです\")\n        # 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        # 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        rear: int = (self._front + self._size) % self.capacity()\n        # num をキュー末尾に追加\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"デキュー\"\"\"\n        num: int = self.peek()\n        # 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        self._front = (self._front + 1) % self.capacity()\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"キュー先頭の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"キューが空です\")\n        return self._nums[self._front]\n\n    def to_list(self) -> list[int]:\n        \"\"\"表示用のリストを返す\"\"\"\n        res = [0] * self.size()\n        j: int = self._front\n        for i in range(self.size()):\n            res[i] = self._nums[(j % self.capacity())]\n            j += 1\n        return res\n
array_queue.cpp
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n  private:\n    int *nums;       // キュー要素を格納する配列\n    int front;       // 先頭ポインタ。先頭要素を指す\n    int queSize;     // キューの長さ\n    int queCapacity; // キューの容量\n\n  public:\n    ArrayQueue(int capacity) {\n        // 配列を初期化\n        nums = new int[capacity];\n        queCapacity = capacity;\n        front = queSize = 0;\n    }\n\n    ~ArrayQueue() {\n        delete[] nums;\n    }\n\n    /* キューの容量を取得 */\n    int capacity() {\n        return queCapacity;\n    }\n\n    /* キューの長さを取得 */\n    int size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* エンキュー */\n    void push(int num) {\n        if (queSize == queCapacity) {\n            cout << \"キューがいっぱいです\" << endl;\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        int rear = (front + queSize) % queCapacity;\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* デキュー */\n    int pop() {\n        int num = peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        front = (front + 1) % queCapacity;\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    int peek() {\n        if (isEmpty())\n            throw out_of_range(\"キューが空です\");\n        return nums[front];\n    }\n\n    /* 配列を Vector に変換して返す */\n    vector<int> toVector() {\n        // 有効長の範囲内のリスト要素のみを変換\n        vector<int> arr(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            arr[i] = nums[j % queCapacity];\n        }\n        return arr;\n    }\n};\n
array_queue.java
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n    private int[] nums; // キュー要素を格納する配列\n    private int front; // 先頭ポインタ。先頭要素を指す\n    private int queSize; // キューの長さ\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* キューの容量を取得 */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* キューの長さを取得 */\n    public int size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* エンキュー */\n    public void push(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"キューは満杯です\");\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        int rear = (front + queSize) % capacity();\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* デキュー */\n    public int pop() {\n        int num = peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        front = (front + 1) % capacity();\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* 配列を返す */\n    public int[] toArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % capacity()];\n        }\n        return res;\n    }\n}\n
array_queue.cs
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n    int[] nums;  // キュー要素を格納する配列\n    int front;   // 先頭ポインタ。先頭要素を指す\n    int queSize; // キューの長さ\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* キューの容量を取得 */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* キューの長さを取得 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* エンキュー */\n    public void Push(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"キューは満杯です\");\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        int rear = (front + queSize) % Capacity();\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* デキュー */\n    public int Pop() {\n        int num = Peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        front = (front + 1) % Capacity();\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return nums[front];\n    }\n\n    /* 配列を返す */\n    public int[] ToArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % this.Capacity()];\n        }\n        return res;\n    }\n}\n
array_queue.go
/* 循環配列ベースのキュー */\ntype arrayQueue struct {\n    nums        []int // キュー要素を格納する配列\n    front       int   // 先頭ポインタ。先頭要素を指す\n    queSize     int   // キューの長さ\n    queCapacity int   // キュー容量(格納できる要素数の上限)\n}\n\n/* キューを初期化 */\nfunc newArrayQueue(queCapacity int) *arrayQueue {\n    return &arrayQueue{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* キューの長さを取得 */\nfunc (q *arrayQueue) size() int {\n    return q.queSize\n}\n\n/* キューが空かどうかを判定 */\nfunc (q *arrayQueue) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* エンキュー */\nfunc (q *arrayQueue) push(num int) {\n    // rear == queCapacity のときキューは満杯\n    if q.queSize == q.queCapacity {\n        return\n    }\n    // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n    // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n    rear := (q.front + q.queSize) % q.queCapacity\n    // num をキュー末尾に追加\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* デキュー */\nfunc (q *arrayQueue) pop() any {\n    num := q.peek()\n    if num == nil {\n        return nil\n    }\n\n    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n    q.front = (q.front + 1) % q.queCapacity\n    q.queSize--\n    return num\n}\n\n/* キュー先頭の要素にアクセス */\nfunc (q *arrayQueue) peek() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* 表示用に Slice を取得 */\nfunc (q *arrayQueue) toSlice() []int {\n    rear := (q.front + q.queSize)\n    if rear >= q.queCapacity {\n        rear %= q.queCapacity\n        return append(q.nums[q.front:], q.nums[:rear]...)\n    }\n    return q.nums[q.front:rear]\n}\n
array_queue.swift
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n    private var nums: [Int] // キュー要素を格納する配列\n    private var front: Int // 先頭ポインタ。先頭要素を指す\n    private var _size: Int // キューの長さ\n\n    init(capacity: Int) {\n        // 配列を初期化\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* キューの容量を取得 */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* キューの長さを取得 */\n    func size() -> Int {\n        _size\n    }\n\n    /* キューが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* エンキュー */\n    func push(num: Int) {\n        if size() == capacity() {\n            print(\"キューがいっぱいです\")\n            return\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        let rear = (front + size()) % capacity()\n        // num をキュー末尾に追加\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* デキュー */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        front = (front + 1) % capacity()\n        _size -= 1\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"キューが空です\")\n        }\n        return nums[front]\n    }\n\n    /* 配列を返す */\n    func toArray() -> [Int] {\n        // 有効長の範囲内のリスト要素のみを変換\n        (front ..< front + size()).map { nums[$0 % capacity()] }\n    }\n}\n
array_queue.js
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n    #nums; // キュー要素を格納する配列\n    #front = 0; // 先頭ポインタ。先頭要素を指す\n    #queSize = 0; // キューの長さ\n\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n    }\n\n    /* キューの容量を取得 */\n    get capacity() {\n        return this.#nums.length;\n    }\n\n    /* キューの長さを取得 */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* エンキュー */\n    push(num) {\n        if (this.size === this.capacity) {\n            console.log('キューがいっぱいです');\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        const rear = (this.#front + this.size) % this.capacity;\n        // num をキュー末尾に追加\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* デキュー */\n    pop() {\n        const num = this.peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        this.#front = (this.#front + 1) % this.capacity;\n        this.#queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peek() {\n        if (this.isEmpty()) throw new Error('キューが空です');\n        return this.#nums[this.#front];\n    }\n\n    /* Array を返す */\n    toArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.#front; i < this.size; i++, j++) {\n            arr[i] = this.#nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
array_queue.ts
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n    private nums: number[]; // キュー要素を格納する配列\n    private front: number; // 先頭ポインタ。先頭要素を指す\n    private queSize: number; // キューの長さ\n\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = this.queSize = 0;\n    }\n\n    /* キューの容量を取得 */\n    get capacity(): number {\n        return this.nums.length;\n    }\n\n    /* キューの長さを取得 */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* エンキュー */\n    push(num: number): void {\n        if (this.size === this.capacity) {\n            console.log('キューは満杯です');\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        const rear = (this.front + this.queSize) % this.capacity;\n        // num をキュー末尾に追加\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* デキュー */\n    pop(): number {\n        const num = this.peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        this.front = (this.front + 1) % this.capacity;\n        this.queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peek(): number {\n        if (this.isEmpty()) throw new Error('キューが空です');\n        return this.nums[this.front];\n    }\n\n    /* Array を返す */\n    toArray(): number[] {\n        // 有効長の範囲内のリスト要素のみを変換\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.front; i < this.size; i++, j++) {\n            arr[i] = this.nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
array_queue.dart
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n  late List<int> _nums; // キュー要素を格納する配列\n  late int _front; // 先頭ポインタ。先頭要素を指す\n  late int _queSize; // キューの長さ\n\n  ArrayQueue(int capacity) {\n    _nums = List.filled(capacity, 0);\n    _front = _queSize = 0;\n  }\n\n  /* キューの容量を取得 */\n  int capaCity() {\n    return _nums.length;\n  }\n\n  /* キューの長さを取得 */\n  int size() {\n    return _queSize;\n  }\n\n  /* キューが空かどうかを判定 */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* エンキュー */\n  void push(int _num) {\n    if (_queSize == capaCity()) {\n      throw Exception(\"キューは満杯です\");\n    }\n    // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n    // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n    int rear = (_front + _queSize) % capaCity();\n    // _num をキュー末尾に追加\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* デキュー */\n  int pop() {\n    int _num = peek();\n    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n    _front = (_front + 1) % capaCity();\n    _queSize--;\n    return _num;\n  }\n\n  /* キュー先頭の要素にアクセス */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"キューが空です\");\n    }\n    return _nums[_front];\n  }\n\n  /* Array を返す */\n  List<int> toArray() {\n    // 有効長の範囲内のリスト要素のみを変換\n    final List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[j % capaCity()];\n    }\n    return res;\n  }\n}\n
array_queue.rs
/* 循環配列ベースのキュー */\nstruct ArrayQueue<T> {\n    nums: Vec<T>,      // キュー要素を格納する配列\n    front: i32,        // 先頭ポインタ。先頭要素を指す\n    que_size: i32,     // キューの長さ\n    que_capacity: i32, // キューの容量\n}\n\nimpl<T: Copy + Default> ArrayQueue<T> {\n    /* コンストラクタ */\n    fn new(capacity: i32) -> ArrayQueue<T> {\n        ArrayQueue {\n            nums: vec![T::default(); capacity as usize],\n            front: 0,\n            que_size: 0,\n            que_capacity: capacity,\n        }\n    }\n\n    /* キューの容量を取得 */\n    fn capacity(&self) -> i32 {\n        self.que_capacity\n    }\n\n    /* キューの長さを取得 */\n    fn size(&self) -> i32 {\n        self.que_size\n    }\n\n    /* キューが空かどうかを判定 */\n    fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* エンキュー */\n    fn push(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"キューがいっぱいです\");\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        let rear = (self.front + self.que_size) % self.que_capacity;\n        // num をキュー末尾に追加\n        self.nums[rear as usize] = num;\n        self.que_size += 1;\n    }\n\n    /* デキュー */\n    fn pop(&mut self) -> T {\n        let num = self.peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        self.front = (self.front + 1) % self.que_capacity;\n        self.que_size -= 1;\n        num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fn peek(&self) -> T {\n        if self.is_empty() {\n            panic!(\"index out of bounds\");\n        }\n        self.nums[self.front as usize]\n    }\n\n    /* 配列を返す */\n    fn to_vector(&self) -> Vec<T> {\n        let cap = self.que_capacity;\n        let mut j = self.front;\n        let mut arr = vec![T::default(); cap as usize];\n        for i in 0..self.que_size {\n            arr[i as usize] = self.nums[(j % cap) as usize];\n            j += 1;\n        }\n        arr\n    }\n}\n
array_queue.c
/* 循環配列ベースのキュー */\ntypedef struct {\n    int *nums;       // キュー要素を格納する配列\n    int front;       // 先頭ポインタ。先頭要素を指す\n    int queSize;     // 末尾ポインタ。キューの末尾 + 1 を指す\n    int queCapacity; // キューの容量\n} ArrayQueue;\n\n/* コンストラクタ */\nArrayQueue *newArrayQueue(int capacity) {\n    ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue));\n    // 配列を初期化\n    queue->queCapacity = capacity;\n    queue->nums = (int *)malloc(sizeof(int) * queue->queCapacity);\n    queue->front = queue->queSize = 0;\n    return queue;\n}\n\n/* デストラクタ */\nvoid delArrayQueue(ArrayQueue *queue) {\n    free(queue->nums);\n    free(queue);\n}\n\n/* キューの容量を取得 */\nint capacity(ArrayQueue *queue) {\n    return queue->queCapacity;\n}\n\n/* キューの長さを取得 */\nint size(ArrayQueue *queue) {\n    return queue->queSize;\n}\n\n/* キューが空かどうかを判定 */\nbool empty(ArrayQueue *queue) {\n    return queue->queSize == 0;\n}\n\n/* キュー先頭の要素にアクセス */\nint peek(ArrayQueue *queue) {\n    assert(size(queue) != 0);\n    return queue->nums[queue->front];\n}\n\n/* エンキュー */\nvoid push(ArrayQueue *queue, int num) {\n    if (size(queue) == capacity(queue)) {\n        printf(\"キューは満杯です\\r\\n\");\n        return;\n    }\n    // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n    // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n    int rear = (queue->front + queue->queSize) % queue->queCapacity;\n    // num をキュー末尾に追加\n    queue->nums[rear] = num;\n    queue->queSize++;\n}\n\n/* デキュー */\nint pop(ArrayQueue *queue) {\n    int num = peek(queue);\n    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n    queue->front = (queue->front + 1) % queue->queCapacity;\n    queue->queSize--;\n    return num;\n}\n\n/* 出力用の配列を返す */\nint *toArray(ArrayQueue *queue, int *queSize) {\n    *queSize = queue->queSize;\n    int *res = (int *)calloc(queue->queSize, sizeof(int));\n    int j = queue->front;\n    for (int i = 0; i < queue->queSize; i++) {\n        res[i] = queue->nums[j % queue->queCapacity];\n        j++;\n    }\n    return res;\n}\n
array_queue.kt
/* 循環配列ベースのキュー */\nclass ArrayQueue(capacity: Int) {\n    private val nums: IntArray = IntArray(capacity) // キュー要素を格納する配列\n    private var front: Int = 0 // 先頭ポインタ。先頭要素を指す\n    private var queSize: Int = 0 // キューの長さ\n\n    /* キューの容量を取得 */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* キューの長さを取得 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* キューが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* エンキュー */\n    fun push(num: Int) {\n        if (queSize == capacity()) {\n            println(\"キューは満杯です\")\n            return\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        val rear = (front + queSize) % capacity()\n        // num をキュー末尾に追加\n        nums[rear] = num\n        queSize++\n    }\n\n    /* デキュー */\n    fun pop(): Int {\n        val num = peek()\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        front = (front + 1) % capacity()\n        queSize--\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* 配列を返す */\n    fun toArray(): IntArray {\n        // 有効長の範囲内のリスト要素のみを変換\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[j % capacity()]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
array_queue.rb
### 循環配列で実装したキュー ###\nclass ArrayQueue\n  ### キューの長さを取得 ###\n  attr_reader :size\n\n  ### コンストラクタ ###\n  def initialize(size)\n    @nums = Array.new(size, 0) # キュー要素を格納する配列\n    @front = 0 # 先頭ポインタ。先頭要素を指す\n    @size = 0 # キューの長さ\n  end\n\n  ### キューの容量を取得 ###\n  def capacity\n    @nums.length\n  end\n\n  ### キューが空か判定 ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### エンキュー ###\n  def push(num)\n    raise IndexError, 'キューがいっぱいです' if size == capacity\n\n    # 末尾ポインタを計算し、末尾インデックス + 1 を指す\n    # 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n    rear = (@front + size) % capacity\n    # num をキュー末尾に追加\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### デキュー ###\n  def pop\n    num = peek\n    # 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n    @front = (@front + 1) % capacity\n    @size -= 1\n    num\n  end\n\n  ### 先頭要素にアクセス ###\n  def peek\n    raise IndexError, 'キューは空です' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### 表示用のリストを返す ###\n  def to_array\n    res = Array.new(size, 0)\n    j = @front\n\n    for i in 0...size\n      res[i] = @nums[j % capacity]\n      j += 1\n    end\n\n    res\n  end\nend\n
コードの可視化

全画面で見る >

上記の実装によるキューにも制約があり、長さを可変にできません。しかし、この問題の解決は難しくなく、配列を動的配列に置き換えれば容量拡張の仕組みを導入できます。興味があれば自分で実装してみてください。

2 つの実装の比較に関する結論はスタックの場合と同じなので、ここでは繰り返しません。

","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#523","level":2,"title":"5.2.3   キューの典型的な応用","text":"
  • 淘宝の注文。購入者が注文すると、その注文はキューに追加され、システムは順番に従って注文を処理します。ダブルイレブンの期間には短時間で膨大な注文が発生するため、高並行性がエンジニアにとって重点的に解決すべき課題になります。
  • 各種の待機事項。先着順の機能を実現する必要があるあらゆる場面、たとえばプリンターのジョブキューや飲食店の配膳キューなどでは、キューによって処理順序を効果的に維持できます。
","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/stack/","level":1,"title":"5.1   スタック","text":"

スタック(stack)は、後入れ先出しの論理に従う線形データ構造です。

スタックは机の上に積まれた皿の山にたとえられます。1回に1枚の皿しか動かせないとすると、いちばん下の皿を取り出すには、上にある皿を順番にどかす必要があります。この皿をさまざまな型の要素(整数、文字、オブジェクトなど)に置き換えたものが、スタックというデータ構造です。

下図のように、積み重なった要素の上端を「スタックトップ」、下端を「スタックボトム」と呼びます。要素をスタックトップに追加する操作を「プッシュ」、スタックトップの要素を削除する操作を「ポップ」と呼びます。

図 5-1   スタックの後入れ先出しの規則

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511","level":2,"title":"5.1.1   スタックの基本操作","text":"

スタックの基本操作を以下の表に示します。具体的なメソッド名は使用するプログラミング言語によって異なります。ここでは、一般的な push()pop()peek() を例に挙げます。

表 5-1   スタックの操作効率

メソッド 説明 時間計算量 push() 要素をプッシュする(スタックトップに追加) \\(O(1)\\) pop() スタックトップの要素をポップする \\(O(1)\\) peek() スタックトップの要素にアクセスする \\(O(1)\\)

通常は、プログラミング言語に組み込まれているスタッククラスをそのまま利用できます。ただし、専用のスタッククラスが用意されていない言語もあります。その場合は、その言語の「配列」や「連結リスト」をスタックとして用い、プログラムのロジック上でスタックに無関係な操作を無視します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby stack.py
# スタックを初期化\n# Python には組み込みのスタッククラスがないため、list をスタックとして使用できる\nstack: list[int] = []\n\n# 要素をプッシュ\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n# スタックトップの要素にアクセス\npeek: int = stack[-1]\n\n# 要素をポップ\npop: int = stack.pop()\n\n# スタックの長さを取得\nsize: int = len(stack)\n\n# 空かどうかを判定\nis_empty: bool = len(stack) == 0\n
stack.cpp
/* スタックを初期化 */\nstack<int> stack;\n\n/* 要素をプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックトップの要素にアクセス */\nint top = stack.top();\n\n/* 要素をポップ */\nstack.pop(); // 戻り値なし\n\n/* スタックの長さを取得 */\nint size = stack.size();\n\n/* 空かどうかを判定 */\nbool empty = stack.empty();\n
stack.java
/* スタックを初期化 */\nStack<Integer> stack = new Stack<>();\n\n/* 要素をプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックトップの要素にアクセス */\nint peek = stack.peek();\n\n/* 要素をポップ */\nint pop = stack.pop();\n\n/* スタックの長さを取得 */\nint size = stack.size();\n\n/* 空かどうかを判定 */\nboolean isEmpty = stack.isEmpty();\n
stack.cs
/* スタックを初期化 */\nStack<int> stack = new();\n\n/* 要素をプッシュ */\nstack.Push(1);\nstack.Push(3);\nstack.Push(2);\nstack.Push(5);\nstack.Push(4);\n\n/* スタックトップの要素にアクセス */\nint peek = stack.Peek();\n\n/* 要素をポップ */\nint pop = stack.Pop();\n\n/* スタックの長さを取得 */\nint size = stack.Count;\n\n/* 空かどうかを判定 */\nbool isEmpty = stack.Count == 0;\n
stack_test.go
/* スタックを初期化 */\n// Go では、Slice をスタックとして使うのが一般的\nvar stack []int\n\n/* 要素をプッシュ */\nstack = append(stack, 1)\nstack = append(stack, 3)\nstack = append(stack, 2)\nstack = append(stack, 5)\nstack = append(stack, 4)\n\n/* スタックトップの要素にアクセス */\npeek := stack[len(stack)-1]\n\n/* 要素をポップ */\npop := stack[len(stack)-1]\nstack = stack[:len(stack)-1]\n\n/* スタックの長さを取得 */\nsize := len(stack)\n\n/* 空かどうかを判定 */\nisEmpty := len(stack) == 0\n
stack.swift
/* スタックを初期化 */\n// Swift には組み込みのスタッククラスがないため、Array をスタックとして使用できる\nvar stack: [Int] = []\n\n/* 要素をプッシュ */\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n/* スタックトップの要素にアクセス */\nlet peek = stack.last!\n\n/* 要素をポップ */\nlet pop = stack.removeLast()\n\n/* スタックの長さを取得 */\nlet size = stack.count\n\n/* 空かどうかを判定 */\nlet isEmpty = stack.isEmpty\n
stack.js
/* スタックを初期化 */\n// JavaScript には組み込みのスタッククラスがないため、Array をスタックとして使用できる\nconst stack = [];\n\n/* 要素をプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックトップの要素にアクセス */\nconst peek = stack[stack.length-1];\n\n/* 要素をポップ */\nconst pop = stack.pop();\n\n/* スタックの長さを取得 */\nconst size = stack.length;\n\n/* 空かどうかを判定 */\nconst is_empty = stack.length === 0;\n
stack.ts
/* スタックを初期化 */\n// TypeScript には組み込みのスタッククラスがないため、Array をスタックとして使用できる\nconst stack: number[] = [];\n\n/* 要素をプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックトップの要素にアクセス */\nconst peek = stack[stack.length - 1];\n\n/* 要素をポップ */\nconst pop = stack.pop();\n\n/* スタックの長さを取得 */\nconst size = stack.length;\n\n/* 空かどうかを判定 */\nconst is_empty = stack.length === 0;\n
stack.dart
/* スタックを初期化 */\n// Dart には組み込みのスタッククラスがないため、List をスタックとして使用できる\nList<int> stack = [];\n\n/* 要素をプッシュ */\nstack.add(1);\nstack.add(3);\nstack.add(2);\nstack.add(5);\nstack.add(4);\n\n/* スタックトップの要素にアクセス */\nint peek = stack.last;\n\n/* 要素をポップ */\nint pop = stack.removeLast();\n\n/* スタックの長さを取得 */\nint size = stack.length;\n\n/* 空かどうかを判定 */\nbool isEmpty = stack.isEmpty;\n
stack.rs
/* スタックを初期化 */\n// Vec をスタックとして使用する\nlet mut stack: Vec<i32> = Vec::new();\n\n/* 要素をプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックトップの要素にアクセス */\nlet top = stack.last().unwrap();\n\n/* 要素をポップ */\nlet pop = stack.pop().unwrap();\n\n/* スタックの長さを取得 */\nlet size = stack.len();\n\n/* 空かどうかを判定 */\nlet is_empty = stack.is_empty();\n
stack.c
// C には組み込みのスタックがない\n
stack.kt
/* スタックを初期化 */\nval stack = Stack<Int>()\n\n/* 要素をプッシュ */\nstack.push(1)\nstack.push(3)\nstack.push(2)\nstack.push(5)\nstack.push(4)\n\n/* スタックトップの要素にアクセス */\nval peek = stack.peek()\n\n/* 要素をポップ */\nval pop = stack.pop()\n\n/* スタックの長さを取得 */\nval size = stack.size\n\n/* 空かどうかを判定 */\nval isEmpty = stack.isEmpty()\n
stack.rb
# スタックを初期化\n# Ruby には組み込みのスタッククラスがないため、Array をスタックとして使用できる\nstack = []\n\n# 要素をプッシュ\nstack << 1\nstack << 3\nstack << 2\nstack << 5\nstack << 4\n\n# スタックトップの要素にアクセス\npeek = stack.last\n\n# 要素をポップ\npop = stack.pop\n\n# スタックの長さを取得\nsize = stack.length\n\n# 空かどうかを判定\nis_empty = stack.empty?\n
実行の可視化

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20%23%20Python%20%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%A0%88%E7%B1%BB%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8A%8A%20list%20%E5%BD%93%E4%BD%9C%E6%A0%88%E6%9D%A5%E4%BD%BF%E7%94%A8%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.append%281%29%0A%20%20%20%20stack.append%283%29%0A%20%20%20%20stack.append%282%29%0A%20%20%20%20stack.append%285%29%0A%20%20%20%20stack.append%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512","level":2,"title":"5.1.2   スタックの実装","text":"

スタックの動作の仕組みをより深く理解するために、自分でスタッククラスを実装してみましょう。

スタックは後入れ先出しの原則に従うため、要素の追加や削除はスタックトップでしか行えません。一方、配列や連結リストでは任意の位置で要素を追加・削除できます。つまり、スタックは制限付きの配列または連結リストとみなせます。 言い換えると、配列や連結リストのうち無関係な操作を「隠蔽」することで、外から見た振る舞いをスタックの特性に合わせられます。

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   連結リストによる実装","text":"

連結リストでスタックを実装する場合、連結リストの先頭ノードをスタックトップ、末尾ノードをスタックボトムとみなせます。

下図のように、プッシュ操作では要素を連結リストの先頭に挿入するだけでよく、このノード挿入方法は「頭部挿入法」と呼ばれます。ポップ操作では、先頭ノードを連結リストから削除するだけです。

LinkedListStackpush()pop()

図 5-2   連結リストによるスタック実装のプッシュ・ポップ操作

以下は、連結リストによってスタックを実装したコード例です:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_stack.py
class LinkedListStack:\n    \"\"\"連結リストベースのスタック\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self._peek: ListNode | None = None\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"スタックの長さを取得\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"スタックが空かどうかを判定\"\"\"\n        return self._size == 0\n\n    def push(self, val: int):\n        \"\"\"プッシュ\"\"\"\n        node = ListNode(val)\n        node.next = self._peek\n        self._peek = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"ポップ\"\"\"\n        num = self.peek()\n        self._peek = self._peek.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"スタックトップの要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"スタックが空です\")\n        return self._peek.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"表示用にリストへ変換\"\"\"\n        arr = []\n        node = self._peek\n        while node:\n            arr.append(node.val)\n            node = node.next\n        arr.reverse()\n        return arr\n
linkedlist_stack.cpp
/* 連結リストベースのスタック */\nclass LinkedListStack {\n  private:\n    ListNode *stackTop; // 先頭ノードをスタックトップとする\n    int stkSize;        // スタックの長さ\n\n  public:\n    LinkedListStack() {\n        stackTop = nullptr;\n        stkSize = 0;\n    }\n\n    ~LinkedListStack() {\n        // 連結リストを走査してノードを削除し、メモリを解放する\n        freeMemoryLinkedList(stackTop);\n    }\n\n    /* スタックの長さを取得 */\n    int size() {\n        return stkSize;\n    }\n\n    /* スタックが空かどうかを判定 */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* プッシュ */\n    void push(int num) {\n        ListNode *node = new ListNode(num);\n        node->next = stackTop;\n        stackTop = node;\n        stkSize++;\n    }\n\n    /* ポップ */\n    int pop() {\n        int num = top();\n        ListNode *tmp = stackTop;\n        stackTop = stackTop->next;\n        // メモリを解放する\n        delete tmp;\n        stkSize--;\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"スタックが空です\");\n        return stackTop->val;\n    }\n\n    /* List を Array に変換して返す */\n    vector<int> toVector() {\n        ListNode *node = stackTop;\n        vector<int> res(size());\n        for (int i = res.size() - 1; i >= 0; i--) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_stack.java
/* 連結リストベースのスタック */\nclass LinkedListStack {\n    private ListNode stackPeek; // 先頭ノードをスタックトップとする\n    private int stkSize = 0; // スタックの長さ\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* スタックの長さを取得 */\n    public int size() {\n        return stkSize;\n    }\n\n    /* スタックが空かどうかを判定 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* プッシュ */\n    public void push(int num) {\n        ListNode node = new ListNode(num);\n        node.next = stackPeek;\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* ポップ */\n    public int pop() {\n        int num = peek();\n        stackPeek = stackPeek.next;\n        stkSize--;\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stackPeek.val;\n    }\n\n    /* List を Array に変換して返す */\n    public int[] toArray() {\n        ListNode node = stackPeek;\n        int[] res = new int[size()];\n        for (int i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.cs
/* 連結リストベースのスタック */\nclass LinkedListStack {\n    ListNode? stackPeek;  // 先頭ノードをスタックトップとする\n    int stkSize = 0;   // スタックの長さ\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* スタックの長さを取得 */\n    public int Size() {\n        return stkSize;\n    }\n\n    /* スタックが空かどうかを判定 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* プッシュ */\n    public void Push(int num) {\n        ListNode node = new(num) {\n            next = stackPeek\n        };\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* ポップ */\n    public int Pop() {\n        int num = Peek();\n        stackPeek = stackPeek!.next;\n        stkSize--;\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stackPeek!.val;\n    }\n\n    /* List を Array に変換して返す */\n    public int[] ToArray() {\n        if (stackPeek == null)\n            return [];\n\n        ListNode? node = stackPeek;\n        int[] res = new int[Size()];\n        for (int i = res.Length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.go
/* 連結リストベースのスタック */\ntype linkedListStack struct {\n    // 組み込みパッケージ list でスタックを実装する\n    data *list.List\n}\n\n/* スタックを初期化 */\nfunc newLinkedListStack() *linkedListStack {\n    return &linkedListStack{\n        data: list.New(),\n    }\n}\n\n/* プッシュ */\nfunc (s *linkedListStack) push(value int) {\n    s.data.PushBack(value)\n}\n\n/* ポップ */\nfunc (s *linkedListStack) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* スタックトップの要素にアクセス */\nfunc (s *linkedListStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* スタックの長さを取得 */\nfunc (s *linkedListStack) size() int {\n    return s.data.Len()\n}\n\n/* スタックが空かどうかを判定 */\nfunc (s *linkedListStack) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* 表示用に List を取得 */\nfunc (s *linkedListStack) toList() *list.List {\n    return s.data\n}\n
linkedlist_stack.swift
/* 連結リストベースのスタック */\nclass LinkedListStack {\n    private var _peek: ListNode? // 先頭ノードをスタックトップとする\n    private var _size: Int // スタックの長さ\n\n    init() {\n        _size = 0\n    }\n\n    /* スタックの長さを取得 */\n    func size() -> Int {\n        _size\n    }\n\n    /* スタックが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* プッシュ */\n    func push(num: Int) {\n        let node = ListNode(x: num)\n        node.next = _peek\n        _peek = node\n        _size += 1\n    }\n\n    /* ポップ */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        _peek = _peek?.next\n        _size -= 1\n        return num\n    }\n\n    /* スタックトップの要素にアクセス */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"スタックが空です\")\n        }\n        return _peek!.val\n    }\n\n    /* List を Array に変換して返す */\n    func toArray() -> [Int] {\n        var node = _peek\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices.reversed() {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_stack.js
/* 連結リストベースのスタック */\nclass LinkedListStack {\n    #stackPeek; // 先頭ノードをスタックトップとする\n    #stkSize = 0; // スタックの長さ\n\n    constructor() {\n        this.#stackPeek = null;\n    }\n\n    /* スタックの長さを取得 */\n    get size() {\n        return this.#stkSize;\n    }\n\n    /* スタックが空かどうかを判定 */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* プッシュ */\n    push(num) {\n        const node = new ListNode(num);\n        node.next = this.#stackPeek;\n        this.#stackPeek = node;\n        this.#stkSize++;\n    }\n\n    /* ポップ */\n    pop() {\n        const num = this.peek();\n        this.#stackPeek = this.#stackPeek.next;\n        this.#stkSize--;\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    peek() {\n        if (!this.#stackPeek) throw new Error('スタックが空');\n        return this.#stackPeek.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    toArray() {\n        let node = this.#stackPeek;\n        const res = new Array(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.ts
/* 連結リストベースのスタック */\nclass LinkedListStack {\n    private stackPeek: ListNode | null; // 先頭ノードをスタックトップとする\n    private stkSize: number = 0; // スタックの長さ\n\n    constructor() {\n        this.stackPeek = null;\n    }\n\n    /* スタックの長さを取得 */\n    get size(): number {\n        return this.stkSize;\n    }\n\n    /* スタックが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* プッシュ */\n    push(num: number): void {\n        const node = new ListNode(num);\n        node.next = this.stackPeek;\n        this.stackPeek = node;\n        this.stkSize++;\n    }\n\n    /* ポップ */\n    pop(): number {\n        const num = this.peek();\n        if (!this.stackPeek) throw new Error('スタックが空です');\n        this.stackPeek = this.stackPeek.next;\n        this.stkSize--;\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    peek(): number {\n        if (!this.stackPeek) throw new Error('スタックが空です');\n        return this.stackPeek.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    toArray(): number[] {\n        let node = this.stackPeek;\n        const res = new Array<number>(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.dart
/* 連結リストクラスに基づくスタック */\nclass LinkedListStack {\n  ListNode? _stackPeek; // 先頭ノードをスタックトップとする\n  int _stkSize = 0; // スタックの長さ\n\n  LinkedListStack() {\n    _stackPeek = null;\n  }\n\n  /* スタックの長さを取得 */\n  int size() {\n    return _stkSize;\n  }\n\n  /* スタックが空かどうかを判定 */\n  bool isEmpty() {\n    return _stkSize == 0;\n  }\n\n  /* プッシュ */\n  void push(int _num) {\n    final ListNode node = ListNode(_num);\n    node.next = _stackPeek;\n    _stackPeek = node;\n    _stkSize++;\n  }\n\n  /* ポップ */\n  int pop() {\n    final int _num = peek();\n    _stackPeek = _stackPeek!.next;\n    _stkSize--;\n    return _num;\n  }\n\n  /* スタックトップの要素にアクセス */\n  int peek() {\n    if (_stackPeek == null) {\n      throw Exception(\"スタックが空です\");\n    }\n    return _stackPeek!.val;\n  }\n\n  /* 連結リストを List に変換して返す */\n  List<int> toList() {\n    ListNode? node = _stackPeek;\n    List<int> list = [];\n    while (node != null) {\n      list.add(node.val);\n      node = node.next;\n    }\n    list = list.reversed.toList();\n    return list;\n  }\n}\n
linkedlist_stack.rs
/* 連結リストベースのスタック */\n#[allow(dead_code)]\npub struct LinkedListStack<T> {\n    stack_peek: Option<Rc<RefCell<ListNode<T>>>>, // 先頭ノードをスタックトップとする\n    stk_size: usize,                              // スタックの長さ\n}\n\nimpl<T: Copy> LinkedListStack<T> {\n    pub fn new() -> Self {\n        Self {\n            stack_peek: None,\n            stk_size: 0,\n        }\n    }\n\n    /* スタックの長さを取得 */\n    pub fn size(&self) -> usize {\n        return self.stk_size;\n    }\n\n    /* スタックが空かどうかを判定 */\n    pub fn is_empty(&self) -> bool {\n        return self.size() == 0;\n    }\n\n    /* プッシュ */\n    pub fn push(&mut self, num: T) {\n        let node = ListNode::new(num);\n        node.borrow_mut().next = self.stack_peek.take();\n        self.stack_peek = Some(node);\n        self.stk_size += 1;\n    }\n\n    /* ポップ */\n    pub fn pop(&mut self) -> Option<T> {\n        self.stack_peek.take().map(|old_head| {\n            self.stack_peek = old_head.borrow_mut().next.take();\n            self.stk_size -= 1;\n\n            old_head.borrow().val\n        })\n    }\n\n    /* スタックトップの要素にアクセス */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.stack_peek.as_ref()\n    }\n\n    /* List を Array に変換して返す */\n    pub fn to_array(&self) -> Vec<T> {\n        fn _to_array<T: Sized + Copy>(head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n            if let Some(node) = head {\n                let mut nums = _to_array(node.borrow().next.as_ref());\n                nums.push(node.borrow().val);\n                return nums;\n            }\n            return Vec::new();\n        }\n\n        _to_array(self.peek())\n    }\n}\n
linkedlist_stack.c
/* 連結リストベースのスタック */\ntypedef struct {\n    ListNode *top; // 先頭ノードをスタックトップとする\n    int size;      // スタックの長さ\n} LinkedListStack;\n\n/* コンストラクタ */\nLinkedListStack *newLinkedListStack() {\n    LinkedListStack *s = malloc(sizeof(LinkedListStack));\n    s->top = NULL;\n    s->size = 0;\n    return s;\n}\n\n/* デストラクタ */\nvoid delLinkedListStack(LinkedListStack *s) {\n    while (s->top) {\n        ListNode *n = s->top->next;\n        free(s->top);\n        s->top = n;\n    }\n    free(s);\n}\n\n/* スタックの長さを取得 */\nint size(LinkedListStack *s) {\n    return s->size;\n}\n\n/* スタックが空かどうかを判定 */\nbool isEmpty(LinkedListStack *s) {\n    return size(s) == 0;\n}\n\n/* プッシュ */\nvoid push(LinkedListStack *s, int num) {\n    ListNode *node = (ListNode *)malloc(sizeof(ListNode));\n    node->next = s->top; // 新しく追加したノードのポインタフィールドを更新\n    node->val = num;     // 新しく追加したノードのデータフィールドを更新\n    s->top = node;       // スタックトップを更新\n    s->size++;           // スタックサイズを更新\n}\n\n/* スタックトップの要素にアクセス */\nint peek(LinkedListStack *s) {\n    if (s->size == 0) {\n        printf(\"スタックは空です\\n\");\n        return INT_MAX;\n    }\n    return s->top->val;\n}\n\n/* ポップ */\nint pop(LinkedListStack *s) {\n    int val = peek(s);\n    ListNode *tmp = s->top;\n    s->top = s->top->next;\n    // メモリを解放する\n    free(tmp);\n    s->size--;\n    return val;\n}\n
linkedlist_stack.kt
/* 連結リストベースのスタック */\nclass LinkedListStack(\n    private var stackPeek: ListNode? = null, // 先頭ノードをスタックトップとする\n    private var stkSize: Int = 0 // スタックの長さ\n) {\n\n    /* スタックの長さを取得 */\n    fun size(): Int {\n        return stkSize\n    }\n\n    /* スタックが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* プッシュ */\n    fun push(num: Int) {\n        val node = ListNode(num)\n        node.next = stackPeek\n        stackPeek = node\n        stkSize++\n    }\n\n    /* ポップ */\n    fun pop(): Int? {\n        val num = peek()\n        stackPeek = stackPeek?.next\n        stkSize--\n        return num\n    }\n\n    /* スタックトップの要素にアクセス */\n    fun peek(): Int? {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stackPeek?._val\n    }\n\n    /* List を Array に変換して返す */\n    fun toArray(): IntArray {\n        var node = stackPeek\n        val res = IntArray(size())\n        for (i in res.size - 1 downTo 0) {\n            res[i] = node?._val!!\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_stack.rb
### 連結リストで実装したスタック ###\nclass LinkedListStack\n  attr_reader :size\n\n  ### コンストラクタ ###\n  def initialize\n    @size = 0\n  end\n\n  ### スタックが空か判定 ###\n  def is_empty?\n    @peek.nil?\n  end\n\n  ### プッシュ ###\n  def push(val)\n    node = ListNode.new(val)\n    node.next = @peek\n    @peek = node\n    @size += 1\n  end\n\n  ### ポップ ###\n  def pop\n    num = peek\n    @peek = @peek.next\n    @size -= 1\n    num\n  end\n\n  ### スタックトップ要素を参照 ###\n  def peek\n    raise IndexError, 'スタックは空です' if is_empty?\n\n    @peek.val\n  end\n\n  ### 連結リストを Array に変換して返す ###\n  def to_array\n    arr = []\n    node = @peek\n    while node\n      arr << node.val\n      node = node.next\n    end\n    arr.reverse\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#2","level":3,"title":"2.   配列による実装","text":"

配列でスタックを実装する場合、配列の末尾をスタックトップとして扱えます。下図のように、プッシュとポップはそれぞれ配列末尾への要素追加と削除に対応し、どちらの時間計算量も \\(O(1)\\) です。

ArrayStackpush()pop()

図 5-3   配列によるスタック実装のプッシュ・ポップ操作

プッシュされる要素は際限なく増える可能性があるため、動的配列を使えば、配列の拡張を自前で処理する必要がありません。以下にコード例を示します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_stack.py
class ArrayStack:\n    \"\"\"配列ベースのスタック\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self._stack: list[int] = []\n\n    def size(self) -> int:\n        \"\"\"スタックの長さを取得\"\"\"\n        return len(self._stack)\n\n    def is_empty(self) -> bool:\n        \"\"\"スタックが空かどうかを判定\"\"\"\n        return self.size() == 0\n\n    def push(self, item: int):\n        \"\"\"プッシュ\"\"\"\n        self._stack.append(item)\n\n    def pop(self) -> int:\n        \"\"\"ポップ\"\"\"\n        if self.is_empty():\n            raise IndexError(\"スタックが空です\")\n        return self._stack.pop()\n\n    def peek(self) -> int:\n        \"\"\"スタックトップの要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"スタックが空です\")\n        return self._stack[-1]\n\n    def to_list(self) -> list[int]:\n        \"\"\"表示用のリストを返す\"\"\"\n        return self._stack\n
array_stack.cpp
/* 配列ベースのスタック */\nclass ArrayStack {\n  private:\n    vector<int> stack;\n\n  public:\n    /* スタックの長さを取得 */\n    int size() {\n        return stack.size();\n    }\n\n    /* スタックが空かどうかを判定 */\n    bool isEmpty() {\n        return stack.size() == 0;\n    }\n\n    /* プッシュ */\n    void push(int num) {\n        stack.push_back(num);\n    }\n\n    /* ポップ */\n    int pop() {\n        int num = top();\n        stack.pop_back();\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"スタックが空です\");\n        return stack.back();\n    }\n\n    /* Vector を返す */\n    vector<int> toVector() {\n        return stack;\n    }\n};\n
array_stack.java
/* 配列ベースのスタック */\nclass ArrayStack {\n    private ArrayList<Integer> stack;\n\n    public ArrayStack() {\n        // リスト(動的配列)を初期化する\n        stack = new ArrayList<>();\n    }\n\n    /* スタックの長さを取得 */\n    public int size() {\n        return stack.size();\n    }\n\n    /* スタックが空かどうかを判定 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* プッシュ */\n    public void push(int num) {\n        stack.add(num);\n    }\n\n    /* ポップ */\n    public int pop() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.remove(size() - 1);\n    }\n\n    /* スタックトップの要素にアクセス */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.get(size() - 1);\n    }\n\n    /* List を Array に変換して返す */\n    public Object[] toArray() {\n        return stack.toArray();\n    }\n}\n
array_stack.cs
/* 配列ベースのスタック */\nclass ArrayStack {\n    List<int> stack;\n    public ArrayStack() {\n        // リスト(動的配列)を初期化する\n        stack = [];\n    }\n\n    /* スタックの長さを取得 */\n    public int Size() {\n        return stack.Count;\n    }\n\n    /* スタックが空かどうかを判定 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* プッシュ */\n    public void Push(int num) {\n        stack.Add(num);\n    }\n\n    /* ポップ */\n    public int Pop() {\n        if (IsEmpty())\n            throw new Exception();\n        var val = Peek();\n        stack.RemoveAt(Size() - 1);\n        return val;\n    }\n\n    /* スタックトップの要素にアクセス */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stack[Size() - 1];\n    }\n\n    /* List を Array に変換して返す */\n    public int[] ToArray() {\n        return [.. stack];\n    }\n}\n
array_stack.go
/* 配列ベースのスタック */\ntype arrayStack struct {\n    data []int // データ\n}\n\n/* スタックを初期化 */\nfunc newArrayStack() *arrayStack {\n    return &arrayStack{\n        // スタックの長さを 0、容量を 16 に設定\n        data: make([]int, 0, 16),\n    }\n}\n\n/* スタックの長さ */\nfunc (s *arrayStack) size() int {\n    return len(s.data)\n}\n\n/* スタックが空かどうか */\nfunc (s *arrayStack) isEmpty() bool {\n    return s.size() == 0\n}\n\n/* プッシュ */\nfunc (s *arrayStack) push(v int) {\n    // スライスは自動で拡張される\n    s.data = append(s.data, v)\n}\n\n/* ポップ */\nfunc (s *arrayStack) pop() any {\n    val := s.peek()\n    s.data = s.data[:len(s.data)-1]\n    return val\n}\n\n/* スタックトップ要素を取得する */\nfunc (s *arrayStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    val := s.data[len(s.data)-1]\n    return val\n}\n\n/* 表示用に Slice を取得 */\nfunc (s *arrayStack) toSlice() []int {\n    return s.data\n}\n
array_stack.swift
/* 配列ベースのスタック */\nclass ArrayStack {\n    private var stack: [Int]\n\n    init() {\n        // リスト(動的配列)を初期化する\n        stack = []\n    }\n\n    /* スタックの長さを取得 */\n    func size() -> Int {\n        stack.count\n    }\n\n    /* スタックが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        stack.isEmpty\n    }\n\n    /* プッシュ */\n    func push(num: Int) {\n        stack.append(num)\n    }\n\n    /* ポップ */\n    @discardableResult\n    func pop() -> Int {\n        if isEmpty() {\n            fatalError(\"スタックが空です\")\n        }\n        return stack.removeLast()\n    }\n\n    /* スタックトップの要素にアクセス */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"スタックが空です\")\n        }\n        return stack.last!\n    }\n\n    /* List を Array に変換して返す */\n    func toArray() -> [Int] {\n        stack\n    }\n}\n
array_stack.js
/* 配列ベースのスタック */\nclass ArrayStack {\n    #stack;\n    constructor() {\n        this.#stack = [];\n    }\n\n    /* スタックの長さを取得 */\n    get size() {\n        return this.#stack.length;\n    }\n\n    /* スタックが空かどうかを判定 */\n    isEmpty() {\n        return this.#stack.length === 0;\n    }\n\n    /* プッシュ */\n    push(num) {\n        this.#stack.push(num);\n    }\n\n    /* ポップ */\n    pop() {\n        if (this.isEmpty()) throw new Error('スタックが空');\n        return this.#stack.pop();\n    }\n\n    /* スタックトップの要素にアクセス */\n    top() {\n        if (this.isEmpty()) throw new Error('スタックが空');\n        return this.#stack[this.#stack.length - 1];\n    }\n\n    /* Array を返す */\n    toArray() {\n        return this.#stack;\n    }\n}\n
array_stack.ts
/* 配列ベースのスタック */\nclass ArrayStack {\n    private stack: number[];\n    constructor() {\n        this.stack = [];\n    }\n\n    /* スタックの長さを取得 */\n    get size(): number {\n        return this.stack.length;\n    }\n\n    /* スタックが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.stack.length === 0;\n    }\n\n    /* プッシュ */\n    push(num: number): void {\n        this.stack.push(num);\n    }\n\n    /* ポップ */\n    pop(): number | undefined {\n        if (this.isEmpty()) throw new Error('スタックが空です');\n        return this.stack.pop();\n    }\n\n    /* スタックトップの要素にアクセス */\n    top(): number | undefined {\n        if (this.isEmpty()) throw new Error('スタックが空です');\n        return this.stack[this.stack.length - 1];\n    }\n\n    /* Array を返す */\n    toArray() {\n        return this.stack;\n    }\n}\n
array_stack.dart
/* 配列ベースのスタック */\nclass ArrayStack {\n  late List<int> _stack;\n  ArrayStack() {\n    _stack = [];\n  }\n\n  /* スタックの長さを取得 */\n  int size() {\n    return _stack.length;\n  }\n\n  /* スタックが空かどうかを判定 */\n  bool isEmpty() {\n    return _stack.isEmpty;\n  }\n\n  /* プッシュ */\n  void push(int _num) {\n    _stack.add(_num);\n  }\n\n  /* ポップ */\n  int pop() {\n    if (isEmpty()) {\n      throw Exception(\"スタックが空です\");\n    }\n    return _stack.removeLast();\n  }\n\n  /* スタックトップの要素にアクセス */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"スタックが空です\");\n    }\n    return _stack.last;\n  }\n\n  /* スタックを Array に変換して返す */\n  List<int> toArray() => _stack;\n}\n
array_stack.rs
/* 配列ベースのスタック */\nstruct ArrayStack<T> {\n    stack: Vec<T>,\n}\n\nimpl<T> ArrayStack<T> {\n    /* スタックを初期化 */\n    fn new() -> ArrayStack<T> {\n        ArrayStack::<T> {\n            stack: Vec::<T>::new(),\n        }\n    }\n\n    /* スタックの長さを取得 */\n    fn size(&self) -> usize {\n        self.stack.len()\n    }\n\n    /* スタックが空かどうかを判定 */\n    fn is_empty(&self) -> bool {\n        self.size() == 0\n    }\n\n    /* プッシュ */\n    fn push(&mut self, num: T) {\n        self.stack.push(num);\n    }\n\n    /* ポップ */\n    fn pop(&mut self) -> Option<T> {\n        self.stack.pop()\n    }\n\n    /* スタックトップの要素にアクセス */\n    fn peek(&self) -> Option<&T> {\n        if self.is_empty() {\n            panic!(\"スタックが空です\")\n        };\n        self.stack.last()\n    }\n\n    /* &Vec を返す */\n    fn to_array(&self) -> &Vec<T> {\n        &self.stack\n    }\n}\n
array_stack.c
/* 配列ベースのスタック */\ntypedef struct {\n    int *data;\n    int size;\n} ArrayStack;\n\n/* コンストラクタ */\nArrayStack *newArrayStack() {\n    ArrayStack *stack = malloc(sizeof(ArrayStack));\n    // 大きめの容量で初期化し、拡張を避ける\n    stack->data = malloc(sizeof(int) * MAX_SIZE);\n    stack->size = 0;\n    return stack;\n}\n\n/* デストラクタ */\nvoid delArrayStack(ArrayStack *stack) {\n    free(stack->data);\n    free(stack);\n}\n\n/* スタックの長さを取得 */\nint size(ArrayStack *stack) {\n    return stack->size;\n}\n\n/* スタックが空かどうかを判定 */\nbool isEmpty(ArrayStack *stack) {\n    return stack->size == 0;\n}\n\n/* プッシュ */\nvoid push(ArrayStack *stack, int num) {\n    if (stack->size == MAX_SIZE) {\n        printf(\"スタックは満杯です\\n\");\n        return;\n    }\n    stack->data[stack->size] = num;\n    stack->size++;\n}\n\n/* スタックトップの要素にアクセス */\nint peek(ArrayStack *stack) {\n    if (stack->size == 0) {\n        printf(\"スタックは空です\\n\");\n        return INT_MAX;\n    }\n    return stack->data[stack->size - 1];\n}\n\n/* ポップ */\nint pop(ArrayStack *stack) {\n    int val = peek(stack);\n    stack->size--;\n    return val;\n}\n
array_stack.kt
/* 配列ベースのスタック */\nclass ArrayStack {\n    // リスト(動的配列)を初期化する\n    private val stack = mutableListOf<Int>()\n\n    /* スタックの長さを取得 */\n    fun size(): Int {\n        return stack.size\n    }\n\n    /* スタックが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* プッシュ */\n    fun push(num: Int) {\n        stack.add(num)\n    }\n\n    /* ポップ */\n    fun pop(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack.removeAt(size() - 1)\n    }\n\n    /* スタックトップの要素にアクセス */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack[size() - 1]\n    }\n\n    /* List を Array に変換して返す */\n    fun toArray(): Array<Any> {\n        return stack.toTypedArray()\n    }\n}\n
array_stack.rb
### 配列で実装したスタック ###\nclass ArrayStack\n  ### コンストラクタ ###\n  def initialize\n    @stack = []\n  end\n\n  ### スタックの長さを取得 ###\n  def size\n    @stack.length\n  end\n\n  ### スタックが空か判定 ###\n  def is_empty?\n    @stack.empty?\n  end\n\n  ### プッシュ ###\n  def push(item)\n    @stack << item\n  end\n\n  ### ポップ ###\n  def pop\n    raise IndexError, 'スタックは空です' if is_empty?\n\n    @stack.pop\n  end\n\n  ### スタックトップ要素を参照 ###\n  def peek\n    raise IndexError, 'スタックは空です' if is_empty?\n\n    @stack.last\n  end\n\n  ### 表示用のリストを返す ###\n  def to_array\n    @stack\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#513-2","level":2,"title":"5.1.3   2つの実装の比較","text":"

対応する操作

どちらの実装も、スタックの定義に含まれる各種操作をサポートします。配列ベースの実装はランダムアクセスも可能ですが、これはスタックの定義範囲を超えているため、通常は利用しません。

時間効率

配列ベースの実装では、プッシュとポップの両方があらかじめ確保された連続メモリ上で行われるため、キャッシュ局所性が高く、効率に優れます。ただし、プッシュ時に配列容量を超えると拡張処理が発生し、その1回のプッシュの時間計算量は \\(O(n)\\) になります。

連結リストベースの実装では、サイズ拡張が非常に柔軟であり、前述のような配列拡張による効率低下はありません。ただし、プッシュ時にはノードオブジェクトの初期化とポインタの更新が必要になるため、効率は相対的に低くなります。もっとも、プッシュする要素自体がノードオブジェクトであれば、初期化の手間を省けるため、効率を高められます。

以上を踏まえると、プッシュおよびポップの対象が intdouble のような基本データ型である場合、次の結論が得られます。

  • 配列ベースのスタックは拡張時に効率が低下しますが、拡張は低頻度の操作であるため、平均効率はより高くなります。
  • 連結リストベースのスタックは、より安定した効率を提供できます。

空間効率

リストを初期化するとき、システムは「初期容量」を割り当てますが、この容量は実際の必要量を上回ることがあります。また、拡張は通常、一定の倍率(たとえば2倍)で行われるため、拡張後の容量も実際の必要量を超える可能性があります。したがって、配列ベースのスタックは一定のメモリ浪費を招く可能性があります。

一方で、連結リストのノードはポインタを追加で保持する必要があるため、連結リストノードは相対的に大きな領域を占有します。

以上より、どちらの実装がより省メモリかを単純に断定することはできず、具体的な状況に応じて分析する必要があります。

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   スタックの典型的な応用","text":"
  • ブラウザにおける戻ると進む、ソフトウェアにおける取り消しとやり直し。新しいWebページを開くたびに、ブラウザは直前のページをスタックにプッシュするため、戻る操作によって前のページに戻れます。戻る操作は実際にはポップに相当します。戻ると進むを同時にサポートするには、2つのスタックを組み合わせて実現する必要があります。
  • プログラムのメモリ管理。関数を呼び出すたびに、システムはスタックトップにスタックフレームを追加し、関数のコンテキスト情報を記録します。再帰関数では、下向きに再帰していく段階でプッシュが繰り返され、上向きにバックトラックする段階でポップが繰り返されます。
","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/summary/","level":1,"title":"5.4   まとめ","text":"","path":["第 5 章   スタックとキュー","5.4   まとめ"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • スタックは後入れ先出しの原則に従うデータ構造であり、配列または連結リストで実装できます。
  • 時間効率の面では、スタックの配列実装は平均効率が高い一方、拡張時には 1 回のプッシュ操作の時間計算量が \\(O(n)\\) まで悪化します。これに対して、スタックの連結リスト実装はより安定した効率を示します。
  • 空間効率の面では、スタックの配列実装はある程度の領域の無駄を生む可能性があります。ただし、連結リストのノードが占有するメモリは配列要素よりも大きい点に注意が必要です。
  • キューは先入れ先出しの原則に従うデータ構造であり、同様に配列または連結リストで実装できます。時間効率と空間効率の比較における結論は、前述のスタックの場合と似ています。
  • 両端キューはより高い自由度を持つキューであり、両端で要素の追加と削除を行えます。
","path":["第 5 章   スタックとキュー","5.4   まとめ"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:ブラウザの進む・戻るは双方向連結リストで実装されているのですか?

ブラウザの進む・戻る機能の本質は「スタック」の表れです。ユーザーが新しいページにアクセスすると、そのページはスタックの先頭に追加されます。ユーザーが戻るボタンをクリックすると、そのページはスタックの先頭から取り出されます。両端キューを使うといくつかの追加操作を簡単に実装でき、この点は「両端キュー」の章で触れています。

Q:ポップした後、そのノードのメモリを解放する必要はありますか?

後で取り出したノードを引き続き使うのであれば、メモリを解放する必要はありません。以降そのノードを使わない場合でも、JavaPython などの言語には自動ガベージコレクション機構があるため、手動でメモリを解放する必要はありません。一方、CC++ では手動でメモリを解放する必要があります。

Q:両端キューは 2 つのスタックをつなげたように見えますが、用途は何ですか?

両端キューは、スタックとキューの組み合わせ、あるいは 2 つのスタックをつなげたもののような構造です。表しているのはスタック + キューのロジックなので、スタックとキューのすべての応用を実現でき、しかもより柔軟です。

Q:取り消し(undo)とやり直し(redo)は具体的にどのように実装されますか?

2 つのスタックを使い、スタック A を取り消し用、スタック B をやり直し用に使います。

  1. ユーザーが操作を 1 つ実行するたびに、その操作をスタック A にプッシュし、スタック B を空にします。
  2. ユーザーが「取り消し」を実行したときは、スタック A から直近の操作をポップし、それをスタック B にプッシュします。
  3. ユーザーが「やり直し」を実行したときは、スタック B から直近の操作をポップし、それをスタック A にプッシュします。
","path":["第 5 章   スタックとキュー","5.4   まとめ"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"第 7 章   木","text":"

Abstract

大樹は生命力に満ち、根は深く葉は生い茂り、枝は豊かに広がる。

それはデータ分割統治の生き生きとした姿を私たちに示してくれる。

","path":["第 7 章   木"],"tags":[]},{"location":"chapter_tree/#_1","level":2,"title":"章の内容","text":"
  • 7.1   二分木
  • 7.2   二分木の走査
  • 7.3   二分木の配列表現
  • 7.4   二分探索木
  • 7.5   AVL 木 *
  • 7.6   まとめ
","path":["第 7 章   木"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/","level":1,"title":"7.3   二分木の配列表現","text":"

連結リスト表現では、二分木の記憶単位はノード TreeNode であり、ノード同士はポインタによって接続されます。前節では、連結リスト表現における二分木の各種基本操作を紹介しました。

では、配列で二分木を表現できるでしょうか?答えはもちろん可能です。

","path":["第 7 章   木","7.3   二分木の配列表現"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#731","level":2,"title":"7.3.1   充足二分木を表現する","text":"

まずは簡単な例を考えます。与えられた 1 本の充足二分木について、すべてのノードをレベル順走査の順に配列へ格納すると、各ノードは一意な配列インデックスに対応します。

レベル順走査の性質に基づくと、親ノードのインデックスと子ノードのインデックスの間にある「対応式」を導けます。あるノードのインデックスが \\(i\\) なら、その左子ノードのインデックスは \\(2i + 1\\) 、右子ノードのインデックスは \\(2i + 2\\) です。以下の図は、各ノードインデックス間の対応関係を示しています。

図 7-12   充足二分木の配列表現

対応式は、連結リストにおけるノード参照(ポインタ)と同じ役割を果たします。与えられた配列内の任意のノードについて、この対応式を使えばその左(右)子ノードにアクセスできます。

","path":["第 7 章   木","7.3   二分木の配列表現"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732","level":2,"title":"7.3.2   任意の二分木を表現する","text":"

充足二分木は特殊なケースであり、一般の二分木では中間層に多数の None が存在することがよくあります。レベル順走査の列にはこれらの None が含まれないため、その列だけから None の数や分布位置を推定することはできません。つまり、このレベル順走査列に一致する二分木構造は複数存在し得ます。

次の図のように、非充足二分木が与えられると、上記の配列表現はすでに成り立ちません。

図 7-13   レベル順走査列に対応する複数の二分木の可能性

この問題を解決するために、**レベル順走査列にすべての None を明示的に書き込む**ことを考えられます。次の図のように、このように処理すればレベル順走査列で二分木を一意に表現できます。コード例は以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# 二分木の配列表現\n# 空き位置を表すために None を使う\ntree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]\n
/* 二分木の配列表現 */\n// int の最大値 INT_MAX を使って空き位置を示す\nvector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
/* 二分木の配列表現 */\n// int のラッパークラス Integer を使えば、null で空き位置を示せる\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
/* 二分木の配列表現 */\n// nullable な int? 型を使えば、null で空き位置を示せる\nint?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二分木の配列表現 */\n// any 型のスライスを使えば、nil で空き位置を示せる\ntree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}\n
/* 二分木の配列表現 */\n// nullable な Int? 型を使えば、nil で空き位置を示せる\nlet tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n
/* 二分木の配列表現 */\n// null を使って空き位置を表す\nlet tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二分木の配列表現 */\n// null を使って空き位置を表す\nlet tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二分木の配列表現 */\n// nullable な int? 型を使えば、null で空き位置を示せる\nList<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二分木の配列表現 */\n// None を使って空き位置を示す\nlet tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];\n
/* 二分木の配列表現 */\n// int の最大値で空き位置を示すため、ノード値は INT_MAX であってはならない\nint tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
/* 二分木の配列表現 */\n// null を使って空き位置を表す\nval tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )\n
### 二分木の配列表現 ###\n# nil を使って空き位置を表す\ntree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n

図 7-14   任意の二分木の配列表現

補足すると、完全二分木は配列による表現に非常に適しています。完全二分木の定義を振り返ると、None は最下層の右側にしか現れないため、すべての None は必ずレベル順走査列の末尾に現れます。

つまり、完全二分木を配列で表す場合は、すべての None の格納を省略できるため、非常に便利です。次の図に例を示します。

図 7-15   完全二分木の配列表現

以下のコードでは、配列ベースで表現した二分木を実装しており、次の操作を含みます。

  • あるノードが与えられたとき、その値、左(右)子ノード、親ノードを取得する。
  • 前順走査、中順走査、後順走査、レベル順走査の列を取得する。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_binary_tree.py
class ArrayBinaryTree:\n    \"\"\"配列表現による二分木クラス\"\"\"\n\n    def __init__(self, arr: list[int | None]):\n        \"\"\"コンストラクタ\"\"\"\n        self._tree = list(arr)\n\n    def size(self):\n        \"\"\"リスト容量\"\"\"\n        return len(self._tree)\n\n    def val(self, i: int) -> int | None:\n        \"\"\"インデックス i のノードの値を取得\"\"\"\n        # インデックスが範囲外なら、空きを表す None を返す\n        if i < 0 or i >= self.size():\n            return None\n        return self._tree[i]\n\n    def left(self, i: int) -> int | None:\n        \"\"\"インデックス i のノードの左子ノードのインデックスを取得\"\"\"\n        return 2 * i + 1\n\n    def right(self, i: int) -> int | None:\n        \"\"\"インデックス i のノードの右子ノードのインデックスを取得\"\"\"\n        return 2 * i + 2\n\n    def parent(self, i: int) -> int | None:\n        \"\"\"インデックス i のノードの親ノードのインデックスを取得\"\"\"\n        return (i - 1) // 2\n\n    def level_order(self) -> list[int]:\n        \"\"\"レベル順走査\"\"\"\n        self.res = []\n        # 配列を直接走査する\n        for i in range(self.size()):\n            if self.val(i) is not None:\n                self.res.append(self.val(i))\n        return self.res\n\n    def dfs(self, i: int, order: str):\n        \"\"\"深さ優先探索\"\"\"\n        if self.val(i) is None:\n            return\n        # 先行順走査\n        if order == \"pre\":\n            self.res.append(self.val(i))\n        self.dfs(self.left(i), order)\n        # 中順走査\n        if order == \"in\":\n            self.res.append(self.val(i))\n        self.dfs(self.right(i), order)\n        # 後順走査\n        if order == \"post\":\n            self.res.append(self.val(i))\n\n    def pre_order(self) -> list[int]:\n        \"\"\"先行順走査\"\"\"\n        self.res = []\n        self.dfs(0, order=\"pre\")\n        return self.res\n\n    def in_order(self) -> list[int]:\n        \"\"\"中順走査\"\"\"\n        self.res = []\n        self.dfs(0, order=\"in\")\n        return self.res\n\n    def post_order(self) -> list[int]:\n        \"\"\"後順走査\"\"\"\n        self.res = []\n        self.dfs(0, order=\"post\")\n        return self.res\n
array_binary_tree.cpp
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n  public:\n    /* コンストラクタ */\n    ArrayBinaryTree(vector<int> arr) {\n        tree = arr;\n    }\n\n    /* リスト容量 */\n    int size() {\n        return tree.size();\n    }\n\n    /* インデックス i のノードの値を取得 */\n    int val(int i) {\n        // インデックスが範囲外なら、空きを表す INT_MAX を返す\n        if (i < 0 || i >= size())\n            return INT_MAX;\n        return tree[i];\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    int left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    int right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    int parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* レベル順走査 */\n    vector<int> levelOrder() {\n        vector<int> res;\n        // 配列を直接走査する\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != INT_MAX)\n                res.push_back(val(i));\n        }\n        return res;\n    }\n\n    /* 先行順走査 */\n    vector<int> preOrder() {\n        vector<int> res;\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* 中順走査 */\n    vector<int> inOrder() {\n        vector<int> res;\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* 後順走査 */\n    vector<int> postOrder() {\n        vector<int> res;\n        dfs(0, \"post\", res);\n        return res;\n    }\n\n  private:\n    vector<int> tree;\n\n    /* 深さ優先探索 */\n    void dfs(int i, string order, vector<int> &res) {\n        // 空きスロットなら返す\n        if (val(i) == INT_MAX)\n            return;\n        // 先行順走査\n        if (order == \"pre\")\n            res.push_back(val(i));\n        dfs(left(i), order, res);\n        // 中順走査\n        if (order == \"in\")\n            res.push_back(val(i));\n        dfs(right(i), order, res);\n        // 後順走査\n        if (order == \"post\")\n            res.push_back(val(i));\n    }\n};\n
array_binary_tree.java
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n    private List<Integer> tree;\n\n    /* コンストラクタ */\n    public ArrayBinaryTree(List<Integer> arr) {\n        tree = new ArrayList<>(arr);\n    }\n\n    /* リスト容量 */\n    public int size() {\n        return tree.size();\n    }\n\n    /* インデックス i のノードの値を取得 */\n    public Integer val(int i) {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if (i < 0 || i >= size())\n            return null;\n        return tree.get(i);\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    public Integer left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    public Integer right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    public Integer parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* レベル順走査 */\n    public List<Integer> levelOrder() {\n        List<Integer> res = new ArrayList<>();\n        // 配列を直接走査する\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != null)\n                res.add(val(i));\n        }\n        return res;\n    }\n\n    /* 深さ優先探索 */\n    private void dfs(Integer i, String order, List<Integer> res) {\n        // 空きスロットなら返す\n        if (val(i) == null)\n            return;\n        // 先行順走査\n        if (\"pre\".equals(order))\n            res.add(val(i));\n        dfs(left(i), order, res);\n        // 中順走査\n        if (\"in\".equals(order))\n            res.add(val(i));\n        dfs(right(i), order, res);\n        // 後順走査\n        if (\"post\".equals(order))\n            res.add(val(i));\n    }\n\n    /* 先行順走査 */\n    public List<Integer> preOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* 中順走査 */\n    public List<Integer> inOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* 後順走査 */\n    public List<Integer> postOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"post\", res);\n        return res;\n    }\n}\n
array_binary_tree.cs
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree(List<int?> arr) {\n    List<int?> tree = new(arr);\n\n    /* リスト容量 */\n    public int Size() {\n        return tree.Count;\n    }\n\n    /* インデックス i のノードの値を取得 */\n    public int? Val(int i) {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if (i < 0 || i >= Size())\n            return null;\n        return tree[i];\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    public int Left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    public int Right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    public int Parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* レベル順走査 */\n    public List<int> LevelOrder() {\n        List<int> res = [];\n        // 配列を直接走査する\n        for (int i = 0; i < Size(); i++) {\n            if (Val(i).HasValue)\n                res.Add(Val(i)!.Value);\n        }\n        return res;\n    }\n\n    /* 深さ優先探索 */\n    void DFS(int i, string order, List<int> res) {\n        // 空きスロットなら返す\n        if (!Val(i).HasValue)\n            return;\n        // 先行順走査\n        if (order == \"pre\")\n            res.Add(Val(i)!.Value);\n        DFS(Left(i), order, res);\n        // 中順走査\n        if (order == \"in\")\n            res.Add(Val(i)!.Value);\n        DFS(Right(i), order, res);\n        // 後順走査\n        if (order == \"post\")\n            res.Add(Val(i)!.Value);\n    }\n\n    /* 先行順走査 */\n    public List<int> PreOrder() {\n        List<int> res = [];\n        DFS(0, \"pre\", res);\n        return res;\n    }\n\n    /* 中順走査 */\n    public List<int> InOrder() {\n        List<int> res = [];\n        DFS(0, \"in\", res);\n        return res;\n    }\n\n    /* 後順走査 */\n    public List<int> PostOrder() {\n        List<int> res = [];\n        DFS(0, \"post\", res);\n        return res;\n    }\n}\n
array_binary_tree.go
/* 配列表現による二分木クラス */\ntype arrayBinaryTree struct {\n    tree []any\n}\n\n/* コンストラクタ */\nfunc newArrayBinaryTree(arr []any) *arrayBinaryTree {\n    return &arrayBinaryTree{\n        tree: arr,\n    }\n}\n\n/* リスト容量 */\nfunc (abt *arrayBinaryTree) size() int {\n    return len(abt.tree)\n}\n\n/* インデックス i のノードの値を取得 */\nfunc (abt *arrayBinaryTree) val(i int) any {\n    // インデックスが範囲外なら、空きを表す null を返す\n    if i < 0 || i >= abt.size() {\n        return nil\n    }\n    return abt.tree[i]\n}\n\n/* インデックス i のノードの左子ノードのインデックスを取得 */\nfunc (abt *arrayBinaryTree) left(i int) int {\n    return 2*i + 1\n}\n\n/* インデックス i のノードの右子ノードのインデックスを取得 */\nfunc (abt *arrayBinaryTree) right(i int) int {\n    return 2*i + 2\n}\n\n/* インデックス i のノードの親ノードのインデックスを取得 */\nfunc (abt *arrayBinaryTree) parent(i int) int {\n    return (i - 1) / 2\n}\n\n/* レベル順走査 */\nfunc (abt *arrayBinaryTree) levelOrder() []any {\n    var res []any\n    // 配列を直接走査する\n    for i := 0; i < abt.size(); i++ {\n        if abt.val(i) != nil {\n            res = append(res, abt.val(i))\n        }\n    }\n    return res\n}\n\n/* 深さ優先探索 */\nfunc (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) {\n    // 空きスロットなら返す\n    if abt.val(i) == nil {\n        return\n    }\n    // 先行順走査\n    if order == \"pre\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.left(i), order, res)\n    // 中順走査\n    if order == \"in\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.right(i), order, res)\n    // 後順走査\n    if order == \"post\" {\n        *res = append(*res, abt.val(i))\n    }\n}\n\n/* 先行順走査 */\nfunc (abt *arrayBinaryTree) preOrder() []any {\n    var res []any\n    abt.dfs(0, \"pre\", &res)\n    return res\n}\n\n/* 中順走査 */\nfunc (abt *arrayBinaryTree) inOrder() []any {\n    var res []any\n    abt.dfs(0, \"in\", &res)\n    return res\n}\n\n/* 後順走査 */\nfunc (abt *arrayBinaryTree) postOrder() []any {\n    var res []any\n    abt.dfs(0, \"post\", &res)\n    return res\n}\n
array_binary_tree.swift
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n    private var tree: [Int?]\n\n    /* コンストラクタ */\n    init(arr: [Int?]) {\n        tree = arr\n    }\n\n    /* リスト容量 */\n    func size() -> Int {\n        tree.count\n    }\n\n    /* インデックス i のノードの値を取得 */\n    func val(i: Int) -> Int? {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if i < 0 || i >= size() {\n            return nil\n        }\n        return tree[i]\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    func left(i: Int) -> Int {\n        2 * i + 1\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    func right(i: Int) -> Int {\n        2 * i + 2\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    func parent(i: Int) -> Int {\n        (i - 1) / 2\n    }\n\n    /* レベル順走査 */\n    func levelOrder() -> [Int] {\n        var res: [Int] = []\n        // 配列を直接走査する\n        for i in 0 ..< size() {\n            if let val = val(i: i) {\n                res.append(val)\n            }\n        }\n        return res\n    }\n\n    /* 深さ優先探索 */\n    private func dfs(i: Int, order: String, res: inout [Int]) {\n        // 空きスロットなら返す\n        guard let val = val(i: i) else {\n            return\n        }\n        // 先行順走査\n        if order == \"pre\" {\n            res.append(val)\n        }\n        dfs(i: left(i: i), order: order, res: &res)\n        // 中順走査\n        if order == \"in\" {\n            res.append(val)\n        }\n        dfs(i: right(i: i), order: order, res: &res)\n        // 後順走査\n        if order == \"post\" {\n            res.append(val)\n        }\n    }\n\n    /* 先行順走査 */\n    func preOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"pre\", res: &res)\n        return res\n    }\n\n    /* 中順走査 */\n    func inOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"in\", res: &res)\n        return res\n    }\n\n    /* 後順走査 */\n    func postOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"post\", res: &res)\n        return res\n    }\n}\n
array_binary_tree.js
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n    #tree;\n\n    /* コンストラクタ */\n    constructor(arr) {\n        this.#tree = arr;\n    }\n\n    /* リスト容量 */\n    size() {\n        return this.#tree.length;\n    }\n\n    /* インデックス i のノードの値を取得 */\n    val(i) {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    left(i) {\n        return 2 * i + 1;\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    right(i) {\n        return 2 * i + 2;\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    parent(i) {\n        return Math.floor((i - 1) / 2); // 切り捨て除算\n    }\n\n    /* レベル順走査 */\n    levelOrder() {\n        let res = [];\n        // 配列を直接走査する\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* 深さ優先探索 */\n    #dfs(i, order, res) {\n        // 空きスロットなら返す\n        if (this.val(i) === null) return;\n        // 先行順走査\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // 中順走査\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // 後順走査\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* 先行順走査 */\n    preOrder() {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* 中順走査 */\n    inOrder() {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* 後順走査 */\n    postOrder() {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
array_binary_tree.ts
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n    #tree: (number | null)[];\n\n    /* コンストラクタ */\n    constructor(arr: (number | null)[]) {\n        this.#tree = arr;\n    }\n\n    /* リスト容量 */\n    size(): number {\n        return this.#tree.length;\n    }\n\n    /* インデックス i のノードの値を取得 */\n    val(i: number): number | null {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    left(i: number): number {\n        return 2 * i + 1;\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    right(i: number): number {\n        return 2 * i + 2;\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    parent(i: number): number {\n        return Math.floor((i - 1) / 2); // 切り捨て除算\n    }\n\n    /* レベル順走査 */\n    levelOrder(): number[] {\n        let res = [];\n        // 配列を直接走査する\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* 深さ優先探索 */\n    #dfs(i: number, order: Order, res: (number | null)[]): void {\n        // 空きスロットなら返す\n        if (this.val(i) === null) return;\n        // 先行順走査\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // 中順走査\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // 後順走査\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* 先行順走査 */\n    preOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* 中順走査 */\n    inOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* 後順走査 */\n    postOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
array_binary_tree.dart
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n  late List<int?> _tree;\n\n  /* コンストラクタ */\n  ArrayBinaryTree(this._tree);\n\n  /* リスト容量 */\n  int size() {\n    return _tree.length;\n  }\n\n  /* インデックス i のノードの値を取得 */\n  int? val(int i) {\n    // インデックスが範囲外なら、空きを表す null を返す\n    if (i < 0 || i >= size()) {\n      return null;\n    }\n    return _tree[i];\n  }\n\n  /* インデックス i のノードの左子ノードのインデックスを取得 */\n  int? left(int i) {\n    return 2 * i + 1;\n  }\n\n  /* インデックス i のノードの右子ノードのインデックスを取得 */\n  int? right(int i) {\n    return 2 * i + 2;\n  }\n\n  /* インデックス i のノードの親ノードのインデックスを取得 */\n  int? parent(int i) {\n    return (i - 1) ~/ 2;\n  }\n\n  /* レベル順走査 */\n  List<int> levelOrder() {\n    List<int> res = [];\n    for (int i = 0; i < size(); i++) {\n      if (val(i) != null) {\n        res.add(val(i)!);\n      }\n    }\n    return res;\n  }\n\n  /* 深さ優先探索 */\n  void dfs(int i, String order, List<int?> res) {\n    // 空きスロットなら返す\n    if (val(i) == null) {\n      return;\n    }\n    // 先行順走査\n    if (order == 'pre') {\n      res.add(val(i));\n    }\n    dfs(left(i)!, order, res);\n    // 中順走査\n    if (order == 'in') {\n      res.add(val(i));\n    }\n    dfs(right(i)!, order, res);\n    // 後順走査\n    if (order == 'post') {\n      res.add(val(i));\n    }\n  }\n\n  /* 先行順走査 */\n  List<int?> preOrder() {\n    List<int?> res = [];\n    dfs(0, 'pre', res);\n    return res;\n  }\n\n  /* 中順走査 */\n  List<int?> inOrder() {\n    List<int?> res = [];\n    dfs(0, 'in', res);\n    return res;\n  }\n\n  /* 後順走査 */\n  List<int?> postOrder() {\n    List<int?> res = [];\n    dfs(0, 'post', res);\n    return res;\n  }\n}\n
array_binary_tree.rs
/* 配列表現による二分木クラス */\nstruct ArrayBinaryTree {\n    tree: Vec<Option<i32>>,\n}\n\nimpl ArrayBinaryTree {\n    /* コンストラクタ */\n    fn new(arr: Vec<Option<i32>>) -> Self {\n        Self { tree: arr }\n    }\n\n    /* リスト容量 */\n    fn size(&self) -> i32 {\n        self.tree.len() as i32\n    }\n\n    /* インデックス i のノードの値を取得 */\n    fn val(&self, i: i32) -> Option<i32> {\n        // インデックスが範囲外なら、空きを表す None を返す\n        if i < 0 || i >= self.size() {\n            None\n        } else {\n            self.tree[i as usize]\n        }\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    fn left(&self, i: i32) -> i32 {\n        2 * i + 1\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    fn right(&self, i: i32) -> i32 {\n        2 * i + 2\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    fn parent(&self, i: i32) -> i32 {\n        (i - 1) / 2\n    }\n\n    /* レベル順走査 */\n    fn level_order(&self) -> Vec<i32> {\n        self.tree.iter().filter_map(|&x| x).collect()\n    }\n\n    /* 深さ優先探索 */\n    fn dfs(&self, i: i32, order: &'static str, res: &mut Vec<i32>) {\n        if self.val(i).is_none() {\n            return;\n        }\n        let val = self.val(i).unwrap();\n        // 先行順走査\n        if order == \"pre\" {\n            res.push(val);\n        }\n        self.dfs(self.left(i), order, res);\n        // 中順走査\n        if order == \"in\" {\n            res.push(val);\n        }\n        self.dfs(self.right(i), order, res);\n        // 後順走査\n        if order == \"post\" {\n            res.push(val);\n        }\n    }\n\n    /* 先行順走査 */\n    fn pre_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"pre\", &mut res);\n        res\n    }\n\n    /* 中順走査 */\n    fn in_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"in\", &mut res);\n        res\n    }\n\n    /* 後順走査 */\n    fn post_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"post\", &mut res);\n        res\n    }\n}\n
array_binary_tree.c
/* 配列表現による二分木の構造体 */\ntypedef struct {\n    int *tree;\n    int size;\n} ArrayBinaryTree;\n\n/* コンストラクタ */\nArrayBinaryTree *newArrayBinaryTree(int *arr, int arrSize) {\n    ArrayBinaryTree *abt = (ArrayBinaryTree *)malloc(sizeof(ArrayBinaryTree));\n    abt->tree = malloc(sizeof(int) * arrSize);\n    memcpy(abt->tree, arr, sizeof(int) * arrSize);\n    abt->size = arrSize;\n    return abt;\n}\n\n/* デストラクタ */\nvoid delArrayBinaryTree(ArrayBinaryTree *abt) {\n    free(abt->tree);\n    free(abt);\n}\n\n/* リスト容量 */\nint size(ArrayBinaryTree *abt) {\n    return abt->size;\n}\n\n/* インデックス i のノードの値を取得 */\nint val(ArrayBinaryTree *abt, int i) {\n    // インデックスが範囲外なら、空きを表す INT_MAX を返す\n    if (i < 0 || i >= size(abt))\n        return INT_MAX;\n    return abt->tree[i];\n}\n\n/* レベル順走査 */\nint *levelOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    // 配列を直接走査する\n    for (int i = 0; i < size(abt); i++) {\n        if (val(abt, i) != INT_MAX)\n            res[index++] = val(abt, i);\n    }\n    *returnSize = index;\n    return res;\n}\n\n/* 深さ優先探索 */\nvoid dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) {\n    // 空きスロットなら返す\n    if (val(abt, i) == INT_MAX)\n        return;\n    // 先行順走査\n    if (strcmp(order, \"pre\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, left(i), order, res, index);\n    // 中順走査\n    if (strcmp(order, \"in\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, right(i), order, res, index);\n    // 後順走査\n    if (strcmp(order, \"post\") == 0)\n        res[(*index)++] = val(abt, i);\n}\n\n/* 先行順走査 */\nint *preOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"pre\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* 中順走査 */\nint *inOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"in\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* 後順走査 */\nint *postOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"post\", res, &index);\n    *returnSize = index;\n    return res;\n}\n
array_binary_tree.kt
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree(val tree: MutableList<Int?>) {\n    /* リスト容量 */\n    fun size(): Int {\n        return tree.size\n    }\n\n    /* インデックス i のノードの値を取得 */\n    fun _val(i: Int): Int? {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if (i < 0 || i >= size()) return null\n        return tree[i]\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    fun parent(i: Int): Int {\n        return (i - 1) / 2\n    }\n\n    /* レベル順走査 */\n    fun levelOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        // 配列を直接走査する\n        for (i in 0..<size()) {\n            if (_val(i) != null)\n                res.add(_val(i))\n        }\n        return res\n    }\n\n    /* 深さ優先探索 */\n    fun dfs(i: Int, order: String, res: MutableList<Int?>) {\n        // 空きスロットなら返す\n        if (_val(i) == null)\n            return\n        // 先行順走査\n        if (\"pre\" == order)\n            res.add(_val(i))\n        dfs(left(i), order, res)\n        // 中順走査\n        if (\"in\" == order)\n            res.add(_val(i))\n        dfs(right(i), order, res)\n        // 後順走査\n        if (\"post\" == order)\n            res.add(_val(i))\n    }\n\n    /* 先行順走査 */\n    fun preOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"pre\", res)\n        return res\n    }\n\n    /* 中順走査 */\n    fun inOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"in\", res)\n        return res\n    }\n\n    /* 後順走査 */\n    fun postOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"post\", res)\n        return res\n    }\n}\n
array_binary_tree.rb
### 配列表現の二分木クラス ###\nclass ArrayBinaryTree\n  ### コンストラクタ ###\n  def initialize(arr)\n    @tree = arr.to_a\n  end\n\n  ### リスト容量 ###\n  def size\n    @tree.length\n  end\n\n  ### インデックス i のノードの値を取得 ###\n  def val(i)\n    # インデックスが範囲外なら `nil` を返し、空きスロットを表す\n    return if i < 0 || i >= size\n\n    @tree[i]\n  end\n\n  ### インデックス i のノードの左子ノードのインデックスを取得 ###\n  def left(i)\n    2 * i + 1\n  end\n\n  ### インデックス i のノードの右子ノードのインデックスを取得 ###\n  def right(i)\n    2 * i + 2\n  end\n\n  ### インデックス i のノードの親ノードのインデックスを取得 ###\n  def parent(i)\n    (i - 1) / 2\n  end\n\n  ### レベル順走査 ###\n  def level_order\n    @res = []\n\n    # 配列を直接走査する\n    for i in 0...size\n      @res << val(i) unless val(i).nil?\n    end\n\n    @res\n  end\n\n  ### 深さ優先探索 ###\n  def dfs(i, order)\n    return if val(i).nil?\n    # 先行順走査\n    @res << val(i) if order == :pre\n    dfs(left(i), order)\n    # 中順走査\n    @res << val(i) if order == :in\n    dfs(right(i), order)\n    # 後順走査\n    @res << val(i) if order == :post\n  end\n\n  ### 前順走査 ###\n  def pre_order\n    @res = []\n    dfs(0, :pre)\n    @res\n  end\n\n  ### 中順走査 ###\n  def in_order\n    @res = []\n    dfs(0, :in)\n    @res\n  end\n\n  ### 後順走査 ###\n  def post_order\n    @res = []\n    dfs(0, :post)\n    @res\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 7 章   木","7.3   二分木の配列表現"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#733","level":2,"title":"7.3.3   利点と制約","text":"

二分木の配列表現には主に次の利点があります。

  • 配列は連続したメモリ空間に格納されるため、キャッシュ効率が高く、アクセスと走査が速い。
  • ポインタを格納する必要がなく、比較的省スペースである。
  • ノードへのランダムアクセスが可能である。

ただし、配列表現にはいくつかの制約もあります。

  • 配列による格納には連続したメモリ空間が必要なため、データ量が大きすぎる木の格納には向かない。
  • ノードの追加と削除は配列の挿入・削除操作で実現する必要があり、効率は低い。
  • 二分木に大量の None が存在すると、配列に占める実ノードデータの比率が低くなり、空間利用率も低下する。
","path":["第 7 章   木","7.3   二分木の配列表現"],"tags":[]},{"location":"chapter_tree/avl_tree/","level":1,"title":"7.5   AVL 木 *","text":"

「二分探索木」章で述べたように、挿入と削除を何度も繰り返すと、二分探索木は連結リストへ退化する可能性があります。この場合、すべての操作の時間計算量は \\(O(\\log n)\\) から \\(O(n)\\) へ劣化します。

以下の図に示すように、ノード削除を 2 回行うと、この二分探索木は連結リストへ退化します。

図 7-24   AVL 木がノード削除後に退化する

別の例として、以下の図に示す完全二分木に 2 つのノードを挿入すると、木は大きく左に傾き、探索操作の時間計算量もそれに伴って劣化します。

図 7-25   AVL 木がノード挿入後に退化する

1962 年、G. M. Adelson-Velsky と E. M. Landis は論文“An algorithm for the organization of information”の中で AVL 木 を提案しました。論文では一連の操作が詳しく説明されており、ノードの追加と削除を続けても AVL 木が退化しないようにして、各種操作の時間計算量を \\(O(\\log n)\\) の水準に保ちます。言い換えると、追加・削除・探索・更新を頻繁に行う場面でも、AVL 木は常に高いデータ操作性能を維持でき、実用価値の高い構造です。

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#751-avl","level":2,"title":"7.5.1   AVL 木の基本用語","text":"

AVL 木は二分探索木であると同時に平衡二分木でもあり、これら 2 種類の二分木の性質をすべて満たします。したがって、平衡二分探索木(balanced binary search tree)の一種です。

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1","level":3,"title":"1.   ノードの高さ","text":"

AVL 木の操作ではノードの高さを取得する必要があるため、ノードクラスに height 変数を追加します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class TreeNode:\n    \"\"\"AVL 木ノードクラス\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                 # ノード値\n        self.height: int = 0                # ノードの高さ\n        self.left: TreeNode | None = None   # 左の子ノード参照\n        self.right: TreeNode | None = None  # 右の子ノード参照\n
/* AVL 木ノードクラス */\nstruct TreeNode {\n    int val{};          // ノード値\n    int height = 0;     // ノードの高さ\n    TreeNode *left{};   // 左の子ノード\n    TreeNode *right{};  // 右の子ノード\n    TreeNode() = default;\n    explicit TreeNode(int x) : val(x){}\n};\n
/* AVL 木ノードクラス */\nclass TreeNode {\n    public int val;        // ノード値\n    public int height;     // ノードの高さ\n    public TreeNode left;  // 左の子ノード\n    public TreeNode right; // 右の子ノード\n    public TreeNode(int x) { val = x; }\n}\n
/* AVL 木ノードクラス */\nclass TreeNode(int? x) {\n    public int? val = x;    // ノード値\n    public int height;      // ノードの高さ\n    public TreeNode? left;  // 左の子ノード参照\n    public TreeNode? right; // 右の子ノード参照\n}\n
/* AVL 木ノード構造体 */\ntype TreeNode struct {\n    Val    int       // ノード値\n    Height int       // ノードの高さ\n    Left   *TreeNode // 左の子ノード参照\n    Right  *TreeNode // 右の子ノード参照\n}\n
/* AVL 木ノードクラス */\nclass TreeNode {\n    var val: Int // ノード値\n    var height: Int // ノードの高さ\n    var left: TreeNode? // 左の子ノード\n    var right: TreeNode? // 右の子ノード\n\n    init(x: Int) {\n        val = x\n        height = 0\n    }\n}\n
/* AVL 木ノードクラス */\nclass TreeNode {\n    val; // ノード値\n    height; //ノードの高さ\n    left; // 左の子ノードポインタ\n    right; // 右の子ノードポインタ\n    constructor(val, left, right, height) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* AVL 木ノードクラス */\nclass TreeNode {\n    val: number;            // ノード値\n    height: number;         // ノードの高さ\n    left: TreeNode | null;  // 左の子ノードポインタ\n    right: TreeNode | null; // 右の子ノードポインタ\n    constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* AVL 木ノードクラス */\nclass TreeNode {\n  int val;         // ノード値\n  int height;      // ノードの高さ\n  TreeNode? left;  // 左の子ノード\n  TreeNode? right; // 右の子ノード\n  TreeNode(this.val, [this.height = 0, this.left, this.right]);\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* AVL 木ノード構造体 */\nstruct TreeNode {\n    val: i32,                               // ノード値\n    height: i32,                            // ノードの高さ\n    left: Option<Rc<RefCell<TreeNode>>>,    // 左の子ノード\n    right: Option<Rc<RefCell<TreeNode>>>,   // 右の子ノード\n}\n\nimpl TreeNode {\n    /* コンストラクタ */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            height: 0,\n            left: None,\n            right: None\n        }))\n    }\n}\n
/* AVL 木ノード構造体 */\ntypedef struct TreeNode {\n    int val;\n    int height;\n    struct TreeNode *left;\n    struct TreeNode *right;\n} TreeNode;\n\n/* コンストラクタ */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
/* AVL 木ノードクラス */\nclass TreeNode(val _val: Int) {  // ノード値\n    val height: Int = 0          // ノードの高さ\n    val left: TreeNode? = null   // 左の子ノード\n    val right: TreeNode? = null  // 右の子ノード\n}\n
### AVL 木ノードクラス ###\nclass TreeNode\n  attr_accessor :val    # ノード値\n  attr_accessor :height # ノードの高さ\n  attr_accessor :left   # 左の子ノード参照\n  attr_accessor :right  # 右の子ノード参照\n\n  def initialize(val)\n    @val = val\n    @height = 0\n  end\nend\n

「ノードの高さ」とは、そのノードから最も遠い葉ノードまでの距離、すなわち通過する「辺」の本数を指します。特に、葉ノードの高さは \\(0\\)、空ノードの高さは \\(-1\\) です。ここでは、ノードの高さを取得・更新するための 2 つの補助関数を用意します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def height(self, node: TreeNode | None) -> int:\n    \"\"\"ノードの高さを取得\"\"\"\n    # 空ノードの高さは -1、葉ノードの高さは 0\n    if node is not None:\n        return node.height\n    return -1\n\ndef update_height(self, node: TreeNode | None):\n    \"\"\"ノードの高さを更新する\"\"\"\n    # ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node.height = max([self.height(node.left), self.height(node.right)]) + 1\n
avl_tree.cpp
/* ノードの高さを取得 */\nint height(TreeNode *node) {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node == nullptr ? -1 : node->height;\n}\n\n/* ノードの高さを更新する */\nvoid updateHeight(TreeNode *node) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node->height = max(height(node->left), height(node->right)) + 1;\n}\n
avl_tree.java
/* ノードの高さを取得 */\nint height(TreeNode node) {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node == null ? -1 : node.height;\n}\n\n/* ノードの高さを更新する */\nvoid updateHeight(TreeNode node) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node.height = Math.max(height(node.left), height(node.right)) + 1;\n}\n
avl_tree.cs
/* ノードの高さを取得 */\nint Height(TreeNode? node) {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node == null ? -1 : node.height;\n}\n\n/* ノードの高さを更新する */\nvoid UpdateHeight(TreeNode node) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node.height = Math.Max(Height(node.left), Height(node.right)) + 1;\n}\n
avl_tree.go
/* ノードの高さを取得 */\nfunc (t *aVLTree) height(node *TreeNode) int {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    if node != nil {\n        return node.Height\n    }\n    return -1\n}\n\n/* ノードの高さを更新する */\nfunc (t *aVLTree) updateHeight(node *TreeNode) {\n    lh := t.height(node.Left)\n    rh := t.height(node.Right)\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    if lh > rh {\n        node.Height = lh + 1\n    } else {\n        node.Height = rh + 1\n    }\n}\n
avl_tree.swift
/* ノードの高さを取得 */\nfunc height(node: TreeNode?) -> Int {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    node?.height ?? -1\n}\n\n/* ノードの高さを更新する */\nfunc updateHeight(node: TreeNode?) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node?.height = max(height(node: node?.left), height(node: node?.right)) + 1\n}\n
avl_tree.js
/* ノードの高さを取得 */\nheight(node) {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node === null ? -1 : node.height;\n}\n\n/* ノードの高さを更新する */\n#updateHeight(node) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
avl_tree.ts
/* ノードの高さを取得 */\nheight(node: TreeNode): number {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node === null ? -1 : node.height;\n}\n\n/* ノードの高さを更新する */\nupdateHeight(node: TreeNode): void {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
avl_tree.dart
/* ノードの高さを取得 */\nint height(TreeNode? node) {\n  // 空ノードの高さは -1、葉ノードの高さは 0\n  return node == null ? -1 : node.height;\n}\n\n/* ノードの高さを更新する */\nvoid updateHeight(TreeNode? node) {\n  // ノードの高さは最も高い部分木の高さ + 1 に等しい\n  node!.height = max(height(node.left), height(node.right)) + 1;\n}\n
avl_tree.rs
/* ノードの高さを取得 */\nfn height(node: OptionTreeNodeRc) -> i32 {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    match node {\n        Some(node) => node.borrow().height,\n        None => -1,\n    }\n}\n\n/* ノードの高さを更新する */\nfn update_height(node: OptionTreeNodeRc) {\n    if let Some(node) = node {\n        let left = node.borrow().left.clone();\n        let right = node.borrow().right.clone();\n        // ノードの高さは最も高い部分木の高さ + 1 に等しい\n        node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1;\n    }\n}\n
avl_tree.c
/* ノードの高さを取得 */\nint height(TreeNode *node) {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    if (node != NULL) {\n        return node->height;\n    }\n    return -1;\n}\n\n/* ノードの高さを更新する */\nvoid updateHeight(TreeNode *node) {\n    int lh = height(node->left);\n    int rh = height(node->right);\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    if (lh > rh) {\n        node->height = lh + 1;\n    } else {\n        node->height = rh + 1;\n    }\n}\n
avl_tree.kt
/* ノードの高さを取得 */\nfun height(node: TreeNode?): Int {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node?.height ?: -1\n}\n\n/* ノードの高さを更新する */\nfun updateHeight(node: TreeNode?) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node?.height = max(height(node?.left), height(node?.right)) + 1\n}\n
avl_tree.rb
### ノードの高さを取得 ###\ndef height(node)\n  # 空ノードの高さは -1、葉ノードの高さは 0\n  return node.height unless node.nil?\n\n  -1\nend\n\n### ノードの高さを更新 ###\ndef update_height(node)\n  # ノードの高さは最も高い部分木の高さ + 1 に等しい\n  node.height = [height(node.left), height(node.right)].max + 1\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2","level":3,"title":"2.   ノードの平衡係数","text":"

ノードの平衡係数(balance factor)は、左部分木の高さから右部分木の高さを引いた値と定義し、空ノードの平衡係数は \\(0\\) とします。同様に、ノードの平衡係数を取得する機能も関数にカプセル化して、後続で使いやすくします:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def balance_factor(self, node: TreeNode | None) -> int:\n    \"\"\"平衡係数を取得\"\"\"\n    # 空ノードの平衡係数は 0\n    if node is None:\n        return 0\n    # ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return self.height(node.left) - self.height(node.right)\n
avl_tree.cpp
/* 平衡係数を取得 */\nint balanceFactor(TreeNode *node) {\n    // 空ノードの平衡係数は 0\n    if (node == nullptr)\n        return 0;\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return height(node->left) - height(node->right);\n}\n
avl_tree.java
/* 平衡係数を取得 */\nint balanceFactor(TreeNode node) {\n    // 空ノードの平衡係数は 0\n    if (node == null)\n        return 0;\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return height(node.left) - height(node.right);\n}\n
avl_tree.cs
/* 平衡係数を取得 */\nint BalanceFactor(TreeNode? node) {\n    // 空ノードの平衡係数は 0\n    if (node == null) return 0;\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return Height(node.left) - Height(node.right);\n}\n
avl_tree.go
/* 平衡係数を取得 */\nfunc (t *aVLTree) balanceFactor(node *TreeNode) int {\n    // 空ノードの平衡係数は 0\n    if node == nil {\n        return 0\n    }\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return t.height(node.Left) - t.height(node.Right)\n}\n
avl_tree.swift
/* 平衡係数を取得 */\nfunc balanceFactor(node: TreeNode?) -> Int {\n    // 空ノードの平衡係数は 0\n    guard let node = node else { return 0 }\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return height(node: node.left) - height(node: node.right)\n}\n
avl_tree.js
/* 平衡係数を取得 */\nbalanceFactor(node) {\n    // 空ノードの平衡係数は 0\n    if (node === null) return 0;\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return this.height(node.left) - this.height(node.right);\n}\n
avl_tree.ts
/* 平衡係数を取得 */\nbalanceFactor(node: TreeNode): number {\n    // 空ノードの平衡係数は 0\n    if (node === null) return 0;\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return this.height(node.left) - this.height(node.right);\n}\n
avl_tree.dart
/* 平衡係数を取得 */\nint balanceFactor(TreeNode? node) {\n  // 空ノードの平衡係数は 0\n  if (node == null) return 0;\n  // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n  return height(node.left) - height(node.right);\n}\n
avl_tree.rs
/* 平衡係数を取得 */\nfn balance_factor(node: OptionTreeNodeRc) -> i32 {\n    match node {\n        // 空ノードの平衡係数は 0\n        None => 0,\n        // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n        Some(node) => {\n            Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone())\n        }\n    }\n}\n
avl_tree.c
/* 平衡係数を取得 */\nint balanceFactor(TreeNode *node) {\n    // 空ノードの平衡係数は 0\n    if (node == NULL) {\n        return 0;\n    }\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return height(node->left) - height(node->right);\n}\n
avl_tree.kt
/* 平衡係数を取得 */\nfun balanceFactor(node: TreeNode?): Int {\n    // 空ノードの平衡係数は 0\n    if (node == null) return 0\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return height(node.left) - height(node.right)\n}\n
avl_tree.rb
### 平衡係数を取得 ###\ndef balance_factor(node)\n  # 空ノードの平衡係数は 0\n  return 0 if node.nil?\n\n  # ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n  height(node.left) - height(node.right)\nend\n

Tip

平衡係数を \\(f\\) とすると、AVL 木の任意のノードの平衡係数は常に \\(-1 \\le f \\le 1\\) を満たします。

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#752-avl","level":2,"title":"7.5.2   AVL 木の回転","text":"

AVL 木の特徴は「回転」操作にあり、二分木の中順走査列を変えずに、不平衡ノードを再び平衡に戻せます。言い換えると、回転操作は「二分探索木」の性質を保ちながら、木を再び「平衡二分木」に戻すことができます。

平衡係数の絶対値が \\(> 1\\) のノードを「不平衡ノード」と呼びます。ノードの不平衡の形に応じて、回転操作は 4 種類に分かれます。右回転、左回転、右回転してから左回転、左回転してから右回転です。以下でこれらを順に説明します。

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_1","level":3,"title":"1.   右回転","text":"

以下の図では、ノードの下に平衡係数を示しています。下から上へ見ると、二分木で最初に不平衡になるのは「ノード 3」です。この不平衡ノードを根とする部分木に注目し、そのノードを node、左の子ノードを child として、「右回転」を行います。右回転後、部分木は平衡を回復し、なおかつ二分探索木の性質も保たれます。

<1><2><3><4>

図 7-26   右回転の手順

以下の図に示すように、ノード child に右の子ノード(grand_child と記す)がある場合、右回転には 1 ステップ追加する必要があります。すなわち、grand_childnode の左の子ノードにします。

図 7-27   grand_child を持つ右回転

「右に回転する」というのはあくまでイメージしやすい表現であり、実際にはノードポインタを変更して実現します。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def right_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"右回転\"\"\"\n    child = node.left\n    grand_child = child.right\n    # child を支点として node を右回転させる\n    child.right = node\n    node.left = grand_child\n    # ノードの高さを更新する\n    self.update_height(node)\n    self.update_height(child)\n    # 回転後の部分木の根ノードを返す\n    return child\n
avl_tree.cpp
/* 右回転 */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child = node->left;\n    TreeNode *grandChild = child->right;\n    // child を支点として node を右回転させる\n    child->right = node;\n    node->left = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.java
/* 右回転 */\nTreeNode rightRotate(TreeNode node) {\n    TreeNode child = node.left;\n    TreeNode grandChild = child.right;\n    // child を支点として node を右回転させる\n    child.right = node;\n    node.left = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.cs
/* 右回転 */\nTreeNode? RightRotate(TreeNode? node) {\n    TreeNode? child = node?.left;\n    TreeNode? grandChild = child?.right;\n    // child を支点として node を右回転させる\n    child.right = node;\n    node.left = grandChild;\n    // ノードの高さを更新する\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.go
/* 右回転 */\nfunc (t *aVLTree) rightRotate(node *TreeNode) *TreeNode {\n    child := node.Left\n    grandChild := child.Right\n    // child を支点として node を右回転させる\n    child.Right = node\n    node.Left = grandChild\n    // ノードの高さを更新する\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.swift
/* 右回転 */\nfunc rightRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.left\n    let grandChild = child?.right\n    // child を支点として node を右回転させる\n    child?.right = node\n    node?.left = grandChild\n    // ノードの高さを更新する\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.js
/* 右回転 */\n#rightRotate(node) {\n    const child = node.left;\n    const grandChild = child.right;\n    // child を支点として node を右回転させる\n    child.right = node;\n    node.left = grandChild;\n    // ノードの高さを更新する\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.ts
/* 右回転 */\nrightRotate(node: TreeNode): TreeNode {\n    const child = node.left;\n    const grandChild = child.right;\n    // child を支点として node を右回転させる\n    child.right = node;\n    node.left = grandChild;\n    // ノードの高さを更新する\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.dart
/* 右回転 */\nTreeNode? rightRotate(TreeNode? node) {\n  TreeNode? child = node!.left;\n  TreeNode? grandChild = child!.right;\n  // child を支点として node を右回転させる\n  child.right = node;\n  node.left = grandChild;\n  // ノードの高さを更新する\n  updateHeight(node);\n  updateHeight(child);\n  // 回転後の部分木の根ノードを返す\n  return child;\n}\n
avl_tree.rs
/* 右回転 */\nfn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().left.clone().unwrap();\n            let grand_child = child.borrow().right.clone();\n            // child を支点として node を右回転させる\n            child.borrow_mut().right = Some(node.clone());\n            node.borrow_mut().left = grand_child;\n            // ノードの高さを更新する\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // 回転後の部分木の根ノードを返す\n            Some(child)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* 右回転 */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->left;\n    grandChild = child->right;\n    // child を支点として node を右回転させる\n    child->right = node;\n    node->left = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.kt
/* 右回転 */\nfun rightRotate(node: TreeNode?): TreeNode {\n    val child = node!!.left\n    val grandChild = child!!.right\n    // child を支点として node を右回転させる\n    child.right = node\n    node.left = grandChild\n    // ノードの高さを更新する\n    updateHeight(node)\n    updateHeight(child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.rb
### 右回転操作 ###\ndef right_rotate(node)\n  child = node.left\n  grand_child = child.right\n  # child を支点として node を右回転させる\n  child.right = node\n  node.left = grand_child\n  # ノードの高さを更新する\n  update_height(node)\n  update_height(child)\n  # 回転後の部分木の根ノードを返す\n  child\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_1","level":3,"title":"2.   左回転","text":"

対応する鏡像として、上記の不平衡二分木を左右反転して考えると、以下の図に示す「左回転」が必要になります。

図 7-28   左回転

同様に、以下の図に示すように、ノード child に左の子ノード(grand_child と記す)がある場合、左回転にも 1 ステップ追加する必要があります。すなわち、grand_childnode の右の子ノードにします。

図 7-29   grand_child を持つ左回転

分かるように、右回転と左回転は論理的に鏡像対称であり、それぞれが解決する 2 種類の不平衡も対称です。この対称性に基づけば、右回転の実装コードにあるすべての leftright に、すべての rightleft に置き換えるだけで、左回転の実装コードが得られます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def left_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"左回転\"\"\"\n    child = node.right\n    grand_child = child.left\n    # child を支点として node を左回転させる\n    child.left = node\n    node.right = grand_child\n    # ノードの高さを更新する\n    self.update_height(node)\n    self.update_height(child)\n    # 回転後の部分木の根ノードを返す\n    return child\n
avl_tree.cpp
/* 左回転 */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child = node->right;\n    TreeNode *grandChild = child->left;\n    // child を支点として node を左回転させる\n    child->left = node;\n    node->right = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.java
/* 左回転 */\nTreeNode leftRotate(TreeNode node) {\n    TreeNode child = node.right;\n    TreeNode grandChild = child.left;\n    // child を支点として node を左回転させる\n    child.left = node;\n    node.right = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.cs
/* 左回転 */\nTreeNode? LeftRotate(TreeNode? node) {\n    TreeNode? child = node?.right;\n    TreeNode? grandChild = child?.left;\n    // child を支点として node を左回転させる\n    child.left = node;\n    node.right = grandChild;\n    // ノードの高さを更新する\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.go
/* 左回転 */\nfunc (t *aVLTree) leftRotate(node *TreeNode) *TreeNode {\n    child := node.Right\n    grandChild := child.Left\n    // child を支点として node を左回転させる\n    child.Left = node\n    node.Right = grandChild\n    // ノードの高さを更新する\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.swift
/* 左回転 */\nfunc leftRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.right\n    let grandChild = child?.left\n    // child を支点として node を左回転させる\n    child?.left = node\n    node?.right = grandChild\n    // ノードの高さを更新する\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.js
/* 左回転 */\n#leftRotate(node) {\n    const child = node.right;\n    const grandChild = child.left;\n    // child を支点として node を左回転させる\n    child.left = node;\n    node.right = grandChild;\n    // ノードの高さを更新する\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.ts
/* 左回転 */\nleftRotate(node: TreeNode): TreeNode {\n    const child = node.right;\n    const grandChild = child.left;\n    // child を支点として node を左回転させる\n    child.left = node;\n    node.right = grandChild;\n    // ノードの高さを更新する\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.dart
/* 左回転 */\nTreeNode? leftRotate(TreeNode? node) {\n  TreeNode? child = node!.right;\n  TreeNode? grandChild = child!.left;\n  // child を支点として node を左回転させる\n  child.left = node;\n  node.right = grandChild;\n  // ノードの高さを更新する\n  updateHeight(node);\n  updateHeight(child);\n  // 回転後の部分木の根ノードを返す\n  return child;\n}\n
avl_tree.rs
/* 左回転 */\nfn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().right.clone().unwrap();\n            let grand_child = child.borrow().left.clone();\n            // child を支点として node を左回転させる\n            child.borrow_mut().left = Some(node.clone());\n            node.borrow_mut().right = grand_child;\n            // ノードの高さを更新する\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // 回転後の部分木の根ノードを返す\n            Some(child)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* 左回転 */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->right;\n    grandChild = child->left;\n    // child を支点として node を左回転させる\n    child->left = node;\n    node->right = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.kt
/* 左回転 */\nfun leftRotate(node: TreeNode?): TreeNode {\n    val child = node!!.right\n    val grandChild = child!!.left\n    // child を支点として node を左回転させる\n    child.left = node\n    node.right = grandChild\n    // ノードの高さを更新する\n    updateHeight(node)\n    updateHeight(child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.rb
### 左回転操作 ###\ndef left_rotate(node)\n  child = node.right\n  grand_child = child.left\n  # child を支点として node を左回転させる\n  child.left = node\n  node.right = grand_child\n  # ノードの高さを更新する\n  update_height(node)\n  update_height(child)\n  # 回転後の部分木の根ノードを返す\n  child\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3","level":3,"title":"3.   左回転してから右回転","text":"

以下の図の不平衡ノード 3 では、左回転だけでも右回転だけでも部分木を平衡に戻せません。この場合は、まず child に「左回転」を行い、次に node に「右回転」を行います。

図 7-30   左回転してから右回転

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4","level":3,"title":"4.   右回転してから左回転","text":"

以下の図に示すように、上記の不平衡二分木の鏡像のケースでは、まず child に「右回転」を行い、次に node に「左回転」を行います。

図 7-31   右回転してから左回転

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5","level":3,"title":"5.   回転の選択","text":"

以下の図に示す 4 種類の不平衡は、上の各ケースにそれぞれ対応しており、必要な操作は順に右回転、左回転してから右回転、右回転してから左回転、左回転です。

図 7-32   AVL 木の 4 つの回転ケース

以下の表に示すように、不平衡ノードの平衡係数と、高い側の子ノードの平衡係数の符号を判定することで、その不平衡ノードが上図のどのケースに属するかを判断できます。

表 7-3   4 種類の回転ケースの選択条件

不平衡ノードの平衡係数 子ノードの平衡係数 採用すべき回転方法 \\(> 1\\) (左に偏った木) \\(\\geq 0\\) 右回転 \\(> 1\\) (左に偏った木) \\(<0\\) 左回転してから右回転 \\(< -1\\) (右に偏った木) \\(\\leq 0\\) 左回転 \\(< -1\\) (右に偏った木) \\(>0\\) 右回転してから左回転

使いやすくするために、回転操作を 1 つの関数にカプセル化します。この関数があれば、さまざまな不平衡ケースに対して回転を行い、不平衡ノードを再び平衡に戻せます。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"回転操作を行い、この部分木の平衡を回復する\"\"\"\n    # ノード node の平衡係数を取得\n    balance_factor = self.balance_factor(node)\n    # 左に偏った木\n    if balance_factor > 1:\n        if self.balance_factor(node.left) >= 0:\n            # 右回転\n            return self.right_rotate(node)\n        else:\n            # 左回転してから右回転\n            node.left = self.left_rotate(node.left)\n            return self.right_rotate(node)\n    # 右に偏った木\n    elif balance_factor < -1:\n        if self.balance_factor(node.right) <= 0:\n            # 左回転\n            return self.left_rotate(node)\n        else:\n            # 右回転してから左回転\n            node.right = self.right_rotate(node.right)\n            return self.left_rotate(node)\n    # 平衡木なので回転不要、そのまま返す\n    return node\n
avl_tree.cpp
/* 回転操作を行い、この部分木の平衡を回復する */\nTreeNode *rotate(TreeNode *node) {\n    // ノード node の平衡係数を取得\n    int _balanceFactor = balanceFactor(node);\n    // 左に偏った木\n    if (_balanceFactor > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // 右回転\n            return rightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (_balanceFactor < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // 左回転\n            return leftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.java
/* 回転操作を行い、この部分木の平衡を回復する */\nTreeNode rotate(TreeNode node) {\n    // ノード node の平衡係数を取得\n    int balanceFactor = balanceFactor(node);\n    // 左に偏った木\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // 右回転\n            return rightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node.left = leftRotate(node.left);\n            return rightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // 左回転\n            return leftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node.right = rightRotate(node.right);\n            return leftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.cs
/* 回転操作を行い、この部分木の平衡を回復する */\nTreeNode? Rotate(TreeNode? node) {\n    // ノード node の平衡係数を取得\n    int balanceFactorInt = BalanceFactor(node);\n    // 左に偏った木\n    if (balanceFactorInt > 1) {\n        if (BalanceFactor(node?.left) >= 0) {\n            // 右回転\n            return RightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node!.left = LeftRotate(node!.left);\n            return RightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (balanceFactorInt < -1) {\n        if (BalanceFactor(node?.right) <= 0) {\n            // 左回転\n            return LeftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node!.right = RightRotate(node!.right);\n            return LeftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.go
/* 回転操作を行い、この部分木の平衡を回復する */\nfunc (t *aVLTree) rotate(node *TreeNode) *TreeNode {\n    // ノード `node` の平衡係数を取得する\n    // Go では短い変数名が推奨されるため、ここで `bf` は `t.balanceFactor` を表す\n    bf := t.balanceFactor(node)\n    // 左に偏った木\n    if bf > 1 {\n        if t.balanceFactor(node.Left) >= 0 {\n            // 右回転\n            return t.rightRotate(node)\n        } else {\n            // 左回転してから右回転\n            node.Left = t.leftRotate(node.Left)\n            return t.rightRotate(node)\n        }\n    }\n    // 右に偏った木\n    if bf < -1 {\n        if t.balanceFactor(node.Right) <= 0 {\n            // 左回転\n            return t.leftRotate(node)\n        } else {\n            // 右回転してから左回転\n            node.Right = t.rightRotate(node.Right)\n            return t.leftRotate(node)\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node\n}\n
avl_tree.swift
/* 回転操作を行い、この部分木の平衡を回復する */\nfunc rotate(node: TreeNode?) -> TreeNode? {\n    // ノード node の平衡係数を取得\n    let balanceFactor = balanceFactor(node: node)\n    // 左に偏った木\n    if balanceFactor > 1 {\n        if self.balanceFactor(node: node?.left) >= 0 {\n            // 右回転\n            return rightRotate(node: node)\n        } else {\n            // 左回転してから右回転\n            node?.left = leftRotate(node: node?.left)\n            return rightRotate(node: node)\n        }\n    }\n    // 右に偏った木\n    if balanceFactor < -1 {\n        if self.balanceFactor(node: node?.right) <= 0 {\n            // 左回転\n            return leftRotate(node: node)\n        } else {\n            // 右回転してから左回転\n            node?.right = rightRotate(node: node?.right)\n            return leftRotate(node: node)\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node\n}\n
avl_tree.js
/* 回転操作を行い、この部分木の平衡を回復する */\n#rotate(node) {\n    // ノード node の平衡係数を取得\n    const balanceFactor = this.balanceFactor(node);\n    // 左に偏った木\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // 右回転\n            return this.#rightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node.left = this.#leftRotate(node.left);\n            return this.#rightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // 左回転\n            return this.#leftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node.right = this.#rightRotate(node.right);\n            return this.#leftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.ts
/* 回転操作を行い、この部分木の平衡を回復する */\nrotate(node: TreeNode): TreeNode {\n    // ノード node の平衡係数を取得\n    const balanceFactor = this.balanceFactor(node);\n    // 左に偏った木\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // 右回転\n            return this.rightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node.left = this.leftRotate(node.left);\n            return this.rightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // 左回転\n            return this.leftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node.right = this.rightRotate(node.right);\n            return this.leftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.dart
/* 回転操作を行い、この部分木の平衡を回復する */\nTreeNode? rotate(TreeNode? node) {\n  // ノード node の平衡係数を取得\n  int factor = balanceFactor(node);\n  // 左に偏った木\n  if (factor > 1) {\n    if (balanceFactor(node!.left) >= 0) {\n      // 右回転\n      return rightRotate(node);\n    } else {\n      // 左回転してから右回転\n      node.left = leftRotate(node.left);\n      return rightRotate(node);\n    }\n  }\n  // 右に偏った木\n  if (factor < -1) {\n    if (balanceFactor(node!.right) <= 0) {\n      // 左回転\n      return leftRotate(node);\n    } else {\n      // 右回転してから左回転\n      node.right = rightRotate(node.right);\n      return leftRotate(node);\n    }\n  }\n  // 平衡木なので回転不要、そのまま返す\n  return node;\n}\n
avl_tree.rs
/* 回転操作を行い、この部分木の平衡を回復する */\nfn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    // ノード node の平衡係数を取得\n    let balance_factor = Self::balance_factor(node.clone());\n    // 左に偏った木\n    if balance_factor > 1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().left.clone()) >= 0 {\n            // 右回転\n            Self::right_rotate(Some(node))\n        } else {\n            // 左回転してから右回転\n            let left = node.borrow().left.clone();\n            node.borrow_mut().left = Self::left_rotate(left);\n            Self::right_rotate(Some(node))\n        }\n    }\n    // 右に偏った木\n    else if balance_factor < -1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().right.clone()) <= 0 {\n            // 左回転\n            Self::left_rotate(Some(node))\n        } else {\n            // 右回転してから左回転\n            let right = node.borrow().right.clone();\n            node.borrow_mut().right = Self::right_rotate(right);\n            Self::left_rotate(Some(node))\n        }\n    } else {\n        // 平衡木なので回転不要、そのまま返す\n        node\n    }\n}\n
avl_tree.c
/* 回転操作を行い、この部分木の平衡を回復する */\nTreeNode *rotate(TreeNode *node) {\n    // ノード node の平衡係数を取得\n    int bf = balanceFactor(node);\n    // 左に偏った木\n    if (bf > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // 右回転\n            return rightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (bf < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // 左回転\n            return leftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.kt
/* 回転操作を行い、この部分木の平衡を回復する */\nfun rotate(node: TreeNode): TreeNode {\n    // ノード node の平衡係数を取得\n    val balanceFactor = balanceFactor(node)\n    // 左に偏った木\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // 右回転\n            return rightRotate(node)\n        } else {\n            // 左回転してから右回転\n            node.left = leftRotate(node.left)\n            return rightRotate(node)\n        }\n    }\n    // 右に偏った木\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // 左回転\n            return leftRotate(node)\n        } else {\n            // 右回転してから左回転\n            node.right = rightRotate(node.right)\n            return leftRotate(node)\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node\n}\n
avl_tree.rb
### 回転操作を行い、この部分木の平衡を回復する ###\ndef rotate(node)\n  # ノード node の平衡係数を取得\n  balance_factor = balance_factor(node)\n  # 左部分木をたどる\n  if balance_factor > 1\n    if balance_factor(node.left) >= 0\n      # 右回転\n      return right_rotate(node)\n    else\n      # 左回転してから右回転\n      node.left = left_rotate(node.left)\n      return right_rotate(node)\n    end\n  # 右に偏った木\n  elsif balance_factor < -1\n    if balance_factor(node.right) <= 0\n      # 左回転\n      return left_rotate(node)\n    else\n      # 右回転してから左回転\n      node.right = right_rotate(node.right)\n      return left_rotate(node)\n    end\n  end\n  # 平衡木なので回転不要、そのまま返す\n  node\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#753-avl","level":2,"title":"7.5.3   AVL 木の基本操作","text":"","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_2","level":3,"title":"1.   ノードの挿入","text":"

AVL 木のノード挿入は、基本的には二分探索木と同じです。唯一の違いは、AVL 木ではノード挿入後に、そのノードから根ノードまでの経路上に複数の不平衡ノードが現れる可能性があることです。したがって、このノードから開始して、下から上へ回転操作を行い、すべての不平衡ノードを平衡に戻す必要があります。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def insert(self, val):\n    \"\"\"ノードを挿入\"\"\"\n    self._root = self.insert_helper(self._root, val)\n\ndef insert_helper(self, node: TreeNode | None, val: int) -> TreeNode:\n    \"\"\"ノードを再帰的に挿入する(補助メソッド)\"\"\"\n    if node is None:\n        return TreeNode(val)\n    # 1. 挿入位置を探索してノードを挿入\n    if val < node.val:\n        node.left = self.insert_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.insert_helper(node.right, val)\n    else:\n        # 重複ノードは挿入せず、そのまま返す\n        return node\n    # ノードの高さを更新する\n    self.update_height(node)\n    # 2. 回転操作を行い、部分木の平衡を回復する\n    return self.rotate(node)\n
avl_tree.cpp
/* ノードを挿入 */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return new TreeNode(val);\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node->val)\n        node->left = insertHelper(node->left, val);\n    else if (val > node->val)\n        node->right = insertHelper(node->right, val);\n    else\n        return node;    // 重複ノードは挿入せず、そのまま返す\n    updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.java
/* ノードを挿入 */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nTreeNode insertHelper(TreeNode node, int val) {\n    if (node == null)\n        return new TreeNode(val);\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node.val)\n        node.left = insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = insertHelper(node.right, val);\n    else\n        return node; // 重複ノードは挿入せず、そのまま返す\n    updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.cs
/* ノードを挿入 */\nvoid Insert(int val) {\n    root = InsertHelper(root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nTreeNode? InsertHelper(TreeNode? node, int val) {\n    if (node == null) return new TreeNode(val);\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node.val)\n        node.left = InsertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = InsertHelper(node.right, val);\n    else\n        return node;     // 重複ノードは挿入せず、そのまま返す\n    UpdateHeight(node);  // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = Rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.go
/* ノードを挿入 */\nfunc (t *aVLTree) insert(val int) {\n    t.root = t.insertHelper(t.root, val)\n}\n\n/* ノードを再帰的に挿入する(補助関数) */\nfunc (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return NewTreeNode(val)\n    }\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if val < node.Val.(int) {\n        node.Left = t.insertHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.insertHelper(node.Right, val)\n    } else {\n        // 重複ノードは挿入せず、そのまま返す\n        return node\n    }\n    // ノードの高さを更新する\n    t.updateHeight(node)\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = t.rotate(node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.swift
/* ノードを挿入 */\nfunc insert(val: Int) {\n    root = insertHelper(node: root, val: val)\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nfunc insertHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return TreeNode(x: val)\n    }\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if val < node!.val {\n        node?.left = insertHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = insertHelper(node: node?.right, val: val)\n    } else {\n        return node // 重複ノードは挿入せず、そのまま返す\n    }\n    updateHeight(node: node) // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node: node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.js
/* ノードを挿入 */\ninsert(val) {\n    this.root = this.#insertHelper(this.root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\n#insertHelper(node, val) {\n    if (node === null) return new TreeNode(val);\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node.val) node.left = this.#insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#insertHelper(node.right, val);\n    else return node; // 重複ノードは挿入せず、そのまま返す\n    this.#updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = this.#rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.ts
/* ノードを挿入 */\ninsert(val: number): void {\n    this.root = this.insertHelper(this.root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\ninsertHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return new TreeNode(val);\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node.val) {\n        node.left = this.insertHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.insertHelper(node.right, val);\n    } else {\n        return node; // 重複ノードは挿入せず、そのまま返す\n    }\n    this.updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = this.rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.dart
/* ノードを挿入 */\nvoid insert(int val) {\n  root = insertHelper(root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nTreeNode? insertHelper(TreeNode? node, int val) {\n  if (node == null) return TreeNode(val);\n  /* 1. 挿入位置を探索してノードを挿入 */\n  if (val < node.val)\n    node.left = insertHelper(node.left, val);\n  else if (val > node.val)\n    node.right = insertHelper(node.right, val);\n  else\n    return node; // 重複ノードは挿入せず、そのまま返す\n  updateHeight(node); // ノードの高さを更新する\n  /* 2. 回転操作を行い、部分木の平衡を回復する */\n  node = rotate(node);\n  // 部分木の根ノードを返す\n  return node;\n}\n
avl_tree.rs
/* ノードを挿入 */\nfn insert(&mut self, val: i32) {\n    self.root = Self::insert_helper(self.root.clone(), val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nfn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. 挿入位置を探索してノードを挿入 */\n            match {\n                let node_val = node.borrow().val;\n                node_val\n            }\n            .cmp(&val)\n            {\n                Ordering::Greater => {\n                    let left = node.borrow().left.clone();\n                    node.borrow_mut().left = Self::insert_helper(left, val);\n                }\n                Ordering::Less => {\n                    let right = node.borrow().right.clone();\n                    node.borrow_mut().right = Self::insert_helper(right, val);\n                }\n                Ordering::Equal => {\n                    return Some(node); // 重複ノードは挿入せず、そのまま返す\n                }\n            }\n            Self::update_height(Some(node.clone())); // ノードの高さを更新する\n\n            /* 2. 回転操作を行い、部分木の平衡を回復する */\n            node = Self::rotate(Some(node)).unwrap();\n            // 部分木の根ノードを返す\n            Some(node)\n        }\n        None => Some(TreeNode::new(val)),\n    }\n}\n
avl_tree.c
/* ノードを挿入 */\nvoid insert(AVLTree *tree, int val) {\n    tree->root = insertHelper(tree->root, val);\n}\n\n/* ノードを再帰的に挿入する(補助関数) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == NULL) {\n        return newTreeNode(val);\n    }\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node->val) {\n        node->left = insertHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = insertHelper(node->right, val);\n    } else {\n        // 重複ノードは挿入せず、そのまま返す\n        return node;\n    }\n    // ノードの高さを更新する\n    updateHeight(node);\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.kt
/* ノードを挿入 */\nfun insert(_val: Int) {\n    root = insertHelper(root, _val)\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nfun insertHelper(n: TreeNode?, _val: Int): TreeNode {\n    if (n == null)\n        return TreeNode(_val)\n    var node = n\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (_val < node._val)\n        node.left = insertHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = insertHelper(node.right, _val)\n    else\n        return node // 重複ノードは挿入せず、そのまま返す\n    updateHeight(node) // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.rb
### ノードを挿入 ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n### ノードを挿入 ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n# ## ノードを再帰的に挿入(補助メソッド)###\ndef insert_helper(node, val)\n  return TreeNode.new(val) if node.nil?\n  # 1. 挿入位置を探索してノードを挿入\n  if val < node.val\n    node.left = insert_helper(node.left, val)\n  elsif val > node.val\n    node.right = insert_helper(node.right, val)\n  else\n    # 重複ノードは挿入せず、そのまま返す\n    return node\n  end\n  # ノードの高さを更新する\n  update_height(node)\n  # 2. 回転操作を行い、部分木の平衡を回復する\n  rotate(node)\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_2","level":3,"title":"2.   ノードの削除","text":"

同様に、二分探索木のノード削除メソッドを土台として、下から上へ回転操作を行い、すべての不平衡ノードを平衡に戻す必要があります。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def remove(self, val: int):\n    \"\"\"ノードを削除\"\"\"\n    self._root = self.remove_helper(self._root, val)\n\ndef remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None:\n    \"\"\"ノードを再帰的に削除する(補助メソッド)\"\"\"\n    if node is None:\n        return None\n    # 1. ノードを探索して削除\n    if val < node.val:\n        node.left = self.remove_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.remove_helper(node.right, val)\n    else:\n        if node.left is None or node.right is None:\n            child = node.left or node.right\n            # 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if child is None:\n                return None\n            # 子ノード数 = 1 の場合、node をそのまま削除する\n            else:\n                node = child\n        else:\n            # 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            temp = node.right\n            while temp.left is not None:\n                temp = temp.left\n            node.right = self.remove_helper(node.right, temp.val)\n            node.val = temp.val\n    # ノードの高さを更新する\n    self.update_height(node)\n    # 2. 回転操作を行い、部分木の平衡を回復する\n    return self.rotate(node)\n
avl_tree.cpp
/* ノードを削除 */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return nullptr;\n    /* 1. ノードを探索して削除 */\n    if (val < node->val)\n        node->left = removeHelper(node->left, val);\n    else if (val > node->val)\n        node->right = removeHelper(node->right, val);\n    else {\n        if (node->left == nullptr || node->right == nullptr) {\n            TreeNode *child = node->left != nullptr ? node->left : node->right;\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child == nullptr) {\n                delete node;\n                return nullptr;\n            }\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else {\n                delete node;\n                node = child;\n            }\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            TreeNode *temp = node->right;\n            while (temp->left != nullptr) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.java
/* ノードを削除 */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nTreeNode removeHelper(TreeNode node, int val) {\n    if (node == null)\n        return null;\n    /* 1. ノードを探索して削除 */\n    if (val < node.val)\n        node.left = removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = removeHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode child = node.left != null ? node.left : node.right;\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child == null)\n                return null;\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else\n                node = child;\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            TreeNode temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.cs
/* ノードを削除 */\nvoid Remove(int val) {\n    root = RemoveHelper(root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nTreeNode? RemoveHelper(TreeNode? node, int val) {\n    if (node == null) return null;\n    /* 1. ノードを探索して削除 */\n    if (val < node.val)\n        node.left = RemoveHelper(node.left, val);\n    else if (val > node.val)\n        node.right = RemoveHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode? child = node.left ?? node.right;\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child == null)\n                return null;\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else\n                node = child;\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            TreeNode? temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = RemoveHelper(node.right, temp.val!.Value);\n            node.val = temp.val;\n        }\n    }\n    UpdateHeight(node);  // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = Rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.go
/* ノードを削除 */\nfunc (t *aVLTree) remove(val int) {\n    t.root = t.removeHelper(t.root, val)\n}\n\n/* ノードを再帰的に削除する(補助関数) */\nfunc (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return nil\n    }\n    /* 1. ノードを探索して削除 */\n    if val < node.Val.(int) {\n        node.Left = t.removeHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.removeHelper(node.Right, val)\n    } else {\n        if node.Left == nil || node.Right == nil {\n            child := node.Left\n            if node.Right != nil {\n                child = node.Right\n            }\n            if child == nil {\n                // 子ノード数 = 0 の場合、node をそのまま削除して返す\n                return nil\n            } else {\n                // 子ノード数 = 1 の場合、node をそのまま削除する\n                node = child\n            }\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            temp := node.Right\n            for temp.Left != nil {\n                temp = temp.Left\n            }\n            node.Right = t.removeHelper(node.Right, temp.Val.(int))\n            node.Val = temp.Val\n        }\n    }\n    // ノードの高さを更新する\n    t.updateHeight(node)\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = t.rotate(node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.swift
/* ノードを削除 */\nfunc remove(val: Int) {\n    root = removeHelper(node: root, val: val)\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nfunc removeHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return nil\n    }\n    /* 1. ノードを探索して削除 */\n    if val < node!.val {\n        node?.left = removeHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = removeHelper(node: node?.right, val: val)\n    } else {\n        if node?.left == nil || node?.right == nil {\n            let child = node?.left ?? node?.right\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if child == nil {\n                return nil\n            }\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else {\n                node = child\n            }\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            var temp = node?.right\n            while temp?.left != nil {\n                temp = temp?.left\n            }\n            node?.right = removeHelper(node: node?.right, val: temp!.val)\n            node?.val = temp!.val\n        }\n    }\n    updateHeight(node: node) // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node: node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.js
/* ノードを削除 */\nremove(val) {\n    this.root = this.#removeHelper(this.root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\n#removeHelper(node, val) {\n    if (node === null) return null;\n    /* 1. ノードを探索して削除 */\n    if (val < node.val) node.left = this.#removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#removeHelper(node.right, val);\n    else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child === null) return null;\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else node = child;\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.#removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.#updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = this.#rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.ts
/* ノードを削除 */\nremove(val: number): void {\n    this.root = this.removeHelper(this.root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nremoveHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return null;\n    /* 1. ノードを探索して削除 */\n    if (val < node.val) {\n        node.left = this.removeHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.removeHelper(node.right, val);\n    } else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child === null) {\n                return null;\n            } else {\n                // 子ノード数 = 1 の場合、node をそのまま削除する\n                node = child;\n            }\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = this.rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.dart
/* ノードを削除 */\nvoid remove(int val) {\n  root = removeHelper(root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nTreeNode? removeHelper(TreeNode? node, int val) {\n  if (node == null) return null;\n  /* 1. ノードを探索して削除 */\n  if (val < node.val)\n    node.left = removeHelper(node.left, val);\n  else if (val > node.val)\n    node.right = removeHelper(node.right, val);\n  else {\n    if (node.left == null || node.right == null) {\n      TreeNode? child = node.left ?? node.right;\n      // 子ノード数 = 0 の場合、node をそのまま削除して返す\n      if (child == null)\n        return null;\n      // 子ノード数 = 1 の場合、node をそのまま削除する\n      else\n        node = child;\n    } else {\n      // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n      TreeNode? temp = node.right;\n      while (temp!.left != null) {\n        temp = temp.left;\n      }\n      node.right = removeHelper(node.right, temp.val);\n      node.val = temp.val;\n    }\n  }\n  updateHeight(node); // ノードの高さを更新する\n  /* 2. 回転操作を行い、部分木の平衡を回復する */\n  node = rotate(node);\n  // 部分木の根ノードを返す\n  return node;\n}\n
avl_tree.rs
/* ノードを削除 */\nfn remove(&self, val: i32) {\n    Self::remove_helper(self.root.clone(), val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nfn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. ノードを探索して削除 */\n            if val < node.borrow().val {\n                let left = node.borrow().left.clone();\n                node.borrow_mut().left = Self::remove_helper(left, val);\n            } else if val > node.borrow().val {\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, val);\n            } else if node.borrow().left.is_none() || node.borrow().right.is_none() {\n                let child = if node.borrow().left.is_some() {\n                    node.borrow().left.clone()\n                } else {\n                    node.borrow().right.clone()\n                };\n                match child {\n                    // 子ノード数 = 0 の場合、node をそのまま削除して返す\n                    None => {\n                        return None;\n                    }\n                    // 子ノード数 = 1 の場合、node をそのまま削除する\n                    Some(child) => node = child,\n                }\n            } else {\n                // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n                let mut temp = node.borrow().right.clone().unwrap();\n                loop {\n                    let temp_left = temp.borrow().left.clone();\n                    if temp_left.is_none() {\n                        break;\n                    }\n                    temp = temp_left.unwrap();\n                }\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val);\n                node.borrow_mut().val = temp.borrow().val;\n            }\n            Self::update_height(Some(node.clone())); // ノードの高さを更新する\n\n            /* 2. 回転操作を行い、部分木の平衡を回復する */\n            node = Self::rotate(Some(node)).unwrap();\n            // 部分木の根ノードを返す\n            Some(node)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* ノードを削除 */\n// stdio.h を導入しているため、ここでは remove 識別子を使えない\nvoid removeItem(AVLTree *tree, int val) {\n    TreeNode *root = removeHelper(tree->root, val);\n}\n\n/* ノードを再帰的に削除する(補助関数) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    TreeNode *child, *grandChild;\n    if (node == NULL) {\n        return NULL;\n    }\n    /* 1. ノードを探索して削除 */\n    if (val < node->val) {\n        node->left = removeHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = removeHelper(node->right, val);\n    } else {\n        if (node->left == NULL || node->right == NULL) {\n            child = node->left;\n            if (node->right != NULL) {\n                child = node->right;\n            }\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child == NULL) {\n                return NULL;\n            } else {\n                // 子ノード数 = 1 の場合、node をそのまま削除する\n                node = child;\n            }\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            TreeNode *temp = node->right;\n            while (temp->left != NULL) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    // ノードの高さを更新する\n    updateHeight(node);\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.kt
/* ノードを削除 */\nfun remove(_val: Int) {\n    root = removeHelper(root, _val)\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nfun removeHelper(n: TreeNode?, _val: Int): TreeNode? {\n    var node = n ?: return null\n    /* 1. ノードを探索して削除 */\n    if (_val < node._val)\n        node.left = removeHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = removeHelper(node.right, _val)\n    else {\n        if (node.left == null || node.right == null) {\n            val child = if (node.left != null)\n                node.left\n            else\n                node.right\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child == null)\n                return null\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else\n                node = child\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            var temp = node.right\n            while (temp!!.left != null) {\n                temp = temp.left\n            }\n            node.right = removeHelper(node.right, temp._val)\n            node._val = temp._val\n        }\n    }\n    updateHeight(node) // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.rb
### ノードを削除 ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n### ノードを削除 ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n# ## ノードを再帰的に削除(補助メソッド)###\ndef remove_helper(node, val)\n  return if node.nil?\n  # 1. ノードを探索して削除\n  if val < node.val\n    node.left = remove_helper(node.left, val)\n  elsif val > node.val\n    node.right = remove_helper(node.right, val)\n  else\n    if node.left.nil? || node.right.nil?\n      child = node.left || node.right\n      # 子ノード数 = 0 の場合、node をそのまま削除して返す\n      return if child.nil?\n      # 子ノード数 = 1 の場合、node をそのまま削除する\n      node = child\n    else\n      # 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n      temp = node.right\n      while !temp.left.nil?\n        temp = temp.left\n      end\n      node.right = remove_helper(node.right, temp.val)\n      node.val = temp.val\n    end\n  end\n  # ノードの高さを更新する\n  update_height(node)\n  # 2. 回転操作を行い、部分木の平衡を回復する\n  rotate(node)\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3_1","level":3,"title":"3.   ノードの探索","text":"

AVL 木のノード探索操作は二分探索木と同じなので、ここでは繰り返しません。

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#754-avl","level":2,"title":"7.5.4   AVL 木の代表的な応用","text":"
  • 大規模データの整理・格納に用いられ、高頻度の探索と低頻度の追加・削除に適しています。
  • データベースのインデックスシステムの構築に使われます。
  • 赤黒木も代表的な平衡二分探索木の一つです。AVL 木と比べると、赤黒木は平衡条件がより緩く、ノードの挿入・削除に必要な回転操作が少ないため、平均的な更新効率はより高くなります。
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/binary_search_tree/","level":1,"title":"7.4   二分探索木","text":"

以下の図に示すように、二分探索木(binary search tree)は次の条件を満たします。

  1. 根ノードについて、左部分木のすべてのノードの値 \\(<\\) 根ノードの値 \\(<\\) 右部分木のすべてのノードの値。
  2. 任意のノードの左部分木と右部分木も二分探索木であり、すなわち条件 1. も満たします。

図 7-16   二分探索木

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#741","level":2,"title":"7.4.1   二分探索木の操作","text":"

二分探索木をクラス BinarySearchTree としてカプセル化し、木の根ノードを指すメンバ変数 root を宣言します。

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#1","level":3,"title":"1.   ノードの探索","text":"

目標ノードの値 num が与えられたら、二分探索木の性質に基づいて探索できます。以下の図に示すように、ノード cur を宣言し、二分木の根ノード root から出発して、ノード値 cur.valnum の大小関係を繰り返し比較します。

  • cur.val < num の場合、目標ノードは cur の右部分木にあるため、cur = cur.right を実行します。
  • cur.val > num の場合、目標ノードは cur の左部分木にあるため、cur = cur.left を実行します。
  • cur.val = num の場合、目標ノードが見つかったことを表し、ループを抜けてそのノードを返します。
<1><2><3><4>

図 7-17   二分探索木のノード探索例

二分探索木の探索操作は二分探索アルゴリズムと同じ原理で動作し、各ラウンドで半分の候補を除外します。ループ回数の上限は二分木の高さであり、二分木が平衡であれば \\(O(\\log n)\\) 時間です。コード例は次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def search(self, num: int) -> TreeNode | None:\n    \"\"\"ノードを探索\"\"\"\n    cur = self._root\n    # ループで探索し、葉ノードを越えたら抜ける\n    while cur is not None:\n        # 目標ノードは cur の右部分木にある\n        if cur.val < num:\n            cur = cur.right\n        # 目標ノードは cur の左部分木にある\n        elif cur.val > num:\n            cur = cur.left\n        # 目標ノードが見つかったらループを抜ける\n        else:\n            break\n    return cur\n
binary_search_tree.cpp
/* ノードを探索 */\nTreeNode *search(int num) {\n    TreeNode *cur = root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != nullptr) {\n        // 目標ノードは cur の右部分木にある\n        if (cur->val < num)\n            cur = cur->right;\n        // 目標ノードは cur の左部分木にある\n        else if (cur->val > num)\n            cur = cur->left;\n        // 目標ノードが見つかったらループを抜ける\n        else\n            break;\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.java
/* ノードを探索 */\nTreeNode search(int num) {\n    TreeNode cur = root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 目標ノードは cur の右部分木にある\n        if (cur.val < num)\n            cur = cur.right;\n        // 目標ノードは cur の左部分木にある\n        else if (cur.val > num)\n            cur = cur.left;\n        // 目標ノードが見つかったらループを抜ける\n        else\n            break;\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.cs
/* ノードを探索 */\nTreeNode? Search(int num) {\n    TreeNode? cur = root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 目標ノードは cur の右部分木にある\n        if (cur.val < num) cur =\n            cur.right;\n        // 目標ノードは cur の左部分木にある\n        else if (cur.val > num)\n            cur = cur.left;\n        // 目標ノードが見つかったらループを抜ける\n        else\n            break;\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.go
/* ノードを探索 */\nfunc (bst *binarySearchTree) search(num int) *TreeNode {\n    node := bst.root\n    // ループで探索し、葉ノードを越えたら抜ける\n    for node != nil {\n        if node.Val.(int) < num {\n            // 目標ノードは cur の右部分木にある\n            node = node.Right\n        } else if node.Val.(int) > num {\n            // 目標ノードは cur の左部分木にある\n            node = node.Left\n        } else {\n            // 目標ノードが見つかったらループを抜ける\n            break\n        }\n    }\n    // 目標ノードを返す\n    return node\n}\n
binary_search_tree.swift
/* ノードを探索 */\nfunc search(num: Int) -> TreeNode? {\n    var cur = root\n    // ループで探索し、葉ノードを越えたら抜ける\n    while cur != nil {\n        // 目標ノードは cur の右部分木にある\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // 目標ノードは cur の左部分木にある\n        else if cur!.val > num {\n            cur = cur?.left\n        }\n        // 目標ノードが見つかったらループを抜ける\n        else {\n            break\n        }\n    }\n    // 目標ノードを返す\n    return cur\n}\n
binary_search_tree.js
/* ノードを探索 */\nsearch(num) {\n    let cur = this.root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 目標ノードは cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 目標ノードは cur の左部分木にある\n        else if (cur.val > num) cur = cur.left;\n        // 目標ノードが見つかったらループを抜ける\n        else break;\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.ts
/* ノードを探索 */\nsearch(num: number): TreeNode | null {\n    let cur = this.root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 目標ノードは cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 目標ノードは cur の左部分木にある\n        else if (cur.val > num) cur = cur.left;\n        // 目標ノードが見つかったらループを抜ける\n        else break;\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.dart
/* ノードを探索 */\nTreeNode? search(int _num) {\n  TreeNode? cur = _root;\n  // ループで探索し、葉ノードを越えたら抜ける\n  while (cur != null) {\n    // 目標ノードは cur の右部分木にある\n    if (cur.val < _num)\n      cur = cur.right;\n    // 目標ノードは cur の左部分木にある\n    else if (cur.val > _num)\n      cur = cur.left;\n    // 目標ノードが見つかったらループを抜ける\n    else\n      break;\n  }\n  // 目標ノードを返す\n  return cur;\n}\n
binary_search_tree.rs
/* ノードを探索 */\npub fn search(&self, num: i32) -> OptionTreeNodeRc {\n    let mut cur = self.root.clone();\n    // ループで探索し、葉ノードを越えたら抜ける\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // 目標ノードは cur の右部分木にある\n            Ordering::Greater => cur = node.borrow().right.clone(),\n            // 目標ノードは cur の左部分木にある\n            Ordering::Less => cur = node.borrow().left.clone(),\n            // 目標ノードが見つかったらループを抜ける\n            Ordering::Equal => break,\n        }\n    }\n\n    // 目標ノードを返す\n    cur\n}\n
binary_search_tree.c
/* ノードを探索 */\nTreeNode *search(BinarySearchTree *bst, int num) {\n    TreeNode *cur = bst->root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != NULL) {\n        if (cur->val < num) {\n            // 目標ノードは cur の右部分木にある\n            cur = cur->right;\n        } else if (cur->val > num) {\n            // 目標ノードは cur の左部分木にある\n            cur = cur->left;\n        } else {\n            // 目標ノードが見つかったらループを抜ける\n            break;\n        }\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.kt
/* ノードを探索 */\nfun search(num: Int): TreeNode? {\n    var cur = root\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 目標ノードは cur の右部分木にある\n        cur = if (cur._val < num)\n            cur.right\n        // 目標ノードは cur の左部分木にある\n        else if (cur._val > num)\n            cur.left\n        // 目標ノードが見つかったらループを抜ける\n        else\n            break\n    }\n    // 目標ノードを返す\n    return cur\n}\n
binary_search_tree.rb
### ノードを検索 ###\ndef search(num)\n  cur = @root\n\n  # ループで探索し、葉ノードを越えたら抜ける\n  while !cur.nil?\n    # 目標ノードは cur の右部分木にある\n    if cur.val < num\n      cur = cur.right\n    # 目標ノードは cur の左部分木にある\n    elsif cur.val > num\n      cur = cur.left\n    # 目標ノードが見つかったらループを抜ける\n    else\n      break\n    end\n  end\n\n  cur\nend\n
コードの可視化

全画面で見る >

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#2","level":3,"title":"2.   ノードの挿入","text":"

挿入する要素 num が与えられたとき、二分探索木の「左部分木 < 根ノード < 右部分木」という性質を保つため、挿入操作の流れは以下の図のようになります。

  1. 挿入位置を探索する:探索操作と同様に、根ノードから出発し、現在のノード値と num の大小関係に基づいて下方向へ探索を繰り返し、葉ノードを越えて(None まで到達して)ループを抜けます。
  2. その位置にノードを挿入する:ノード num を初期化し、そのノードを None の位置に置きます。

図 7-18   二分探索木にノードを挿入する

コード実装では、次の 2 点に注意が必要です。

  • 二分探索木では重複ノードを許可しません。そうでないと定義に反するためです。したがって、挿入対象のノードが木内にすでに存在する場合は、挿入を行わずそのまま返します。
  • ノード挿入を実現するために、ノード pre を用いて前回のループのノードを保持する必要があります。これにより、None までたどり着いたときにその親ノードを取得でき、ノード挿入を完了できます。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def insert(self, num: int):\n    \"\"\"ノードを挿入\"\"\"\n    # 木が空なら、根ノードを初期化する\n    if self._root is None:\n        self._root = TreeNode(num)\n        return\n    # ループで探索し、葉ノードを越えたら抜ける\n    cur, pre = self._root, None\n    while cur is not None:\n        # 重複ノードが見つかったら、直ちに返す\n        if cur.val == num:\n            return\n        pre = cur\n        # 挿入位置は cur の右部分木にある\n        if cur.val < num:\n            cur = cur.right\n        # 挿入位置は cur の左部分木にある\n        else:\n            cur = cur.left\n    # ノードを挿入\n    node = TreeNode(num)\n    if pre.val < num:\n        pre.right = node\n    else:\n        pre.left = node\n
binary_search_tree.cpp
/* ノードを挿入 */\nvoid insert(int num) {\n    // 木が空なら、根ノードを初期化する\n    if (root == nullptr) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode *cur = root, *pre = nullptr;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != nullptr) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur->val == num)\n            return;\n        pre = cur;\n        // 挿入位置は cur の右部分木にある\n        if (cur->val < num)\n            cur = cur->right;\n        // 挿入位置は cur の左部分木にある\n        else\n            cur = cur->left;\n    }\n    // ノードを挿入\n    TreeNode *node = new TreeNode(num);\n    if (pre->val < num)\n        pre->right = node;\n    else\n        pre->left = node;\n}\n
binary_search_tree.java
/* ノードを挿入 */\nvoid insert(int num) {\n    // 木が空なら、根ノードを初期化する\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode cur = root, pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // 挿入位置は cur の右部分木にある\n        if (cur.val < num)\n            cur = cur.right;\n        // 挿入位置は cur の左部分木にある\n        else\n            cur = cur.left;\n    }\n    // ノードを挿入\n    TreeNode node = new TreeNode(num);\n    if (pre.val < num)\n        pre.right = node;\n    else\n        pre.left = node;\n}\n
binary_search_tree.cs
/* ノードを挿入 */\nvoid Insert(int num) {\n    // 木が空なら、根ノードを初期化する\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode? cur = root, pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // 挿入位置は cur の右部分木にある\n        if (cur.val < num)\n            cur = cur.right;\n        // 挿入位置は cur の左部分木にある\n        else\n            cur = cur.left;\n    }\n\n    // ノードを挿入\n    TreeNode node = new(num);\n    if (pre != null) {\n        if (pre.val < num)\n            pre.right = node;\n        else\n            pre.left = node;\n    }\n}\n
binary_search_tree.go
/* ノードを挿入 */\nfunc (bst *binarySearchTree) insert(num int) {\n    cur := bst.root\n    // 木が空なら、根ノードを初期化する\n    if cur == nil {\n        bst.root = NewTreeNode(num)\n        return\n    }\n    // 挿入対象ノードの直前のノード位置\n    var pre *TreeNode = nil\n    // ループで探索し、葉ノードを越えたら抜ける\n    for cur != nil {\n        if cur.Val == num {\n            return\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            cur = cur.Right\n        } else {\n            cur = cur.Left\n        }\n    }\n    // ノードを挿入\n    node := NewTreeNode(num)\n    if pre.Val.(int) < num {\n        pre.Right = node\n    } else {\n        pre.Left = node\n    }\n}\n
binary_search_tree.swift
/* ノードを挿入 */\nfunc insert(num: Int) {\n    // 木が空なら、根ノードを初期化する\n    if root == nil {\n        root = TreeNode(x: num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // ループで探索し、葉ノードを越えたら抜ける\n    while cur != nil {\n        // 重複ノードが見つかったら、直ちに返す\n        if cur!.val == num {\n            return\n        }\n        pre = cur\n        // 挿入位置は cur の右部分木にある\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // 挿入位置は cur の左部分木にある\n        else {\n            cur = cur?.left\n        }\n    }\n    // ノードを挿入\n    let node = TreeNode(x: num)\n    if pre!.val < num {\n        pre?.right = node\n    } else {\n        pre?.left = node\n    }\n}\n
binary_search_tree.js
/* ノードを挿入 */\ninsert(num) {\n    // 木が空なら、根ノードを初期化する\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur = this.root,\n        pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur.val === num) return;\n        pre = cur;\n        // 挿入位置は cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 挿入位置は cur の左部分木にある\n        else cur = cur.left;\n    }\n    // ノードを挿入\n    const node = new TreeNode(num);\n    if (pre.val < num) pre.right = node;\n    else pre.left = node;\n}\n
binary_search_tree.ts
/* ノードを挿入 */\ninsert(num: number): void {\n    // 木が空なら、根ノードを初期化する\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur.val === num) return;\n        pre = cur;\n        // 挿入位置は cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 挿入位置は cur の左部分木にある\n        else cur = cur.left;\n    }\n    // ノードを挿入\n    const node = new TreeNode(num);\n    if (pre!.val < num) pre!.right = node;\n    else pre!.left = node;\n}\n
binary_search_tree.dart
/* ノードを挿入 */\nvoid insert(int _num) {\n  // 木が空なら、根ノードを初期化する\n  if (_root == null) {\n    _root = TreeNode(_num);\n    return;\n  }\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // ループで探索し、葉ノードを越えたら抜ける\n  while (cur != null) {\n    // 重複ノードが見つかったら、直ちに返す\n    if (cur.val == _num) return;\n    pre = cur;\n    // 挿入位置は cur の右部分木にある\n    if (cur.val < _num)\n      cur = cur.right;\n    // 挿入位置は cur の左部分木にある\n    else\n      cur = cur.left;\n  }\n  // ノードを挿入\n  TreeNode? node = TreeNode(_num);\n  if (pre!.val < _num)\n    pre.right = node;\n  else\n    pre.left = node;\n}\n
binary_search_tree.rs
/* ノードを挿入 */\npub fn insert(&mut self, num: i32) {\n    // 木が空なら、根ノードを初期化する\n    if self.root.is_none() {\n        self.root = Some(TreeNode::new(num));\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // 重複ノードが見つかったら、直ちに返す\n            Ordering::Equal => return,\n            // 挿入位置は cur の右部分木にある\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // 挿入位置は cur の左部分木にある\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // ノードを挿入\n    let pre = pre.unwrap();\n    let node = Some(TreeNode::new(num));\n    if num > pre.borrow().val {\n        pre.borrow_mut().right = node;\n    } else {\n        pre.borrow_mut().left = node;\n    }\n}\n
binary_search_tree.c
/* ノードを挿入 */\nvoid insert(BinarySearchTree *bst, int num) {\n    // 木が空なら、根ノードを初期化する\n    if (bst->root == NULL) {\n        bst->root = newTreeNode(num);\n        return;\n    }\n    TreeNode *cur = bst->root, *pre = NULL;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != NULL) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur->val == num) {\n            return;\n        }\n        pre = cur;\n        if (cur->val < num) {\n            // 挿入位置は cur の右部分木にある\n            cur = cur->right;\n        } else {\n            // 挿入位置は cur の左部分木にある\n            cur = cur->left;\n        }\n    }\n    // ノードを挿入\n    TreeNode *node = newTreeNode(num);\n    if (pre->val < num) {\n        pre->right = node;\n    } else {\n        pre->left = node;\n    }\n}\n
binary_search_tree.kt
/* ノードを挿入 */\nfun insert(num: Int) {\n    // 木が空なら、根ノードを初期化する\n    if (root == null) {\n        root = TreeNode(num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode? = null\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur._val == num)\n            return\n        pre = cur\n        // 挿入位置は cur の右部分木にある\n        cur = if (cur._val < num)\n            cur.right\n        // 挿入位置は cur の左部分木にある\n        else\n            cur.left\n    }\n    // ノードを挿入\n    val node = TreeNode(num)\n    if (pre?._val!! < num)\n        pre.right = node\n    else\n        pre.left = node\n}\n
binary_search_tree.rb
### ノードを挿入 ###\ndef insert(num)\n  # 木が空なら、根ノードを初期化する\n  if @root.nil?\n    @root = TreeNode.new(num)\n    return\n  end\n\n  # ループで探索し、葉ノードを越えたら抜ける\n  cur, pre = @root, nil\n  while !cur.nil?\n    # 重複ノードが見つかったら、直ちに返す\n    return if cur.val == num\n\n    pre = cur\n    # 挿入位置は cur の右部分木にある\n    if cur.val < num\n      cur = cur.right\n    # 挿入位置は cur の左部分木にある\n    else\n      cur = cur.left\n    end\n  end\n\n  # ノードを挿入\n  node = TreeNode.new(num)\n  if pre.val < num\n    pre.right = node\n  else\n    pre.left = node\n  end\nend\n
コードの可視化

全画面で見る >

ノード探索と同様に、ノード挿入には \\(O(\\log n)\\) 時間を要します。

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#3","level":3,"title":"3.   ノードの削除","text":"

まず二分木内で目標ノードを見つけ、その後で削除します。ノード挿入と同様に、削除操作の完了後も二分探索木の「左部分木 < 根ノード < 右部分木」という性質が保たれる必要があります。そのため、目標ノードの子ノード数に応じて、0、1、2 の 3 つのケースに分けて対応する削除操作を行います。

以下の図に示すように、削除対象ノードの次数が \\(0\\) のとき、そのノードは葉ノードであり、直接削除できます。

図 7-19   二分探索木でノードを削除する(次数 0 )

以下の図に示すように、削除対象ノードの次数が \\(1\\) のとき、削除対象ノードをその子ノードで置き換えれば十分です。

図 7-20   二分探索木でノードを削除する(次数 1 )

削除対象ノードの次数が \\(2\\) のときは、直接削除できず、別のノードでそのノードを置き換える必要があります。二分探索木の「左部分木 \\(<\\) 根ノード \\(<\\) 右部分木」という性質を保つ必要があるため、このノードには右部分木の最小ノードまたは左部分木の最大ノードを使えます。

右部分木の最小ノード(中順走査で次のノード)を選ぶと仮定すると、削除操作の流れは以下の図のようになります。

  1. 削除対象ノードの「中順走査列」における次のノードを見つけ、tmp と記します。
  2. tmp の値で削除対象ノードの値を上書きし、木の中でノード tmp を再帰的に削除します。
<1><2><3><4>

図 7-21   二分探索木でノードを削除する(次数 2 )

ノード削除操作も同様に \\(O(\\log n)\\) 時間を要します。削除対象ノードの探索に \\(O(\\log n)\\) 時間、中順走査の後続ノードの取得に \\(O(\\log n)\\) 時間が必要です。コード例は次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def remove(self, num: int):\n    \"\"\"ノードを削除\"\"\"\n    # 木が空なら、そのまま早期リターンする\n    if self._root is None:\n        return\n    # ループで探索し、葉ノードを越えたら抜ける\n    cur, pre = self._root, None\n    while cur is not None:\n        # 削除対象のノードが見つかったら、ループを抜ける\n        if cur.val == num:\n            break\n        pre = cur\n        # 削除対象ノードは cur の右部分木にある\n        if cur.val < num:\n            cur = cur.right\n        # 削除対象ノードは cur の左部分木にある\n        else:\n            cur = cur.left\n    # 削除対象ノードがなければそのまま返す\n    if cur is None:\n        return\n\n    # 子ノード数 = 0 or 1\n    if cur.left is None or cur.right is None:\n        # 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        child = cur.left or cur.right\n        # ノード cur を削除する\n        if cur != self._root:\n            if pre.left == cur:\n                pre.left = child\n            else:\n                pre.right = child\n        else:\n            # 削除ノードが根ノードなら、根ノードを再設定\n            self._root = child\n    # 子ノード数 = 2\n    else:\n        # 中順走査における cur の次ノードを取得\n        tmp: TreeNode = cur.right\n        while tmp.left is not None:\n            tmp = tmp.left\n        # ノード tmp を再帰的に削除\n        self.remove(tmp.val)\n        # tmp で cur を上書きする\n        cur.val = tmp.val\n
binary_search_tree.cpp
/* ノードを削除 */\nvoid remove(int num) {\n    // 木が空なら、そのまま早期リターンする\n    if (root == nullptr)\n        return;\n    TreeNode *cur = root, *pre = nullptr;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != nullptr) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur->val == num)\n            break;\n        pre = cur;\n        // 削除対象ノードは cur の右部分木にある\n        if (cur->val < num)\n            cur = cur->right;\n        // 削除対象ノードは cur の左部分木にある\n        else\n            cur = cur->left;\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur == nullptr)\n        return;\n    // 子ノード数 = 0 or 1\n    if (cur->left == nullptr || cur->right == nullptr) {\n        // 子ノード数 = 0 / 1 のとき、child = nullptr / その子ノード\n        TreeNode *child = cur->left != nullptr ? cur->left : cur->right;\n        // ノード cur を削除する\n        if (cur != root) {\n            if (pre->left == cur)\n                pre->left = child;\n            else\n                pre->right = child;\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            root = child;\n        }\n        // メモリを解放する\n        delete cur;\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        TreeNode *tmp = cur->right;\n        while (tmp->left != nullptr) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // ノード tmp を再帰的に削除\n        remove(tmp->val);\n        // tmp で cur を上書きする\n        cur->val = tmpVal;\n    }\n}\n
binary_search_tree.java
/* ノードを削除 */\nvoid remove(int num) {\n    // 木が空なら、そのまま早期リターンする\n    if (root == null)\n        return;\n    TreeNode cur = root, pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // 削除対象ノードは cur の右部分木にある\n        if (cur.val < num)\n            cur = cur.right;\n        // 削除対象ノードは cur の左部分木にある\n        else\n            cur = cur.left;\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur == null)\n        return;\n    // 子ノード数 = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        TreeNode child = cur.left != null ? cur.left : cur.right;\n        // ノード cur を削除する\n        if (cur != root) {\n            if (pre.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            root = child;\n        }\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        TreeNode tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // ノード tmp を再帰的に削除\n        remove(tmp.val);\n        // tmp で cur を上書きする\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.cs
/* ノードを削除 */\nvoid Remove(int num) {\n    // 木が空なら、そのまま早期リターンする\n    if (root == null)\n        return;\n    TreeNode? cur = root, pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // 削除対象ノードは cur の右部分木にある\n        if (cur.val < num)\n            cur = cur.right;\n        // 削除対象ノードは cur の左部分木にある\n        else\n            cur = cur.left;\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur == null)\n        return;\n    // 子ノード数 = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        TreeNode? child = cur.left ?? cur.right;\n        // ノード cur を削除する\n        if (cur != root) {\n            if (pre!.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            root = child;\n        }\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        TreeNode? tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // ノード tmp を再帰的に削除\n        Remove(tmp.val!.Value);\n        // tmp で cur を上書きする\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.go
/* ノードを削除 */\nfunc (bst *binarySearchTree) remove(num int) {\n    cur := bst.root\n    // 木が空なら、そのまま早期リターンする\n    if cur == nil {\n        return\n    }\n    // 削除対象ノードの直前のノード位置\n    var pre *TreeNode = nil\n    // ループで探索し、葉ノードを越えたら抜ける\n    for cur != nil {\n        if cur.Val == num {\n            break\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            // 削除対象ノードは右部分木にある\n            cur = cur.Right\n        } else {\n            // 削除対象ノードは左部分木にある\n            cur = cur.Left\n        }\n    }\n    // 削除対象ノードがなければそのまま返す\n    if cur == nil {\n        return\n    }\n    // 子ノード数は 0 または 1\n    if cur.Left == nil || cur.Right == nil {\n        var child *TreeNode = nil\n        // 削除対象ノードの子ノードを取り出す\n        if cur.Left != nil {\n            child = cur.Left\n        } else {\n            child = cur.Right\n        }\n        // ノード cur を削除する\n        if cur != bst.root {\n            if pre.Left == cur {\n                pre.Left = child\n            } else {\n                pre.Right = child\n            }\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            bst.root = child\n        }\n        // 子ノード数は 2\n    } else {\n        // 中順走査で削除対象ノード `cur` の次のノードを取得する\n        tmp := cur.Right\n        for tmp.Left != nil {\n            tmp = tmp.Left\n        }\n        // ノード tmp を再帰的に削除\n        bst.remove(tmp.Val.(int))\n        // tmp で cur を上書きする\n        cur.Val = tmp.Val\n    }\n}\n
binary_search_tree.swift
/* ノードを削除 */\nfunc remove(num: Int) {\n    // 木が空なら、そのまま早期リターンする\n    if root == nil {\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // ループで探索し、葉ノードを越えたら抜ける\n    while cur != nil {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if cur!.val == num {\n            break\n        }\n        pre = cur\n        // 削除対象ノードは cur の右部分木にある\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // 削除対象ノードは cur の左部分木にある\n        else {\n            cur = cur?.left\n        }\n    }\n    // 削除対象ノードがなければそのまま返す\n    if cur == nil {\n        return\n    }\n    // 子ノード数 = 0 or 1\n    if cur?.left == nil || cur?.right == nil {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        let child = cur?.left ?? cur?.right\n        // ノード cur を削除する\n        if cur !== root {\n            if pre?.left === cur {\n                pre?.left = child\n            } else {\n                pre?.right = child\n            }\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            root = child\n        }\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        var tmp = cur?.right\n        while tmp?.left != nil {\n            tmp = tmp?.left\n        }\n        // ノード tmp を再帰的に削除\n        remove(num: tmp!.val)\n        // tmp で cur を上書きする\n        cur?.val = tmp!.val\n    }\n}\n
binary_search_tree.js
/* ノードを削除 */\nremove(num) {\n    // 木が空なら、そのまま早期リターンする\n    if (this.root === null) return;\n    let cur = this.root,\n        pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur.val === num) break;\n        pre = cur;\n        // 削除対象ノードは cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 削除対象ノードは cur の左部分木にある\n        else cur = cur.left;\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur === null) return;\n    // 子ノード数 = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        const child = cur.left !== null ? cur.left : cur.right;\n        // ノード cur を削除する\n        if (cur !== this.root) {\n            if (pre.left === cur) pre.left = child;\n            else pre.right = child;\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            this.root = child;\n        }\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        let tmp = cur.right;\n        while (tmp.left !== null) {\n            tmp = tmp.left;\n        }\n        // ノード tmp を再帰的に削除\n        this.remove(tmp.val);\n        // tmp で cur を上書きする\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.ts
/* ノードを削除 */\nremove(num: number): void {\n    // 木が空なら、そのまま早期リターンする\n    if (this.root === null) return;\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur.val === num) break;\n        pre = cur;\n        // 削除対象ノードは cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 削除対象ノードは cur の左部分木にある\n        else cur = cur.left;\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur === null) return;\n    // 子ノード数 = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        const child: TreeNode | null =\n            cur.left !== null ? cur.left : cur.right;\n        // ノード cur を削除する\n        if (cur !== this.root) {\n            if (pre!.left === cur) pre!.left = child;\n            else pre!.right = child;\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            this.root = child;\n        }\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        let tmp: TreeNode | null = cur.right;\n        while (tmp!.left !== null) {\n            tmp = tmp!.left;\n        }\n        // ノード tmp を再帰的に削除\n        this.remove(tmp!.val);\n        // tmp で cur を上書きする\n        cur.val = tmp!.val;\n    }\n}\n
binary_search_tree.dart
/* ノードを削除 */\nvoid remove(int _num) {\n  // 木が空なら、そのまま早期リターンする\n  if (_root == null) return;\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // ループで探索し、葉ノードを越えたら抜ける\n  while (cur != null) {\n    // 削除対象のノードが見つかったら、ループを抜ける\n    if (cur.val == _num) break;\n    pre = cur;\n    // 削除対象ノードは cur の右部分木にある\n    if (cur.val < _num)\n      cur = cur.right;\n    // 削除対象ノードは cur の左部分木にある\n    else\n      cur = cur.left;\n  }\n  // 削除対象ノードがない場合は、そのまま返す\n  if (cur == null) return;\n  // 子ノード数 = 0 or 1\n  if (cur.left == null || cur.right == null) {\n    // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n    TreeNode? child = cur.left ?? cur.right;\n    // ノード cur を削除する\n    if (cur != _root) {\n      if (pre!.left == cur)\n        pre.left = child;\n      else\n        pre.right = child;\n    } else {\n      // 削除ノードが根ノードなら、根ノードを再設定\n      _root = child;\n    }\n  } else {\n    // 子ノード数 = 2\n    // 中順走査における cur の次のノードを取得\n    TreeNode? tmp = cur.right;\n    while (tmp!.left != null) {\n      tmp = tmp.left;\n    }\n    // ノード tmp を再帰的に削除\n    remove(tmp.val);\n    // tmp で cur を上書きする\n    cur.val = tmp.val;\n  }\n}\n
binary_search_tree.rs
/* ノードを削除 */\npub fn remove(&mut self, num: i32) {\n    // 木が空なら、そのまま早期リターンする\n    if self.root.is_none() {\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // 削除対象のノードが見つかったら、ループを抜ける\n            Ordering::Equal => break,\n            // 削除対象ノードは cur の右部分木にある\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // 削除対象ノードは cur の左部分木にある\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // 削除対象ノードがなければそのまま返す\n    if cur.is_none() {\n        return;\n    }\n    let cur = cur.unwrap();\n    let (left_child, right_child) = (cur.borrow().left.clone(), cur.borrow().right.clone());\n    match (left_child.clone(), right_child.clone()) {\n        // 子ノード数 = 0 or 1\n        (None, None) | (Some(_), None) | (None, Some(_)) => {\n            // 子ノード数 = 0 / 1 のとき、child = nullptr / その子ノード\n            let child = left_child.or(right_child);\n            let pre = pre.unwrap();\n            // ノード cur を削除する\n            if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) {\n                let left = pre.borrow().left.clone();\n                if left.is_some() && Rc::ptr_eq(left.as_ref().unwrap(), &cur) {\n                    pre.borrow_mut().left = child;\n                } else {\n                    pre.borrow_mut().right = child;\n                }\n            } else {\n                // 削除ノードが根ノードなら、根ノードを再設定\n                self.root = child;\n            }\n        }\n        // 子ノード数 = 2\n        (Some(_), Some(_)) => {\n            // 中順走査における cur の次ノードを取得\n            let mut tmp = cur.borrow().right.clone();\n            while let Some(node) = tmp.clone() {\n                if node.borrow().left.is_some() {\n                    tmp = node.borrow().left.clone();\n                } else {\n                    break;\n                }\n            }\n            let tmp_val = tmp.unwrap().borrow().val;\n            // ノード tmp を再帰的に削除\n            self.remove(tmp_val);\n            // tmp で cur を上書きする\n            cur.borrow_mut().val = tmp_val;\n        }\n    }\n}\n
binary_search_tree.c
/* ノードを削除 */\n// stdio.h を導入しているため、ここでは remove 識別子を使えない\nvoid removeItem(BinarySearchTree *bst, int num) {\n    // 木が空なら、そのまま早期リターンする\n    if (bst->root == NULL)\n        return;\n    TreeNode *cur = bst->root, *pre = NULL;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != NULL) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur->val == num)\n            break;\n        pre = cur;\n        if (cur->val < num) {\n            // 削除対象ノードは root の右部分木にある\n            cur = cur->right;\n        } else {\n            // 削除対象ノードは root の左部分木にある\n            cur = cur->left;\n        }\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur == NULL)\n        return;\n    // 削除対象ノードに子ノードがあるかを判定する\n    if (cur->left == NULL || cur->right == NULL) {\n        /* 子ノード数 = 0 or 1 */\n        // 子ノード数 = 0 / 1 のとき、child = nullptr / その子ノード\n        TreeNode *child = cur->left != NULL ? cur->left : cur->right;\n        // ノード cur を削除する\n        if (pre->left == cur) {\n            pre->left = child;\n        } else {\n            pre->right = child;\n        }\n        // メモリを解放する\n        free(cur);\n    } else {\n        /* 子ノード数 = 2 */\n        // 中順走査における cur の次ノードを取得\n        TreeNode *tmp = cur->right;\n        while (tmp->left != NULL) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // ノード tmp を再帰的に削除\n        removeItem(bst, tmp->val);\n        // tmp で cur を上書きする\n        cur->val = tmpVal;\n    }\n}\n
binary_search_tree.kt
/* ノードを削除 */\nfun remove(num: Int) {\n    // 木が空なら、そのまま早期リターンする\n    if (root == null)\n        return\n    var cur = root\n    var pre: TreeNode? = null\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur._val == num)\n            break\n        pre = cur\n        // 削除対象ノードは cur の右部分木にある\n        cur = if (cur._val < num)\n            cur.right\n        // 削除対象ノードは cur の左部分木にある\n        else\n            cur.left\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur == null)\n        return\n    // 子ノード数 = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        val child = if (cur.left != null)\n            cur.left\n        else\n            cur.right\n        // ノード cur を削除する\n        if (cur != root) {\n            if (pre!!.left == cur)\n                pre.left = child\n            else\n                pre.right = child\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            root = child\n        }\n        // 子ノード数 = 2\n    } else {\n        // 中順走査における cur の次ノードを取得\n        var tmp = cur.right\n        while (tmp!!.left != null) {\n            tmp = tmp.left\n        }\n        // ノード tmp を再帰的に削除\n        remove(tmp._val)\n        // tmp で cur を上書きする\n        cur._val = tmp._val\n    }\n}\n
binary_search_tree.rb
### ノードを削除 ###\ndef remove(num)\n  # 木が空なら、そのまま早期リターンする\n  return if @root.nil?\n\n  # ループで探索し、葉ノードを越えたら抜ける\n  cur, pre = @root, nil\n  while !cur.nil?\n    # 削除対象のノードが見つかったら、ループを抜ける\n    break if cur.val == num\n\n    pre = cur\n    # 削除対象ノードは cur の右部分木にある\n    if cur.val < num\n      cur = cur.right\n    # 削除対象ノードは cur の左部分木にある\n    else\n      cur = cur.left\n    end\n  end\n  # 削除対象ノードがなければそのまま返す\n  return if cur.nil?\n\n  # 子ノード数 = 0 or 1\n  if cur.left.nil? || cur.right.nil?\n    # 子ノード数が 0 / 1 のとき、child = null / その子ノード\n    child = cur.left || cur.right\n    # ノード cur を削除する\n    if cur != @root\n      if pre.left == cur\n        pre.left = child\n      else\n        pre.right = child\n      end\n    else\n      # 削除ノードが根ノードなら、根ノードを再設定\n      @root = child\n    end\n  # 子ノード数 = 2\n  else\n    # 中順走査における cur の次ノードを取得\n    tmp = cur.right\n    while !tmp.left.nil?\n      tmp = tmp.left\n    end\n    # ノード tmp を再帰的に削除\n    remove(tmp.val)\n    # tmp で cur を上書きする\n    cur.val = tmp.val\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#4","level":3,"title":"4.   中順走査は昇順","text":"

以下の図に示すように、二分木の中順走査は「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」という順序に従い、二分探索木は「左子ノード \\(<\\) 根ノード \\(<\\) 右子ノード」という大小関係を満たします。

これは、二分探索木で中順走査を行うと常に次の最小ノードが優先して走査されることを意味し、そこから重要な性質が導かれます。二分探索木の中順走査列は昇順です。

中順走査が昇順になる性質を利用すれば、二分探索木から整列済みデータを取得するのに必要な時間は \\(O(n)\\) のみで、追加のソート操作は不要です。非常に効率的です。

図 7-22   二分探索木の中順走査列

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#742","level":2,"title":"7.4.2   二分探索木の効率","text":"

あるデータ集合が与えられたとき、配列または二分探索木で格納する場合を考えます。次の表を見ると、二分探索木の各操作の時間計算量はいずれも対数オーダーであり、安定して高効率です。高頻度の追加と低頻度の探索・削除という場面でのみ、配列のほうが二分探索木より効率的です。

表 7-2   配列と探索木の効率比較

無秩序配列 二分探索木 要素の探索 \\(O(n)\\) \\(O(\\log n)\\) 要素の挿入 \\(O(1)\\) \\(O(\\log n)\\) 要素の削除 \\(O(n)\\) \\(O(\\log n)\\)

理想的な状況では、二分探索木は「平衡」しており、その場合は \\(\\log n\\) 回のループ内で任意のノードを探索できます。

しかし、二分探索木でノードの挿入と削除を繰り返すと、二分木が以下の図のような連結リストへ退化する可能性があり、このとき各操作の時間計算量も \\(O(n)\\) に退化します。

図 7-23   二分探索木の退化

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#743","level":2,"title":"7.4.3   二分探索木の代表的な応用","text":"
  • システム内の多段インデックスとして用いられ、効率的な探索、挿入、削除操作を実現します。
  • 一部の探索アルゴリズムの基盤データ構造として使われます。
  • データストリームを格納し、その順序状態を保つために使われます。
","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_tree/","level":1,"title":"7.1   二分木","text":"

二分木(binary tree)は非線形データ構造の一種であり、「祖先」と「子孫」の派生関係を表し、「一つを二つに分ける」分割統治の考え方を体現しています。連結リストと同様に、二分木の基本単位はノードであり、各ノードは値、左子ノードへの参照、右子ノードへの参照を含みます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class TreeNode:\n    \"\"\"二分木ノードクラス\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # ノード値\n        self.left: TreeNode | None = None  # 左子ノード参照\n        self.right: TreeNode | None = None # 右子ノード参照\n
/* 二分木ノード構造体 */\nstruct TreeNode {\n    int val;          // ノード値\n    TreeNode *left;   // 左子ノードポインタ\n    TreeNode *right;  // 右子ノードポインタ\n    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}\n};\n
/* 二分木ノードクラス */\nclass TreeNode {\n    int val;         // ノード値\n    TreeNode left;   // 左子ノード参照\n    TreeNode right;  // 右子ノード参照\n    TreeNode(int x) { val = x; }\n}\n
/* 二分木ノードクラス */\nclass TreeNode(int? x) {\n    public int? val = x;    // ノード値\n    public TreeNode? left;  // 左子ノード参照\n    public TreeNode? right; // 右子ノード参照\n}\n
/* 二分木ノード構造体 */\ntype TreeNode struct {\n    Val   int\n    Left  *TreeNode\n    Right *TreeNode\n}\n/* コンストラクタ */\nfunc NewTreeNode(v int) *TreeNode {\n    return &TreeNode{\n        Left:  nil, // 左子ノードポインタ\n        Right: nil, // 右子ノードポインタ\n        Val:   v,   // ノード値\n    }\n}\n
/* 二分木ノードクラス */\nclass TreeNode {\n    var val: Int // ノード値\n    var left: TreeNode? // 左子ノード参照\n    var right: TreeNode? // 右子ノード参照\n\n    init(x: Int) {\n        val = x\n    }\n}\n
/* 二分木ノードクラス */\nclass TreeNode {\n    val; // ノード値\n    left; // 左子ノードポインタ\n    right; // 右子ノードポインタ\n    constructor(val, left, right) {\n        this.val = val === undefined ? 0 : val;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* 二分木ノードクラス */\nclass TreeNode {\n    val: number;\n    left: TreeNode | null;\n    right: TreeNode | null;\n\n    constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val; // ノード値\n        this.left = left === undefined ? null : left; // 左子ノード参照\n        this.right = right === undefined ? null : right; // 右子ノード参照\n    }\n}\n
/* 二分木ノードクラス */\nclass TreeNode {\n  int val;         // ノード値\n  TreeNode? left;  // 左子ノード参照\n  TreeNode? right; // 右子ノード参照\n  TreeNode(this.val, [this.left, this.right]);\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 二分木ノード構造体 */\nstruct TreeNode {\n    val: i32,                               // ノード値\n    left: Option<Rc<RefCell<TreeNode>>>,    // 左子ノード参照\n    right: Option<Rc<RefCell<TreeNode>>>,   // 右子ノード参照\n}\n\nimpl TreeNode {\n    /* コンストラクタ */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            left: None,\n            right: None\n        }))\n    }\n}\n
/* 二分木ノード構造体 */\ntypedef struct TreeNode {\n    int val;                // ノード値\n    int height;             // ノードの高さ\n    struct TreeNode *left;  // 左子ノードポインタ\n    struct TreeNode *right; // 右子ノードポインタ\n} TreeNode;\n\n/* コンストラクタ */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
/* 二分木ノードクラス */\nclass TreeNode(val _val: Int) {  // ノード値\n    val left: TreeNode? = null   // 左子ノード参照\n    val right: TreeNode? = null  // 右子ノード参照\n}\n
### 二分木ノードクラス ###\nclass TreeNode\n  attr_accessor :val    # ノード値\n  attr_accessor :left   # 左子ノード参照\n  attr_accessor :right  # 右子ノード参照\n\n  def initialize(val)\n    @val = val\n  end\nend\n

各ノードは 2 つの参照(ポインタ)を持ち、それぞれ左子ノード(left-child node)と右子ノード(right-child node)を指します。このノードはこれら 2 つの子ノードの親ノード(parent node)と呼ばれます。二分木のあるノードが与えられたとき、そのノードの左子ノードとその配下のノードからなる木をそのノードの左部分木(left subtree)と呼び、同様に右部分木(right subtree)が定義されます。

二分木では、葉ノードを除くすべてのノードが子ノードと空でない部分木を持ちます。以下の図に示すように、「ノード 2」を親ノードとみなすと、その左子ノードと右子ノードはそれぞれ「ノード 4」と「ノード 5」であり、左部分木は「ノード 4 とその配下のノードからなる木」、右部分木は「ノード 5 とその配下のノードからなる木」です。

図 7-1   親ノード、子ノード、部分木

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#711","level":2,"title":"7.1.1   二分木のよく使われる用語","text":"

二分木でよく使われる用語を以下の図に示します。

  • 根ノード(root node):二分木の最上位にあるノードで、親ノードを持ちません。
  • 葉ノード(leaf node):子ノードを持たないノードで、2 本のポインタはいずれも None を指します。
  • 辺(edge):2 つのノードを結ぶ線分、すなわちノード参照(ポインタ)です。
  • ノードが属するレベル(level):上から下へ向かって増加し、根ノードのレベルは 1 です。
  • ノードの次数(degree):ノードの子ノードの数。二分木では次数の取り得る値は 0、1、2 です。
  • 二分木の高さ(height):根ノードから最も遠い葉ノードまでに通る辺の数。
  • ノードの深さ(depth):根ノードからそのノードまでに通る辺の数。
  • ノードの高さ(height):そのノードから最も遠い葉ノードまでに通る辺の数。

図 7-2   二分木のよく使われる用語

Tip

なお、通常「高さ」と「深さ」は「通過した辺の数」と定義しますが、問題や教材によっては「通過したノードの数」と定義する場合もあります。その場合、高さと深さはいずれも 1 を加える必要があります。

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#712","level":2,"title":"7.1.2   二分木の基本操作","text":"","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#1","level":3,"title":"1.   二分木を初期化する","text":"

連結リストと同様に、まずノードを初期化し、その後で参照(ポインタ)を構築します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
# 二分木を初期化する\n# ノードを初期化する\nn1 = TreeNode(val=1)\nn2 = TreeNode(val=2)\nn3 = TreeNode(val=3)\nn4 = TreeNode(val=4)\nn5 = TreeNode(val=5)\n# ノード間の参照(ポインタ)を構築する\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.cpp
/* 二分木を初期化する */\n// ノードを初期化する\nTreeNode* n1 = new TreeNode(1);\nTreeNode* n2 = new TreeNode(2);\nTreeNode* n3 = new TreeNode(3);\nTreeNode* n4 = new TreeNode(4);\nTreeNode* n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
binary_tree.java
// ノードを初期化する\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.cs
/* 二分木を初期化する */\n// ノードを初期化する\nTreeNode n1 = new(1);\nTreeNode n2 = new(2);\nTreeNode n3 = new(3);\nTreeNode n4 = new(4);\nTreeNode n5 = new(5);\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.go
/* 二分木を初期化する */\n// ノードを初期化する\nn1 := NewTreeNode(1)\nn2 := NewTreeNode(2)\nn3 := NewTreeNode(3)\nn4 := NewTreeNode(4)\nn5 := NewTreeNode(5)\n// ノード間の参照(ポインタ)を構築する\nn1.Left = n2\nn1.Right = n3\nn2.Left = n4\nn2.Right = n5\n
binary_tree.swift
// ノードを初期化する\nlet n1 = TreeNode(x: 1)\nlet n2 = TreeNode(x: 2)\nlet n3 = TreeNode(x: 3)\nlet n4 = TreeNode(x: 4)\nlet n5 = TreeNode(x: 5)\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.js
/* 二分木を初期化する */\n// ノードを初期化する\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.ts
/* 二分木を初期化する */\n// ノードを初期化する\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.dart
/* 二分木を初期化する */\n// ノードを初期化する\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.rs
// ノードを初期化する\nlet n1 = TreeNode::new(1);\nlet n2 = TreeNode::new(2);\nlet n3 = TreeNode::new(3);\nlet n4 = TreeNode::new(4);\nlet n5 = TreeNode::new(5);\n// ノード間の参照(ポインタ)を構築する\nn1.borrow_mut().left = Some(n2.clone());\nn1.borrow_mut().right = Some(n3);\nn2.borrow_mut().left = Some(n4);\nn2.borrow_mut().right = Some(n5);\n
binary_tree.c
/* 二分木を初期化する */\n// ノードを初期化する\nTreeNode *n1 = newTreeNode(1);\nTreeNode *n2 = newTreeNode(2);\nTreeNode *n3 = newTreeNode(3);\nTreeNode *n4 = newTreeNode(4);\nTreeNode *n5 = newTreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
binary_tree.kt
// ノードを初期化する\nval n1 = TreeNode(1)\nval n2 = TreeNode(2)\nval n3 = TreeNode(3)\nval n4 = TreeNode(4)\nval n5 = TreeNode(5)\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.rb
# 二分木を初期化する\n# ノードを初期化する\nn1 = TreeNode.new(1)\nn2 = TreeNode.new(2)\nn3 = TreeNode.new(3)\nn4 = TreeNode.new(4)\nn5 = TreeNode.new(5)\n# ノード間の参照(ポインタ)を構築する\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
実行の可視化

https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#2","level":3,"title":"2.   ノードの挿入と削除","text":"

連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。以下の図に 1 つの例を示します。

図 7-3   二分木でノードを挿入・削除する

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
# ノードの挿入と削除\np = TreeNode(0)\n# n1 -> n2 の間にノード P を挿入する\nn1.left = p\np.left = n2\n# ノード P を削除する\nn1.left = n2\n
binary_tree.cpp
/* ノードの挿入と削除 */\nTreeNode* P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1->left = P;\nP->left = n2;\n// ノード P を削除する\nn1->left = n2;\n// メモリを解放する\ndelete P;\n
binary_tree.java
TreeNode P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
binary_tree.cs
/* ノードの挿入と削除 */\nTreeNode P = new(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
binary_tree.go
/* ノードの挿入と削除 */\n// n1 -> n2 の間にノード P を挿入する\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// ノード P を削除する\nn1.Left = n2\n
binary_tree.swift
let P = TreeNode(x: 0)\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P\nP.left = n2\n// ノード P を削除する\nn1.left = n2\n
binary_tree.js
/* ノードの挿入と削除 */\nlet P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
binary_tree.ts
/* ノードの挿入と削除 */\nconst P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
binary_tree.dart
/* ノードの挿入と削除 */\nTreeNode P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
binary_tree.rs
let p = TreeNode::new(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.borrow_mut().left = Some(p.clone());\np.borrow_mut().left = Some(n2.clone());\n// ノード p を削除する\nn1.borrow_mut().left = Some(n2);\n
binary_tree.c
/* ノードの挿入と削除 */\nTreeNode *P = newTreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1->left = P;\nP->left = n2;\n// ノード P を削除する\nn1->left = n2;\n// メモリを解放する\nfree(P);\n
binary_tree.kt
val P = TreeNode(0)\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P\nP.left = n2\n// ノード P を削除する\nn1.left = n2\n
binary_tree.rb
# ノードの挿入と削除\n_p = TreeNode.new(0)\n# n1 -> n2 の間にノード _p を挿入する\nn1.left = _p\n_p.left = n2\n# ノードを削除する\nn1.left = n2\n
実行の可視化

https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20n2&cumulative=false&curInstr=37&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

Tip

注意すべき点として、ノードの挿入は二分木の元の論理構造を変える可能性があり、ノードの削除は通常、そのノードと配下のすべての部分木の削除を意味します。そのため、二分木における挿入と削除は、実際に意味のある操作を実現するために、通常は一連の操作を組み合わせて行います。

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#713","level":2,"title":"7.1.3   一般的な二分木の種類","text":"","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#1_1","level":3,"title":"1.   充足二分木","text":"

以下の図に示すように、充足二分木(perfect binary tree)ではすべてのレベルのノードが完全に埋まっています。充足二分木では、葉ノードの次数は \\(0\\) で、それ以外のすべてのノードの次数は \\(2\\) です。木の高さを \\(h\\) とすると、ノード総数は \\(2^{h+1} - 1\\) となり、標準的な指数関係を示して、自然界でよく見られる細胞分裂の現象を反映しています。

Tip

なお、中国語圏では充足二分木は満二分木と呼ばれることもあります。

図 7-4   充足二分木

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#2_1","level":3,"title":"2.   完全二分木","text":"

以下の図に示すように、完全二分木(complete binary tree)では最下層のノードだけが完全に埋まっていなくてもよく、しかも最下層のノードは左から右へ連続して詰められていなければなりません。なお、充足二分木も完全二分木の一種です。

図 7-5   完全二分木

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#3","level":3,"title":"3.   充満二分木","text":"

以下の図に示すように、充満二分木(full binary tree)では、葉ノードを除くすべてのノードが 2 つの子ノードを持ちます。

図 7-6   充満二分木

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#4","level":3,"title":"4.   平衡二分木","text":"

以下の図に示すように、平衡二分木(balanced binary tree)では、任意のノードについて左部分木と右部分木の高さの差の絶対値が 1 を超えません。

図 7-7   平衡二分木

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#714","level":2,"title":"7.1.4   二分木の退化","text":"

以下の図は、二分木の理想的な構造と退化した構造を示しています。二分木の各レベルのノードがすべて埋まっていると「充足二分木」となり、すべてのノードが片側に偏ると二分木は「連結リスト」へ退化します。

  • 充足二分木は理想的なケースであり、二分木の「分割統治」の利点を十分に発揮できます。
  • 連結リストはその対極にあり、各種操作はすべて線形操作となり、時間計算量は \\(O(n)\\) まで退化します。

図 7-8   二分木の最良構造と最悪構造

以下の表に示すように、最良構造と最悪構造では、二分木の葉ノード数、ノード総数、高さなどが極大または極小になります。

表 7-1   二分木の最良構造と最悪構造

充足二分木 連結リスト 第 \\(i\\) レベルのノード数 \\(2^{i-1}\\) \\(1\\) 高さ \\(h\\) の木の葉ノード数 \\(2^h\\) \\(1\\) 高さ \\(h\\) の木のノード総数 \\(2^{h+1} - 1\\) \\(h + 1\\) ノード総数 \\(n\\) の木の高さ \\(\\log_2 (n+1) - 1\\) \\(n - 1\\)","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/","level":1,"title":"7.2   二分木の走査","text":"

物理構造の観点から見ると、木は連結リストを基盤としたデータ構造であり、その走査はポインタを通じてノードへ順にアクセスすることで行われます。しかし、木は非線形データ構造であるため、木の走査は連結リストの走査よりも複雑であり、検索アルゴリズムを用いて実現する必要があります。

二分木の一般的な走査方法には、レベル順走査、先行順走査、中間順走査、後行順走査などがあります。

","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#721","level":2,"title":"7.2.1   レベル順走査","text":"

次の図に示すように、レベル順走査(level-order traversal)では、二分木を上から下へ層ごとに走査し、各層では左から右の順にノードへアクセスします。

レベル順走査は本質的に幅優先走査(breadth-first traversal)に属し、幅優先探索(breadth-first search, BFS)とも呼ばれます。これは「同心円状に外側へ広がる」ような、層ごとの走査方法を表しています。

図 7-9   二分木のレベル順走査

","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1","level":3,"title":"1.   コードの実装","text":"

幅優先走査は通常「キュー」を用いて実装します。キューは「先入れ先出し」の規則に従い、幅優先走査は「層ごとに進む」という規則に従います。両者の背後にある考え方は一致しています。実装コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_bfs.py
def level_order(root: TreeNode | None) -> list[int]:\n    \"\"\"レベル順走査\"\"\"\n    # キューを初期化し、ルートノードを追加する\n    queue: deque[TreeNode] = deque()\n    queue.append(root)\n    # 走査順序を保存するためのリストを初期化する\n    res = []\n    while queue:\n        node: TreeNode = queue.popleft()  # デキュー\n        res.append(node.val)  # ノードの値を保存する\n        if node.left is not None:\n            queue.append(node.left)  # 左子ノードをキューに追加\n        if node.right is not None:\n            queue.append(node.right)  # 右子ノードをキューに追加\n    return res\n
binary_tree_bfs.cpp
/* レベル順走査 */\nvector<int> levelOrder(TreeNode *root) {\n    // キューを初期化し、ルートノードを追加する\n    queue<TreeNode *> queue;\n    queue.push(root);\n    // 走査順序を保存するためのリストを初期化する\n    vector<int> vec;\n    while (!queue.empty()) {\n        TreeNode *node = queue.front();\n        queue.pop();              // デキュー\n        vec.push_back(node->val); // ノードの値を保存する\n        if (node->left != nullptr)\n            queue.push(node->left); // 左子ノードをキューに追加\n        if (node->right != nullptr)\n            queue.push(node->right); // 右子ノードをキューに追加\n    }\n    return vec;\n}\n
binary_tree_bfs.java
/* レベル順走査 */\nList<Integer> levelOrder(TreeNode root) {\n    // キューを初期化し、ルートノードを追加する\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.add(root);\n    // 走査順序を保存するためのリストを初期化する\n    List<Integer> list = new ArrayList<>();\n    while (!queue.isEmpty()) {\n        TreeNode node = queue.poll(); // デキュー\n        list.add(node.val);           // ノードの値を保存する\n        if (node.left != null)\n            queue.offer(node.left);   // 左子ノードをキューに追加\n        if (node.right != null)\n            queue.offer(node.right);  // 右子ノードをキューに追加\n    }\n    return list;\n}\n
binary_tree_bfs.cs
/* レベル順走査 */\nList<int> LevelOrder(TreeNode root) {\n    // キューを初期化し、ルートノードを追加する\n    Queue<TreeNode> queue = new();\n    queue.Enqueue(root);\n    // 走査順序を保存するためのリストを初期化する\n    List<int> list = [];\n    while (queue.Count != 0) {\n        TreeNode node = queue.Dequeue(); // デキュー\n        list.Add(node.val!.Value);       // ノードの値を保存する\n        if (node.left != null)\n            queue.Enqueue(node.left);    // 左子ノードをキューに追加\n        if (node.right != null)\n            queue.Enqueue(node.right);   // 右子ノードをキューに追加\n    }\n    return list;\n}\n
binary_tree_bfs.go
/* レベル順走査 */\nfunc levelOrder(root *TreeNode) []any {\n    // キューを初期化し、ルートノードを追加する\n    queue := list.New()\n    queue.PushBack(root)\n    // 走査順を保存するためのスライスを初期化する\n    nums := make([]any, 0)\n    for queue.Len() > 0 {\n        // デキュー\n        node := queue.Remove(queue.Front()).(*TreeNode)\n        // ノードの値を保存する\n        nums = append(nums, node.Val)\n        if node.Left != nil {\n            // 左子ノードをキューに追加\n            queue.PushBack(node.Left)\n        }\n        if node.Right != nil {\n            // 右子ノードをキューに追加\n            queue.PushBack(node.Right)\n        }\n    }\n    return nums\n}\n
binary_tree_bfs.swift
/* レベル順走査 */\nfunc levelOrder(root: TreeNode) -> [Int] {\n    // キューを初期化し、ルートノードを追加する\n    var queue: [TreeNode] = [root]\n    // 走査順序を保存するためのリストを初期化する\n    var list: [Int] = []\n    while !queue.isEmpty {\n        let node = queue.removeFirst() // デキュー\n        list.append(node.val) // ノードの値を保存する\n        if let left = node.left {\n            queue.append(left) // 左子ノードをキューに追加\n        }\n        if let right = node.right {\n            queue.append(right) // 右子ノードをキューに追加\n        }\n    }\n    return list\n}\n
binary_tree_bfs.js
/* レベル順走査 */\nfunction levelOrder(root) {\n    // キューを初期化し、ルートノードを追加する\n    const queue = [root];\n    // 走査順序を保存するためのリストを初期化する\n    const list = [];\n    while (queue.length) {\n        let node = queue.shift(); // デキュー\n        list.push(node.val); // ノードの値を保存する\n        if (node.left) queue.push(node.left); // 左子ノードをキューに追加\n        if (node.right) queue.push(node.right); // 右子ノードをキューに追加\n    }\n    return list;\n}\n
binary_tree_bfs.ts
/* レベル順走査 */\nfunction levelOrder(root: TreeNode | null): number[] {\n    // キューを初期化し、ルートノードを追加する\n    const queue = [root];\n    // 走査順序を保存するためのリストを初期化する\n    const list: number[] = [];\n    while (queue.length) {\n        let node = queue.shift() as TreeNode; // デキュー\n        list.push(node.val); // ノードの値を保存する\n        if (node.left) {\n            queue.push(node.left); // 左子ノードをキューに追加\n        }\n        if (node.right) {\n            queue.push(node.right); // 右子ノードをキューに追加\n        }\n    }\n    return list;\n}\n
binary_tree_bfs.dart
/* レベル順走査 */\nList<int> levelOrder(TreeNode? root) {\n  // キューを初期化し、ルートノードを追加する\n  Queue<TreeNode?> queue = Queue();\n  queue.add(root);\n  // 走査順序を保存するためのリストを初期化する\n  List<int> res = [];\n  while (queue.isNotEmpty) {\n    TreeNode? node = queue.removeFirst(); // デキュー\n    res.add(node!.val); // ノードの値を保存する\n    if (node.left != null) queue.add(node.left); // 左子ノードをキューに追加\n    if (node.right != null) queue.add(node.right); // 右子ノードをキューに追加\n  }\n  return res;\n}\n
binary_tree_bfs.rs
/* レベル順走査 */\nfn level_order(root: &Rc<RefCell<TreeNode>>) -> Vec<i32> {\n    // キューを初期化し、ルートノードを追加する\n    let mut que = VecDeque::new();\n    que.push_back(root.clone());\n    // 走査順序を保存するためのリストを初期化する\n    let mut vec = Vec::new();\n\n    while let Some(node) = que.pop_front() {\n        // デキュー\n        vec.push(node.borrow().val); // ノードの値を保存する\n        if let Some(left) = node.borrow().left.as_ref() {\n            que.push_back(left.clone()); // 左子ノードをキューに追加\n        }\n        if let Some(right) = node.borrow().right.as_ref() {\n            que.push_back(right.clone()); // 右子ノードをキューに追加\n        };\n    }\n    vec\n}\n
binary_tree_bfs.c
/* レベル順走査 */\nint *levelOrder(TreeNode *root, int *size) {\n    /* 補助キュー */\n    int front, rear;\n    int index, *arr;\n    TreeNode *node;\n    TreeNode **queue;\n\n    /* 補助キュー */\n    queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE);\n    // キューへのポインタ\n    front = 0, rear = 0;\n    // 根ノードを追加する\n    queue[rear++] = root;\n    // 走査順序を保存するためのリストを初期化する\n    /* 補助配列 */\n    arr = (int *)malloc(sizeof(int) * MAX_SIZE);\n    // 配列ポインタ\n    index = 0;\n    while (front < rear) {\n        // デキュー\n        node = queue[front++];\n        // ノードの値を保存する\n        arr[index++] = node->val;\n        if (node->left != NULL) {\n            // 左子ノードをキューに追加\n            queue[rear++] = node->left;\n        }\n        if (node->right != NULL) {\n            // 右子ノードをキューに追加\n            queue[rear++] = node->right;\n        }\n    }\n    // 配列長の値を更新\n    *size = index;\n    arr = realloc(arr, sizeof(int) * (*size));\n\n    // 補助配列の領域を解放する\n    free(queue);\n    return arr;\n}\n
binary_tree_bfs.kt
/* レベル順走査 */\nfun levelOrder(root: TreeNode?): MutableList<Int> {\n    // キューを初期化し、ルートノードを追加する\n    val queue = LinkedList<TreeNode?>()\n    queue.add(root)\n    // 走査順序を保存するためのリストを初期化する\n    val list = mutableListOf<Int>()\n    while (queue.isNotEmpty()) {\n        val node = queue.poll()      // デキュー\n        list.add(node?._val!!)       // ノードの値を保存する\n        if (node.left != null)\n            queue.offer(node.left)   // 左子ノードをキューに追加\n        if (node.right != null)\n            queue.offer(node.right)  // 右子ノードをキューに追加\n    }\n    return list\n}\n
binary_tree_bfs.rb
### レベル順走査 ###\ndef level_order(root)\n  # キューを初期化し、ルートノードを追加する\n  queue = [root]\n  # 走査順序を保存するためのリストを初期化する\n  res = []\n  while !queue.empty?\n    node = queue.shift # デキュー\n    res << node.val # ノードの値を保存する\n    queue << node.left unless node.left.nil? # 左子ノードをキューに追加\n    queue << node.right unless node.right.nil? # 右子ノードをキューに追加\n  end\n  res\nend\n
コードの可視化

全画面で見る >

","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2","level":3,"title":"2.   計算量","text":"
  • 時間計算量は \\(O(n)\\) :すべてのノードを1回ずつ訪問するため、計算量は \\(O(n)\\) です。ここで、\\(n\\) はノード数です。
  • 空間計算量は \\(O(n)\\) :最悪の場合、すなわち完全二分木では、最下層に到達する前に、キュー内には最大で同時に \\((n + 1) / 2\\) 個のノードが存在し、\\(O(n)\\) の空間を使用します。
","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#722","level":2,"title":"7.2.2   先行順・中間順・後行順走査","text":"

同様に、先行順・中間順・後行順走査はいずれも深度優先走査(depth-first traversal)に属し、深度優先探索(depth-first search, DFS)とも呼ばれます。これは「まず行き止まりまで進み、その後で戻って続ける」という走査方法を表しています。

次の図は、二分木に対して深度優先走査を行う仕組みを示しています。深度優先走査は、二分木全体の外周をぐるりと「一周する」ようなものです。各ノードでは3つの位置に出会い、それぞれが先行順走査・中間順走査・後行順走査に対応します。

図 7-10   二分探索木の先行順・中間順・後行順走査

","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1_1","level":3,"title":"1.   コードの実装","text":"

深度優先探索は通常、再帰に基づいて実装されます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_dfs.py
def pre_order(root: TreeNode | None):\n    \"\"\"先行順走査\"\"\"\n    if root is None:\n        return\n    # 訪問順序:根ノード -> 左部分木 -> 右部分木\n    res.append(root.val)\n    pre_order(root=root.left)\n    pre_order(root=root.right)\n\ndef in_order(root: TreeNode | None):\n    \"\"\"中順走査\"\"\"\n    if root is None:\n        return\n    # 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    in_order(root=root.left)\n    res.append(root.val)\n    in_order(root=root.right)\n\ndef post_order(root: TreeNode | None):\n    \"\"\"後順走査\"\"\"\n    if root is None:\n        return\n    # 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    post_order(root=root.left)\n    post_order(root=root.right)\n    res.append(root.val)\n
binary_tree_dfs.cpp
/* 先行順走査 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    vec.push_back(root->val);\n    preOrder(root->left);\n    preOrder(root->right);\n}\n\n/* 中順走査 */\nvoid inOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root->left);\n    vec.push_back(root->val);\n    inOrder(root->right);\n}\n\n/* 後順走査 */\nvoid postOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root->left);\n    postOrder(root->right);\n    vec.push_back(root->val);\n}\n
binary_tree_dfs.java
/* 先行順走査 */\nvoid preOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.add(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* 中順走査 */\nvoid inOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root.left);\n    list.add(root.val);\n    inOrder(root.right);\n}\n\n/* 後順走査 */\nvoid postOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root.left);\n    postOrder(root.right);\n    list.add(root.val);\n}\n
binary_tree_dfs.cs
/* 先行順走査 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) return;\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.Add(root.val!.Value);\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n\n/* 中順走査 */\nvoid InOrder(TreeNode? root) {\n    if (root == null) return;\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    InOrder(root.left);\n    list.Add(root.val!.Value);\n    InOrder(root.right);\n}\n\n/* 後順走査 */\nvoid PostOrder(TreeNode? root) {\n    if (root == null) return;\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    PostOrder(root.left);\n    PostOrder(root.right);\n    list.Add(root.val!.Value);\n}\n
binary_tree_dfs.go
/* 先行順走査 */\nfunc preOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    nums = append(nums, node.Val)\n    preOrder(node.Left)\n    preOrder(node.Right)\n}\n\n/* 中順走査 */\nfunc inOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(node.Left)\n    nums = append(nums, node.Val)\n    inOrder(node.Right)\n}\n\n/* 後順走査 */\nfunc postOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(node.Left)\n    postOrder(node.Right)\n    nums = append(nums, node.Val)\n}\n
binary_tree_dfs.swift
/* 先行順走査 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.append(root.val)\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n\n/* 中順走査 */\nfunc inOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root: root.left)\n    list.append(root.val)\n    inOrder(root: root.right)\n}\n\n/* 後順走査 */\nfunc postOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root: root.left)\n    postOrder(root: root.right)\n    list.append(root.val)\n}\n
binary_tree_dfs.js
/* 先行順走査 */\nfunction preOrder(root) {\n    if (root === null) return;\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* 中順走査 */\nfunction inOrder(root) {\n    if (root === null) return;\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* 後順走査 */\nfunction postOrder(root) {\n    if (root === null) return;\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
binary_tree_dfs.ts
/* 先行順走査 */\nfunction preOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* 中順走査 */\nfunction inOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* 後順走査 */\nfunction postOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
binary_tree_dfs.dart
/* 先行順走査 */\nvoid preOrder(TreeNode? node) {\n  if (node == null) return;\n  // 訪問順序:根ノード -> 左部分木 -> 右部分木\n  list.add(node.val);\n  preOrder(node.left);\n  preOrder(node.right);\n}\n\n/* 中順走査 */\nvoid inOrder(TreeNode? node) {\n  if (node == null) return;\n  // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n  inOrder(node.left);\n  list.add(node.val);\n  inOrder(node.right);\n}\n\n/* 後順走査 */\nvoid postOrder(TreeNode? node) {\n  if (node == null) return;\n  // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n  postOrder(node.left);\n  postOrder(node.right);\n  list.add(node.val);\n}\n
binary_tree_dfs.rs
/* 先行順走査 */\nfn pre_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // 訪問順序:根ノード -> 左部分木 -> 右部分木\n            let node = node.borrow();\n            res.push(node.val);\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* 中順走査 */\nfn in_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            res.push(node.val);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* 後順走査 */\nfn post_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n            res.push(node.val);\n        }\n    }\n\n    dfs(root, &mut result);\n\n    result\n}\n
binary_tree_dfs.c
/* 先行順走査 */\nvoid preOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    arr[(*size)++] = root->val;\n    preOrder(root->left, size);\n    preOrder(root->right, size);\n}\n\n/* 中順走査 */\nvoid inOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root->left, size);\n    arr[(*size)++] = root->val;\n    inOrder(root->right, size);\n}\n\n/* 後順走査 */\nvoid postOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root->left, size);\n    postOrder(root->right, size);\n    arr[(*size)++] = root->val;\n}\n
binary_tree_dfs.kt
/* 先行順走査 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) return\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.add(root._val)\n    preOrder(root.left)\n    preOrder(root.right)\n}\n\n/* 中順走査 */\nfun inOrder(root: TreeNode?) {\n    if (root == null) return\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root.left)\n    list.add(root._val)\n    inOrder(root.right)\n}\n\n/* 後順走査 */\nfun postOrder(root: TreeNode?) {\n    if (root == null) return\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root.left)\n    postOrder(root.right)\n    list.add(root._val)\n}\n
binary_tree_dfs.rb
### 前順走査 ###\ndef pre_order(root)\n  return if root.nil?\n\n  # 訪問順序:根ノード -> 左部分木 -> 右部分木\n  $res << root.val\n  pre_order(root.left)\n  pre_order(root.right)\nend\n\n### 中順走査 ###\ndef in_order(root)\n  return if root.nil?\n\n  # 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n  in_order(root.left)\n  $res << root.val\n  in_order(root.right)\nend\n\n### 後順走査 ###\ndef post_order(root)\n  return if root.nil?\n\n  # 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n  post_order(root.left)\n  post_order(root.right)\n  $res << root.val\nend\n
コードの可視化

全画面で見る >

Tip

深度優先探索は反復によって実装することもできます。興味のある読者は自身で調べてみてください。

次の図は、二分木の先行順走査における再帰の過程を示しており、「行き」と「帰り」という2つの逆向きの部分に分けられます。

  1. 「行き」は新しいメソッドの開始を表し、この過程でプログラムは次のノードにアクセスします。
  2. 「帰り」は関数の復帰を表し、現在のノードへのアクセスが完了したことを意味します。
<1><2><3><4><5><6><7><8><9><10><11>

図 7-11   先行順走査の再帰過程

","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2_1","level":3,"title":"2.   計算量","text":"
  • 時間計算量は \\(O(n)\\) :すべてのノードを1回ずつ訪問するため、計算量は \\(O(n)\\) です。
  • 空間計算量は \\(O(n)\\) :最悪の場合、すなわち木が連結リストに退化したとき、再帰の深さは \\(n\\) に達し、システムは \\(O(n)\\) のスタックフレーム空間を使用します。
","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/summary/","level":1,"title":"7.6   まとめ","text":"","path":["第 7 章   木","7.6   まとめ"],"tags":[]},{"location":"chapter_tree/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • 二分木は非線形データ構造の一種であり、「二分する」分割統治の考え方を体現している。各二分木ノードは 1 つの値と 2 本のポインタを持ち、それぞれ左子ノードと右子ノードを指す。
  • 二分木のあるノードについて、その左(右)子ノードおよびその配下から構成される木を、そのノードの左(右)部分木と呼ぶ。
  • 二分木に関する用語には、根ノード、葉ノード、レベル、次数、辺、高さ、深さなどがある。
  • 二分木の初期化、ノードの挿入、ノードの削除は、連結リストの操作方法と似ている。
  • 一般的な二分木の種類には、perfect 二分木、complete 二分木、full 二分木、平衡二分木がある。perfect 二分木が最も理想的な状態であり、連結リストは退化後の最悪の状態である。
  • 二分木は配列で表現できる。方法としては、ノード値と空き位置をレベル順走査の順に並べ、親ノードと子ノードのインデックス対応関係に基づいてポインタを実現する。
  • 二分木のレベル順走査は幅優先探索の一種であり、「同心円状に外へ広がる」ような逐次的な走査方式を表しており、通常はキューによって実装される。
  • 前順、中順、後順走査はいずれも深さ優先探索に属し、「まず末端まで進み、その後バックトラックして続ける」という走査方式を体現しており、通常は再帰で実装される。
  • 二分探索木は効率的な要素探索データ構造であり、探索、挿入、削除の時間計算量はいずれも \\(O(\\log n)\\) である。二分探索木が連結リストへ退化すると、各操作の時間計算量は \\(O(n)\\) まで悪化する。
  • AVL 木は平衡二分探索木とも呼ばれ、回転操作によって、ノードの挿入と削除を繰り返した後も木が平衡を保つようにしている。
  • AVL 木の回転操作には、右回転、左回転、右回転してから左回転、左回転してから右回転がある。ノードの挿入または削除の後、AVL 木は下から上へ回転操作を行い、木を再び平衡状態に戻す。
","path":["第 7 章   木","7.6   まとめ"],"tags":[]},{"location":"chapter_tree/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:ノードが 1 つしかない二分木では、木の高さと根ノードの深さはどちらも \\(0\\) ですか?

はい。高さと深さは通常「通過した辺の本数」として定義されるからです。

Q:二分木における挿入と削除は通常一連の操作を組み合わせて完了しますが、ここでいう「一連の操作」とは何を指すのでしょうか?リソースの子ノードに対するリソース解放と理解できますか?

二分探索木を例にすると、ノード削除は 3 つのケースに分けて処理する必要があり、各ケースで複数段階のノード操作が必要になります。

Q:なぜ DFS による二分木走査には前順・中順・後順の 3 種類があり、それぞれどのような用途があるのですか?

配列の順方向走査と逆方向走査に似て、前順・中順・後順走査は二分木の 3 つの走査方法であり、特定の順序で走査結果を得るために使えます。たとえば二分探索木では、ノードの大小関係が 左子ノードの値 < 根ノードの値 < 右子ノードの値 を満たすため、「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」の優先順で木を走査すれば、整列済みのノード列を得られます。

Q:右回転操作は不平衡ノード nodechildgrand_child の関係を処理するものですが、node の親ノードと node の元の接続は維持しなくてよいのですか?右回転後に切れてしまいませんか?

この問題は再帰の視点から考える必要があります。右回転操作 right_rotate(root) に渡されるのは部分木の根ノードであり、最終的に return child によって回転後の部分木の根ノードを返します。部分木の根ノードとその親ノードの接続は、この関数の返却後に行われるため、右回転操作自身が管理する範囲には含まれません。

Q:C++ では関数を privatepublic に分けますが、この設計にはどのような考えがありますか?なぜ height() 関数と updateHeight() 関数をそれぞれ publicprivate に置くのですか?

主に、そのメソッドの利用範囲を見て決めます。メソッドがクラス内部でしか使われないなら、private に設計します。たとえば、利用者が updateHeight() を単独で呼び出しても意味はなく、これは挿入や削除の途中の 1 ステップにすぎません。一方で height() はノードの高さにアクセスするためのもので、vector.size() に似た役割を持つため、使いやすいように public に設定します。

Q:入力データの集合から二分探索木をどのように構築しますか?根ノードの選び方は重要ですか?

はい。木の構築方法は、二分探索木のコード中の build_tree() メソッドですでに示されています。根ノードの選択については、通常は入力データをソートし、その中央の要素を根ノードにしてから、左右の部分木を再帰的に構築します。こうすることで、木の平衡性を最大限に保てます。

Q:Java では、文字列比較には必ず equals() メソッドを使うべきですか?

Java では、基本データ型については == を使って 2 つの変数の値が等しいかどうかを比較します。参照型については、この 2 つの記法の働き方は異なります。

  • == :2 つの変数が同じオブジェクトを指しているか、つまりメモリ上の位置が同じかどうかを比較するために使います。
  • equals():2 つのオブジェクトの値が等しいかどうかを比較するために使います。

したがって、値を比較したい場合は equals() を使うべきです。ただし、String a = \"hi\"; String b = \"hi\"; によって初期化された文字列は文字列定数プールに格納され、同じオブジェクトを指すため、a == b でも 2 つの文字列の内容を比較できます。

Q:幅優先走査で最下層に到達する前、キュー内のノード数は \\(2^h\\) ですか?

はい。たとえば高さ \\(h = 2\\) の充足二分木では、ノード総数は \\(n = 7\\) であり、最下層のノード数は \\(4 = 2^h = (n + 1) / 2\\) です。

","path":["第 7 章   木","7.6   まとめ"],"tags":[]}]} \ No newline at end of file +{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"第 16 章   付録","text":"","path":["第 16 章   付録"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"章の内容","text":"
  • 16.1   プログラミング環境のインストール
  • 16.2   一緒に制作に参加しましょう
  • 16.3   用語集
","path":["第 16 章   付録"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   一緒に制作に参加しましょう","text":"

著者の力には限りがあるため、本書にはどうしても一部の漏れや誤りが含まれる可能性があります。ご了承ください。誤字、リンク切れ、内容の欠落、表現の曖昧さ、説明の不明瞭さ、文章構成の不適切さなどの問題を見つけた場合は、ぜひ修正にご協力ください。読者により良い学習リソースを提供できます。

すべての寄稿者の GitHub ID は、本書のリポジトリ、Web 版、PDF 版のホームページに掲載され、オープンソースコミュニティへの惜しみない貢献に感謝を表します。

オープンソースの魅力

紙の書籍では、2 回の増刷の間隔が長くなりがちで、内容更新は非常に不便です。

一方、このオープンソース書籍では、内容更新のサイクルは数日、場合によっては数時間にまで短縮されています。

","path":["第 16 章   付録","16.2   一緒に制作に参加しましょう"],"tags":[]},{"location":"chapter_appendix/contribution/#1","level":3,"title":"1.   内容の微調整","text":"

以下の図のように、各ページの右上には「編集アイコン」があります。次の手順で本文やコードを修正できます。

  1. 「編集アイコン」をクリックし、「このリポジトリを Fork する必要があります」と表示された場合は、その操作を承認してください。
  2. Markdown のソースファイルを修正し、内容が正しいことを確認したうえで、できるだけ書式の統一を保ってください。
  3. ページ下部に修正内容の説明を入力し、その後「Propose file change」ボタンをクリックします。ページ遷移後、「Create pull request」ボタンをクリックするとプルリクエストを作成できます。

図 16-3   ページ編集ボタン

画像は直接修正できないため、新しい Issue を作成するかコメントで問題を説明してください。できるだけ早く描き直して差し替えます。

","path":["第 16 章   付録","16.2   一緒に制作に参加しましょう"],"tags":[]},{"location":"chapter_appendix/contribution/#2","level":3,"title":"2.   コンテンツ制作","text":"

コードを他のプログラミング言語へ翻訳することや、記事内容を拡充することなど、このオープンソースプロジェクトへの参加に興味がある場合は、以下の Pull Request ワークフローに従ってください。

  1. GitHub にログインし、本書のコードリポジトリを個人アカウントに Fork します。
  2. Fork したリポジトリのページに入り、git clone コマンドを使ってリポジトリをローカルにクローンします。
  3. ローカルでコンテンツを作成し、完全なテストを行ってコードの正しさを確認します。
  4. ローカルで行った変更を Commit し、その後リモートリポジトリへ Push します。
  5. リポジトリのページを更新し、「Create pull request」ボタンをクリックするとプルリクエストを作成できます。
","path":["第 16 章   付録","16.2   一緒に制作に参加しましょう"],"tags":[]},{"location":"chapter_appendix/contribution/#3-docker","level":3,"title":"3.   Docker デプロイ","text":"

hello-algo のルートディレクトリで以下の Docker スクリプトを実行すると、http://localhost:8000 で本プロジェクトにアクセスできます。

docker-compose up -d\n

以下のコマンドでデプロイを削除できます。

docker-compose down\n
","path":["第 16 章   付録","16.2   一緒に制作に参加しましょう"],"tags":[]},{"location":"chapter_appendix/installation/","level":1,"title":"16.1   プログラミング環境のインストール","text":"","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#1611-ide","level":2,"title":"16.1.1   IDE のインストール","text":"

オープンソースで軽量な VS Code をローカルの統合開発環境(IDE)として使用することを推奨します。VS Code 公式サイト にアクセスし、使用している OS に応じたバージョンの VS Code をダウンロードしてインストールしてください。

図 16-1   公式サイトから VS Code をダウンロード

VS Code には強力な拡張機能のエコシステムがあり、ほとんどのプログラミング言語の実行とデバッグをサポートしています。Python を例にすると、「Python Extension Pack」拡張機能をインストールした後、Python コードをデバッグできるようになります。インストール手順を以下に示します。

図 16-2   VS Code 拡張機能のインストール

","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#1612","level":2,"title":"16.1.2   言語環境のインストール","text":"","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#1-python","level":3,"title":"1.   Python 環境","text":"
  1. Miniconda3 をダウンロードしてインストールします。Python 3.10 以降が必要です。
  2. VS Code の拡張機能マーケットプレイスで python を検索し、Python Extension Pack をインストールします。
  3. (任意)コマンドラインで pip install black を入力し、コード整形ツールをインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#2-cc","level":3,"title":"2.   C/C++ 環境","text":"
  1. Windows システムでは MinGW をインストールする必要があります(設定チュートリアル)。MacOS には Clang が標準搭載されているため、追加インストールは不要です。
  2. VS Code の拡張機能マーケットプレイスで c++ を検索し、C/C++ Extension Pack をインストールします。
  3. (任意)Settings ページを開き、コード整形オプション Clang_format_fallback Style を検索して、{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach } に設定します。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#3-java","level":3,"title":"3.   Java 環境","text":"
  1. OpenJDK をダウンロードしてインストールします(バージョンは JDK 9 より新しい必要があります)。
  2. VS Code の拡張機能マーケットプレイスで java を検索し、Extension Pack for Java をインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#4-c","level":3,"title":"4.   C# 環境","text":"
  1. .Net 8.0 をダウンロードしてインストールします。
  2. VS Code の拡張機能マーケットプレイスで C# Dev Kit を検索し、C# Dev Kit をインストールします(設定チュートリアル)。
  3. Visual Studio を使用することもできます(インストール手順)。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#5-go","level":3,"title":"5.   Go 環境","text":"
  1. go をダウンロードしてインストールします。
  2. VS Code の拡張機能マーケットプレイスで go を検索し、Go をインストールします。
  3. ショートカットキー Ctrl + Shift + P を押してコマンドパレットを開き、go と入力して Go: Install/Update Tools を選択し、すべてにチェックを入れてインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#6-swift","level":3,"title":"6.   Swift 環境","text":"
  1. Swift をダウンロードしてインストールします。
  2. VS Code の拡張機能マーケットプレイスで swift を検索し、Swift for Visual Studio Code をインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#7-javascript","level":3,"title":"7.   JavaScript 環境","text":"
  1. Node.js をダウンロードしてインストールします。
  2. (任意)VS Code の拡張機能マーケットプレイスで Prettier を検索し、コード整形ツールをインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#8-typescript","level":3,"title":"8.   TypeScript 環境","text":"
  1. JavaScript 環境と同じ手順でインストールします。
  2. TypeScript Execute (tsx) をインストールします。
  3. VS Code の拡張機能マーケットプレイスで typescript を検索し、Pretty TypeScript Errors をインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#9-dart","level":3,"title":"9.   Dart 環境","text":"
  1. Dart をダウンロードしてインストールします。
  2. VS Code の拡張機能マーケットプレイスで dart を検索し、Dart をインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/installation/#10-rust","level":3,"title":"10.   Rust 環境","text":"
  1. Rust をダウンロードしてインストールします。
  2. VS Code の拡張機能マーケットプレイスで rust を検索し、rust-analyzer をインストールします。
","path":["第 16 章   付録","16.1   プログラミング環境のインストール"],"tags":[]},{"location":"chapter_appendix/terminology/","level":1,"title":"16.3   用語集","text":"

以下の表は、本書に登場する重要な用語を一覧にしたものです。特に次の点に注意してください。

  • 名詞の英語表現も覚えておくと、英語文献を読む際に役立ちます。
  • 一部の名詞は、簡体字中国語と繁体字中国語で呼び方が異なります。

表 16-1   データ構造とアルゴリズムの重要用語

English 日本語 日本語 algorithm アルゴリズム アルゴリズム data structure データ構造 データ構造 code コード コード file ファイル ファイル function 関数 関数 method メソッド メソッド variable 変数 変数 asymptotic complexity analysis 漸近計算量解析 漸近計算量解析 time complexity 時間計算量 時間計算量 space complexity 空間計算量 空間計算量 loop ループ ループ iteration 反復 反復 recursion 再帰 再帰 tail recursion 末尾再帰 末尾再帰 recursion tree 再帰木 再帰木 big-\\(O\\) notation ビッグオー記法 ビッグオー記法 asymptotic upper bound 漸近上界 漸近上界 sign-magnitude 符号絶対値表現 符号絶対値表現 1’s complement 1の補数 1の補数 2’s complement 2の補数 2の補数 array 配列 配列 index インデックス インデックス linked list 連結リスト 連結リスト linked list node, list node 連結リストノード 連結リストノード head node 先頭ノード 先頭ノード tail node 末尾ノード 末尾ノード list リスト リスト dynamic array 動的配列 動的配列 hard disk ハードディスク ハードディスク random-access memory (RAM) メモリ メモリ cache memory キャッシュ キャッシュ cache miss キャッシュミス キャッシュミス cache hit rate キャッシュヒット率 キャッシュヒット率 stack スタック スタック top of the stack スタックトップ スタックトップ bottom of the stack スタックボトム スタックボトム queue キュー キュー double-ended queue 両端キュー 両端キュー front of the queue キュー先頭 キュー先頭 rear of the queue キュー末尾 キュー末尾 hash table ハッシュテーブル ハッシュテーブル hash set ハッシュ集合 ハッシュ集合 bucket バケット バケット hash function ハッシュ関数 ハッシュ関数 hash collision ハッシュ衝突 ハッシュ衝突 load factor 負荷率 負荷率 separate chaining 連鎖アドレス法 連鎖アドレス法 open addressing オープンアドレス法 オープンアドレス法 linear probing 線形探索 線形探索 lazy deletion 遅延削除 遅延削除 binary tree 二分木 二分木 tree node ノード ノード left-child node 左子ノード 左子ノード right-child node 右子ノード 右子ノード parent node 親ノード 親ノード left subtree 左部分木 左部分木 right subtree 右部分木 右部分木 root node 根ノード 根ノード leaf node 葉ノード 葉ノード edge 辺 辺 level レベル レベル degree 次数 次数 height 高さ 高さ depth 深さ 深さ perfect binary tree 完備二分木 完備二分木 complete binary tree 完全二分木 完全二分木 full binary tree 満二分木 満二分木 balanced binary tree 平衡二分木 平衡二分木 binary search tree 二分探索木 二分探索木 AVL tree AVL 木 AVL 木 red-black tree 赤黒木 赤黒木 level-order traversal レベル順走査 レベル順走査 breadth-first traversal 幅優先走査 幅優先走査 depth-first traversal 深さ優先走査 深さ優先走査 binary search tree 二分探索木 二分探索木 balanced binary search tree 平衡二分探索木 平衡二分探索木 balance factor 平衡係数 平衡係数 heap ヒープ ヒープ max heap 最大ヒープ 最大ヒープ min heap 最小ヒープ 最小ヒープ priority queue 優先度付きキュー 優先度付きキュー heapify ヒープ化 ヒープ化 top-\\(k\\) problem Top-\\(k\\) 問題 Top-\\(k\\) 問題 graph グラフ グラフ vertex 頂点 頂点 undirected graph 無向グラフ 無向グラフ directed graph 有向グラフ 有向グラフ connected graph 連結グラフ 連結グラフ disconnected graph 非連結グラフ 非連結グラフ weighted graph 重み付きグラフ 重み付きグラフ adjacency 隣接 隣接 path 経路 経路 in-degree 入次数 入次数 out-degree 出次数 出次数 adjacency matrix 隣接行列 隣接行列 adjacency list 隣接リスト 隣接リスト breadth-first search 幅優先探索 幅優先探索 depth-first search 深さ優先探索 深さ優先探索 binary search 二分探索 二分探索 searching algorithm 探索アルゴリズム 探索アルゴリズム sorting algorithm ソートアルゴリズム ソートアルゴリズム selection sort 選択ソート 選択ソート bubble sort バブルソート バブルソート insertion sort 挿入ソート 挿入ソート quick sort クイックソート クイックソート merge sort マージソート マージソート heap sort ヒープソート ヒープソート bucket sort バケットソート バケットソート counting sort 計数ソート 計数ソート radix sort 基数ソート 基数ソート divide and conquer 分割統治 分割統治 hanota problem ハノイの塔問題 ハノイの塔問題 backtracking algorithm バックトラッキングアルゴリズム バックトラッキングアルゴリズム constraint 制約 制約 solution 解 解 state 状態 状態 pruning 枝刈り 枝刈り permutations problem 全順列問題 全順列問題 subset-sum problem 部分和問題 部分和問題 \\(n\\)-queens problem \\(n\\) クイーン問題 \\(n\\) クイーン問題 dynamic programming 動的計画法 動的計画法 initial state 初期状態 初期状態 state-transition equation 状態遷移方程式 状態遷移方程式 knapsack problem ナップサック問題 ナップサック問題 edit distance problem 編集距離問題 編集距離問題 greedy algorithm 貪欲法 貪欲法","path":["第 16 章   付録","16.3   用語集"],"tags":[]},{"location":"chapter_array_and_linkedlist/","level":1,"title":"第 4 章   配列と連結リスト","text":"

Abstract

データ構造の世界は、まるで重厚なれんがの壁のようです。

配列のれんがは整然と並び、一つひとつがぴったりと接しています。連結リストのれんがはあちこちに分散し、それらをつなぐつるがれんがのすき間を自由に行き交います。

","path":["第 4 章   配列と連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"章の内容","text":"
  • 4.1   配列
  • 4.2   連結リスト
  • 4.3   リスト
  • 4.4   メモリとキャッシュ *
  • 4.5   まとめ
","path":["第 4 章   配列と連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   配列","text":"

配列(array)は線形データ構造の一種であり、同じ型の要素を連続したメモリ領域に格納します。要素が配列内にある位置を、その要素のインデックス(index)と呼びます。下図は、配列の主要な概念と格納方式を示しています。

図 4-1   配列の定義と格納方式

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#411","level":2,"title":"4.1.1   配列の一般的な操作","text":"","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#1","level":3,"title":"1.   配列の初期化","text":"

必要に応じて、配列の初期化方法として初期値なしと初期値ありの 2 種類を使い分けられます。初期値を指定しない場合、多くのプログラミング言語では配列要素は \\(0\\) に初期化されます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
# 配列を初期化する\narr: list[int] = [0] * 5  # [ 0, 0, 0, 0, 0 ]\nnums: list[int] = [1, 3, 2, 5, 4]\n
array.cpp
/* 配列を初期化する */\n// スタック上に格納\nint arr[5];\nint nums[5] = { 1, 3, 2, 5, 4 };\n// ヒープ上に格納(手動で領域を解放する必要がある)\nint* arr1 = new int[5];\nint* nums1 = new int[5] { 1, 3, 2, 5, 4 };\n
array.java
/* 配列を初期化する */\nint[] arr = new int[5]; // { 0, 0, 0, 0, 0 }\nint[] nums = { 1, 3, 2, 5, 4 };\n
array.cs
/* 配列を初期化する */\nint[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]\nint[] nums = [1, 3, 2, 5, 4];\n
array.go
/* 配列を初期化する */\nvar arr [5]int\n// Go では、長さを指定する場合([5]int)は配列であり、長さを指定しない場合([]int)はスライス\n// Go の配列はコンパイル時に長さが確定するよう設計されているため、長さの指定には定数しか使用できない\n// 拡張 extend() メソッドを実装しやすくするため、以下ではスライス(Slice)を配列(Array)として扱う\nnums := []int{1, 3, 2, 5, 4}\n
array.swift
/* 配列を初期化する */\nlet arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]\nlet nums = [1, 3, 2, 5, 4]\n
array.js
/* 配列を初期化する */\nvar arr = new Array(5).fill(0);\nvar nums = [1, 3, 2, 5, 4];\n
array.ts
/* 配列を初期化する */\nlet arr: number[] = new Array(5).fill(0);\nlet nums: number[] = [1, 3, 2, 5, 4];\n
array.dart
/* 配列を初期化する */\nList<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]\nList<int> nums = [1, 3, 2, 5, 4];\n
array.rs
/* 配列を初期化する */\nlet arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]\nlet slice: &[i32] = &[0; 5];\n// Rust では、長さを指定する場合([i32; 5])は配列であり、長さを指定しない場合(&[i32])はスライス\n// Rust の配列はコンパイル時に長さが確定するよう設計されているため、長さの指定には定数しか使用できない\n// Vector は Rust で一般に動的配列として使われる型\n// 拡張 extend() メソッドを実装しやすくするため、以下では vector を配列(array)として扱う\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
array.c
/* 配列を初期化する */\nint arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }\nint nums[5] = { 1, 3, 2, 5, 4 };\n
array.kt
/* 配列を初期化する */\nvar arr = IntArray(5) // { 0, 0, 0, 0, 0 }\nvar nums = intArrayOf(1, 3, 2, 5, 4)\n
array.rb
# 配列を初期化する\narr = Array.new(5, 0)\nnums = [1, 3, 2, 5, 4]\n
実行の可視化

https://pythontutor.com/render.html#code=%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0Aarr%20%3D%20%5B0%5D%20*%205%20%20%23%20%5B%200,%200,%200,%200,%200%20%5D%0Anums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#2","level":3,"title":"2.   要素へのアクセス","text":"

配列要素は連続したメモリ領域に格納されるため、要素のメモリアドレスの計算は非常に容易です。配列のメモリアドレス(先頭要素のメモリアドレス)とある要素のインデックスが与えられれば、下図の式を使ってその要素のメモリアドレスを計算でき、直接その要素にアクセスできます。

図 4-2   配列要素のメモリアドレスの計算

上図を見ると、配列の最初の要素のインデックスは \\(0\\) であり、これは少し直感に反するように思えます。というのも、\\(1\\) から数え始めるほうが自然だからです。しかし、アドレス計算式の観点では、**インデックスの本質はメモリアドレスのオフセット**です。先頭要素のアドレスのオフセットは \\(0\\) であるため、そのインデックスが \\(0\\) なのは妥当です。

配列では要素へのアクセスは非常に効率的であり、\\(O(1)\\) 時間で任意の要素にランダムアクセスできます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def random_access(nums: list[int]) -> int:\n    \"\"\"要素へランダムアクセス\"\"\"\n    # 区間 [0, len(nums)-1] からランダムに数字を 1 つ選ぶ\n    random_index = random.randint(0, len(nums) - 1)\n    # ランダムな要素を取得して返す\n    random_num = nums[random_index]\n    return random_num\n
array.cpp
/* 要素へランダムアクセス */\nint randomAccess(int *nums, int size) {\n    // 区間 [0, size) からランダムに 1 つの数を選ぶ\n    int randomIndex = rand() % size;\n    // ランダムな要素を取得して返す\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.java
/* 要素へランダムアクセス */\nint randomAccess(int[] nums) {\n    // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n    int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);\n    // ランダムな要素を取得して返す\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.cs
/* 要素へランダムアクセス */\nint RandomAccess(int[] nums) {\n    Random random = new();\n    // 区間 [0, nums.Length) からランダムに数字を 1 つ選ぶ\n    int randomIndex = random.Next(nums.Length);\n    // ランダムな要素を取得して返す\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.go
/* 要素へランダムアクセス */\nfunc randomAccess(nums []int) (randomNum int) {\n    // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n    randomIndex := rand.Intn(len(nums))\n    // ランダムな要素を取得して返す\n    randomNum = nums[randomIndex]\n    return\n}\n
array.swift
/* 要素へランダムアクセス */\nfunc randomAccess(nums: [Int]) -> Int {\n    // 区間 [0, nums.count) からランダムに数字を 1 つ選ぶ\n    let randomIndex = nums.indices.randomElement()!\n    // ランダムな要素を取得して返す\n    let randomNum = nums[randomIndex]\n    return randomNum\n}\n
array.js
/* 要素へランダムアクセス */\nfunction randomAccess(nums) {\n    // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n    const random_index = Math.floor(Math.random() * nums.length);\n    // ランダムな要素を取得して返す\n    const random_num = nums[random_index];\n    return random_num;\n}\n
array.ts
/* 要素へランダムアクセス */\nfunction randomAccess(nums: number[]): number {\n    // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n    const random_index = Math.floor(Math.random() * nums.length);\n    // ランダムな要素を取得して返す\n    const random_num = nums[random_index];\n    return random_num;\n}\n
array.dart
/* 要素へランダムアクセス */\nint randomAccess(List<int> nums) {\n  // 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n  int randomIndex = Random().nextInt(nums.length);\n  // ランダムな要素を取得して返す\n  int randomNum = nums[randomIndex];\n  return randomNum;\n}\n
array.rs
/* 要素へランダムアクセス */\nfn random_access(nums: &[i32]) -> i32 {\n    // 区間 [0, nums.len()) からランダムに数字を 1 つ選ぶ\n    let random_index = rand::thread_rng().gen_range(0..nums.len());\n    // ランダムな要素を取得して返す\n    let random_num = nums[random_index];\n    random_num\n}\n
array.c
/* 要素へランダムアクセス */\nint randomAccess(int *nums, int size) {\n    // 区間 [0, size) からランダムに 1 つの数を選ぶ\n    int randomIndex = rand() % size;\n    // ランダムな要素を取得して返す\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.kt
/* 要素へランダムアクセス */\nfun randomAccess(nums: IntArray): Int {\n    // 区間 [0, nums.size) からランダムに数字を 1 つ選ぶ\n    val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size)\n    // ランダムな要素を取得して返す\n    val randomNum = nums[randomIndex]\n    return randomNum\n}\n
array.rb
### 要素にランダムアクセス ###\ndef random_access(nums)\n  # 区間 [0, nums.length) からランダムに 1 つの数を選ぶ\n  random_index = Random.rand(0...nums.length)\n\n  # ランダムな要素を取得して返す\n  nums[random_index]\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#3","level":3,"title":"3.   要素の挿入","text":"

配列要素はメモリ内で「ぴったり隣接して」おり、その間にほかのデータを格納する余地はありません。下図のように、配列の途中に要素を挿入したい場合は、その要素より後ろにあるすべての要素を 1 つずつ後ろへずらし、その後でそのインデックスに要素を代入する必要があります。

図 4-3   配列への要素挿入の例

注意すべき点として、配列の長さは固定であるため、要素を 1 つ挿入すると配列末尾の要素が必ず「失われ」ます。この問題の解決策は「リスト」の章で扱います。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def insert(nums: list[int], num: int, index: int):\n    \"\"\"配列の index 番目に要素 num を挿入\"\"\"\n    # インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in range(len(nums) - 1, index, -1):\n        nums[i] = nums[i - 1]\n    # index の要素に num を代入する\n    nums[index] = num\n
array.cpp
/* 配列の index 番目に要素 num を挿入 */\nvoid insert(int *nums, int size, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.java
/* 配列の index 番目に要素 num を挿入 */\nvoid insert(int[] nums, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.cs
/* 配列の index 番目に要素 num を挿入 */\nvoid Insert(int[] nums, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = nums.Length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.go
/* 配列の index 番目に要素 num を挿入 */\nfunc insert(nums []int, num int, index int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i := len(nums) - 1; i > index; i-- {\n        nums[i] = nums[i-1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
array.swift
/* 配列の index 番目に要素 num を挿入 */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in nums.indices.dropFirst(index).reversed() {\n        nums[i] = nums[i - 1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
array.js
/* 配列の index 番目に要素 num を挿入 */\nfunction insert(nums, num, index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.ts
/* 配列の index 番目に要素 num を挿入 */\nfunction insert(nums: number[], num: number, index: number): void {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.dart
/* 配列の添字 index に要素 _num を挿入 */\nvoid insert(List<int> nums, int _num, int index) {\n  // インデックス index 以降の全要素を 1 つ後ろへ移動する\n  for (var i = nums.length - 1; i > index; i--) {\n    nums[i] = nums[i - 1];\n  }\n  // _num を index の位置の要素に代入\n  nums[index] = _num;\n}\n
array.rs
/* 配列の index 番目に要素 num を挿入 */\nfn insert(nums: &mut [i32], num: i32, index: usize) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for i in (index + 1..nums.len()).rev() {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.c
/* 配列の index 番目に要素 num を挿入 */\nvoid insert(int *nums, int size, int num, int index) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // index の要素に num を代入する\n    nums[index] = num;\n}\n
array.kt
/* 配列の index 番目に要素 num を挿入 */\nfun insert(nums: IntArray, num: Int, index: Int) {\n    // インデックス index 以降の全要素を 1 つ後ろへ移動する\n    for (i in nums.size - 1 downTo index + 1) {\n        nums[i] = nums[i - 1]\n    }\n    // index の要素に num を代入する\n    nums[index] = num\n}\n
array.rb
### 配列のインデックス index に要素 num を挿入 ###\ndef insert(nums, num, index)\n  # インデックス index 以降の全要素を 1 つ後ろへ移動する\n  for i in (nums.length - 1).downto(index + 1)\n    nums[i] = nums[i - 1]\n  end\n\n  # index の要素に num を代入する\n  nums[index] = num\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4","level":3,"title":"4.   要素の削除","text":"

同様に、下図のように、インデックス \\(i\\) の要素を削除したい場合は、インデックス \\(i\\) より後ろの要素をすべて 1 つずつ前へずらす必要があります。

図 4-4   配列からの要素削除の例

注意してください。要素の削除が完了すると、もともとの末尾要素は「意味を持たない」状態になるため、わざわざ変更する必要はありません。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def remove(nums: list[int], index: int):\n    \"\"\"index の要素を削除する\"\"\"\n    # インデックス index より後ろの全要素を 1 つ前へ移動する\n    for i in range(index, len(nums) - 1):\n        nums[i] = nums[i + 1]\n
array.cpp
/* index の要素を削除する */\nvoid remove(int *nums, int size, int index) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.java
/* index の要素を削除する */\nvoid remove(int[] nums, int index) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (int i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.cs
/* index の要素を削除する */\nvoid Remove(int[] nums, int index) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (int i = index; i < nums.Length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.go
/* index の要素を削除する */\nfunc remove(nums []int, index int) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for i := index; i < len(nums)-1; i++ {\n        nums[i] = nums[i+1]\n    }\n}\n
array.swift
/* index の要素を削除する */\nfunc remove(nums: inout [Int], index: Int) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for i in nums.indices.dropFirst(index).dropLast() {\n        nums[i] = nums[i + 1]\n    }\n}\n
array.js
/* index の要素を削除する */\nfunction remove(nums, index) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.ts
/* index の要素を削除する */\nfunction remove(nums: number[], index: number): void {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.dart
/* index の要素を削除する */\nvoid remove(List<int> nums, int index) {\n  // インデックス index より後ろの全要素を 1 つ前へ移動する\n  for (var i = index; i < nums.length - 1; i++) {\n    nums[i] = nums[i + 1];\n  }\n}\n
array.rs
/* index の要素を削除する */\nfn remove(nums: &mut [i32], index: usize) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for i in index..nums.len() - 1 {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.c
/* index の要素を削除する */\n// 注意: stdio.h が remove 識別子を使用している\nvoid removeItem(int *nums, int size, int index) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.kt
/* index の要素を削除する */\nfun remove(nums: IntArray, index: Int) {\n    // インデックス index より後ろの全要素を 1 つ前へ移動する\n    for (i in index..<nums.size - 1) {\n        nums[i] = nums[i + 1]\n    }\n}\n
array.rb
### インデックス index の要素を削除 ###\ndef remove(nums, index)\n  # インデックス index より後ろの全要素を 1 つ前へ移動する\n  for i in index...(nums.length - 1)\n    nums[i] = nums[i + 1]\n  end\nend\n
コードの可視化

全画面で見る >

全体として見ると、配列の挿入と削除には次の欠点があります。

  • 時間計算量が高い:配列の挿入と削除の平均時間計算量はいずれも \\(O(n)\\) であり、ここで \\(n\\) は配列長です。
  • 要素が失われる:配列の長さは不変であるため、要素を挿入すると配列長の範囲を超えた要素は失われます。
  • メモリの浪費:やや長めの配列を初期化して先頭部分だけを使うこともでき、この場合データ挿入時に失われる末尾要素はすべて「無意味」ですが、その代わり一部のメモリ領域が無駄になります。
","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#5","level":3,"title":"5.   配列の走査","text":"

ほとんどのプログラミング言語では、インデックスを使って配列を走査することも、各要素を直接取り出しながら走査することもできます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def traverse(nums: list[int]):\n    \"\"\"配列を走査\"\"\"\n    count = 0\n    # インデックスで配列を走査\n    for i in range(len(nums)):\n        count += nums[i]\n    # 配列要素を直接走査\n    for num in nums:\n        count += num\n    # データのインデックスと要素を同時に走査する\n    for i, num in enumerate(nums):\n        count += nums[i]\n        count += num\n
array.cpp
/* 配列を走査 */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // インデックスで配列を走査\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
array.java
/* 配列を走査 */\nvoid traverse(int[] nums) {\n    int count = 0;\n    // インデックスで配列を走査\n    for (int i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // 配列要素を直接走査\n    for (int num : nums) {\n        count += num;\n    }\n}\n
array.cs
/* 配列を走査 */\nvoid Traverse(int[] nums) {\n    int count = 0;\n    // インデックスで配列を走査\n    for (int i = 0; i < nums.Length; i++) {\n        count += nums[i];\n    }\n    // 配列要素を直接走査\n    foreach (int num in nums) {\n        count += num;\n    }\n}\n
array.go
/* 配列を走査 */\nfunc traverse(nums []int) {\n    count := 0\n    // インデックスで配列を走査\n    for i := 0; i < len(nums); i++ {\n        count += nums[i]\n    }\n    count = 0\n    // 配列要素を直接走査\n    for _, num := range nums {\n        count += num\n    }\n    // データのインデックスと要素を同時に走査する\n    for i, num := range nums {\n        count += nums[i]\n        count += num\n    }\n}\n
array.swift
/* 配列を走査 */\nfunc traverse(nums: [Int]) {\n    var count = 0\n    // インデックスで配列を走査\n    for i in nums.indices {\n        count += nums[i]\n    }\n    // 配列要素を直接走査\n    for num in nums {\n        count += num\n    }\n    // データのインデックスと要素を同時に走査する\n    for (i, num) in nums.enumerated() {\n        count += nums[i]\n        count += num\n    }\n}\n
array.js
/* 配列を走査 */\nfunction traverse(nums) {\n    let count = 0;\n    // インデックスで配列を走査\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // 配列要素を直接走査\n    for (const num of nums) {\n        count += num;\n    }\n}\n
array.ts
/* 配列を走査 */\nfunction traverse(nums: number[]): void {\n    let count = 0;\n    // インデックスで配列を走査\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // 配列要素を直接走査\n    for (const num of nums) {\n        count += num;\n    }\n}\n
array.dart
/* 配列要素を走査する */\nvoid traverse(List<int> nums) {\n  int count = 0;\n  // インデックスで配列を走査\n  for (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n  }\n  // 配列要素を直接走査\n  for (int _num in nums) {\n    count += _num;\n  }\n  // forEach メソッドで配列を走査する\n  nums.forEach((_num) {\n    count += _num;\n  });\n}\n
array.rs
/* 配列を走査 */\nfn traverse(nums: &[i32]) {\n    let mut _count = 0;\n    // インデックスで配列を走査\n    for i in 0..nums.len() {\n        _count += nums[i];\n    }\n    // 配列要素を直接走査\n    _count = 0;\n    for &num in nums {\n        _count += num;\n    }\n}\n
array.c
/* 配列を走査 */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // インデックスで配列を走査\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
array.kt
/* 配列を走査 */\nfun traverse(nums: IntArray) {\n    var count = 0\n    // インデックスで配列を走査\n    for (i in nums.indices) {\n        count += nums[i]\n    }\n    // 配列要素を直接走査\n    for (j in nums) {\n        count += j\n    }\n}\n
array.rb
### 配列を走査 ###\ndef traverse(nums)\n  count = 0\n\n  # インデックスで配列を走査\n  for i in 0...nums.length\n    count += nums[i]\n  end\n\n  # 配列要素を直接走査\n  for num in nums\n    count += num\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#6","level":3,"title":"6.   要素の検索","text":"

配列内で指定した要素を探すには、配列を走査し、各反復で要素値が一致するかを判定し、一致したら対応するインデックスを出力します。

配列は線形データ構造であるため、上記の検索操作は「線形探索」と呼ばれます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def find(nums: list[int], target: int) -> int:\n    \"\"\"配列内で指定要素を探す\"\"\"\n    for i in range(len(nums)):\n        if nums[i] == target:\n            return i\n    return -1\n
array.cpp
/* 配列内で指定要素を探す */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.java
/* 配列内で指定要素を探す */\nint find(int[] nums, int target) {\n    for (int i = 0; i < nums.length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.cs
/* 配列内で指定要素を探す */\nint Find(int[] nums, int target) {\n    for (int i = 0; i < nums.Length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.go
/* 配列内で指定要素を探す */\nfunc find(nums []int, target int) (index int) {\n    index = -1\n    for i := 0; i < len(nums); i++ {\n        if nums[i] == target {\n            index = i\n            break\n        }\n    }\n    return\n}\n
array.swift
/* 配列内で指定要素を探す */\nfunc find(nums: [Int], target: Int) -> Int {\n    for i in nums.indices {\n        if nums[i] == target {\n            return i\n        }\n    }\n    return -1\n}\n
array.js
/* 配列内で指定要素を探す */\nfunction find(nums, target) {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) return i;\n    }\n    return -1;\n}\n
array.ts
/* 配列内で指定要素を探す */\nfunction find(nums: number[], target: number): number {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) {\n            return i;\n        }\n    }\n    return -1;\n}\n
array.dart
/* 配列内で指定要素を探す */\nint find(List<int> nums, int target) {\n  for (var i = 0; i < nums.length; i++) {\n    if (nums[i] == target) return i;\n  }\n  return -1;\n}\n
array.rs
/* 配列内で指定要素を探す */\nfn find(nums: &[i32], target: i32) -> Option<usize> {\n    for i in 0..nums.len() {\n        if nums[i] == target {\n            return Some(i);\n        }\n    }\n    None\n}\n
array.c
/* 配列内で指定要素を探す */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.kt
/* 配列内で指定要素を探す */\nfun find(nums: IntArray, target: Int): Int {\n    for (i in nums.indices) {\n        if (nums[i] == target)\n            return i\n    }\n    return -1\n}\n
array.rb
### 配列内の指定要素を検索 ###\ndef find(nums, target)\n  for i in 0...nums.length\n    return i if nums[i] == target\n  end\n\n  -1\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#7","level":3,"title":"7.   配列の拡張","text":"

複雑なシステム環境では、配列の後方にあるメモリ領域が利用可能であることをプログラム側で保証できず、そのため安全に配列容量を拡張できません。したがって、ほとんどのプログラミング言語では、配列の長さは不変です。

配列を拡張したい場合は、より大きな新しい配列を作り、元の配列の要素を順に新配列へコピーする必要があります。これは \\(O(n)\\) の操作であり、配列が大きい場合は非常に時間がかかります。コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def extend(nums: list[int], enlarge: int) -> list[int]:\n    \"\"\"配列長を拡張する\"\"\"\n    # 拡張後の長さを持つ配列を初期化する\n    res = [0] * (len(nums) + enlarge)\n    # 元の配列の全要素を新しい配列にコピー\n    for i in range(len(nums)):\n        res[i] = nums[i]\n    # 拡張後の新しい配列を返す\n    return res\n
array.cpp
/* 配列長を拡張する */\nint *extend(int *nums, int size, int enlarge) {\n    // 拡張後の長さを持つ配列を初期化する\n    int *res = new int[size + enlarge];\n    // 元の配列の全要素を新しい配列にコピー\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // メモリを解放する\n    delete[] nums;\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.java
/* 配列長を拡張する */\nint[] extend(int[] nums, int enlarge) {\n    // 拡張後の長さを持つ配列を初期化する\n    int[] res = new int[nums.length + enlarge];\n    // 元の配列の全要素を新しい配列にコピー\n    for (int i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.cs
/* 配列長を拡張する */\nint[] Extend(int[] nums, int enlarge) {\n    // 拡張後の長さを持つ配列を初期化する\n    int[] res = new int[nums.Length + enlarge];\n    // 元の配列の全要素を新しい配列にコピー\n    for (int i = 0; i < nums.Length; i++) {\n        res[i] = nums[i];\n    }\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.go
/* 配列長を拡張する */\nfunc extend(nums []int, enlarge int) []int {\n    // 拡張後の長さを持つ配列を初期化する\n    res := make([]int, len(nums)+enlarge)\n    // 元の配列の全要素を新しい配列にコピー\n    for i, num := range nums {\n        res[i] = num\n    }\n    // 拡張後の新しい配列を返す\n    return res\n}\n
array.swift
/* 配列長を拡張する */\nfunc extend(nums: [Int], enlarge: Int) -> [Int] {\n    // 拡張後の長さを持つ配列を初期化する\n    var res = Array(repeating: 0, count: nums.count + enlarge)\n    // 元の配列の全要素を新しい配列にコピー\n    for i in nums.indices {\n        res[i] = nums[i]\n    }\n    // 拡張後の新しい配列を返す\n    return res\n}\n
array.js
/* 配列長を拡張する */\n// JavaScript の Array は動的配列であり、直接拡張できます\n// 学習しやすいよう、本関数では Array を長さ不変の配列として扱います\nfunction extend(nums, enlarge) {\n    // 拡張後の長さを持つ配列を初期化する\n    const res = new Array(nums.length + enlarge).fill(0);\n    // 元の配列の全要素を新しい配列にコピー\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.ts
/* 配列長を拡張する */\n// TypeScript の Array は動的配列であり、直接拡張できます\n// 学習しやすいよう、本関数では Array を長さ不変の配列として扱います\nfunction extend(nums: number[], enlarge: number): number[] {\n    // 拡張後の長さを持つ配列を初期化する\n    const res = new Array(nums.length + enlarge).fill(0);\n    // 元の配列の全要素を新しい配列にコピー\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.dart
/* 配列長を拡張する */\nList<int> extend(List<int> nums, int enlarge) {\n  // 拡張後の長さを持つ配列を初期化する\n  List<int> res = List.filled(nums.length + enlarge, 0);\n  // 元の配列の全要素を新しい配列にコピー\n  for (var i = 0; i < nums.length; i++) {\n    res[i] = nums[i];\n  }\n  // 拡張後の新しい配列を返す\n  return res;\n}\n
array.rs
/* 配列長を拡張する */\nfn extend(nums: &[i32], enlarge: usize) -> Vec<i32> {\n    // 拡張後の長さを持つ配列を初期化する\n    let mut res: Vec<i32> = vec![0; nums.len() + enlarge];\n    // 元の配列の全要素を新しい配列にコピー\n    res[0..nums.len()].copy_from_slice(nums);\n\n    // 拡張後の新しい配列を返す\n    res\n}\n
array.c
/* 配列長を拡張する */\nint *extend(int *nums, int size, int enlarge) {\n    // 拡張後の長さを持つ配列を初期化する\n    int *res = (int *)malloc(sizeof(int) * (size + enlarge));\n    // 元の配列の全要素を新しい配列にコピー\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // 拡張後の領域を初期化する\n    for (int i = size; i < size + enlarge; i++) {\n        res[i] = 0;\n    }\n    // 拡張後の新しい配列を返す\n    return res;\n}\n
array.kt
/* 配列長を拡張する */\nfun extend(nums: IntArray, enlarge: Int): IntArray {\n    // 拡張後の長さを持つ配列を初期化する\n    val res = IntArray(nums.size + enlarge)\n    // 元の配列の全要素を新しい配列にコピー\n    for (i in nums.indices) {\n        res[i] = nums[i]\n    }\n    // 拡張後の新しい配列を返す\n    return res\n}\n
array.rb
### 配列長を拡張 ###\n# Ruby の Array は動的配列であり、直接拡張できます\n# 学習しやすいよう、本関数では Array を長さ不変の配列として扱います\ndef extend(nums, enlarge)\n  # 拡張後の長さを持つ配列を初期化する\n  res = Array.new(nums.length + enlarge, 0)\n\n  # 元の配列の全要素を新しい配列にコピー\n  for i in 0...nums.length\n    res[i] = nums[i]\n  end\n\n  # 拡張後の新しい配列を返す\n  res\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#412","level":2,"title":"4.1.2   配列の利点と限界","text":"

配列は連続したメモリ領域に格納され、要素の型も同一です。この方法には豊富な事前情報が含まれており、システムはそれらを利用してデータ構造の操作効率を最適化できます。

  • 空間効率が高い:配列はデータに連続したメモリブロックを割り当てるため、追加の構造オーバーヘッドが不要です。
  • ランダムアクセスをサポートする:配列では任意の要素に \\(O(1)\\) 時間でアクセスできます。
  • キャッシュ局所性:配列要素にアクセスする際、コンピュータはその要素だけでなく周囲のデータもキャッシュするため、高速キャッシュを利用して後続操作の実行速度を高められます。

連続領域への格納は諸刃の剣であり、次のような制約があります。

  • 挿入と削除の効率が低い:配列内の要素が多い場合、挿入や削除では大量の要素を移動する必要があります。
  • 長さが不変:配列は初期化後に長さが固定され、拡張するにはすべてのデータを新しい配列へコピーする必要があり、コストが大きくなります。
  • 空間の浪費:配列に割り当てたサイズが実際の必要量を上回る場合、余分な領域は無駄になります。
","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#413","level":2,"title":"4.1.3   配列の典型的な応用","text":"

配列は基礎的で一般的なデータ構造であり、さまざまなアルゴリズムで頻繁に使われるだけでなく、多様な複雑データ構造の実装にも利用できます。

  • ランダムアクセス:いくつかのサンプルをランダムに抽出したい場合、配列に格納してランダムな系列を生成し、インデックスに基づいてランダムサンプリングを行えます。
  • ソートと探索:配列はソートアルゴリズムと探索アルゴリズムで最もよく使われるデータ構造です。クイックソート、マージソート、二分探索などは主に配列上で行われます。
  • ルックアップテーブル:ある要素やその対応関係を高速に調べる必要がある場合、配列をルックアップテーブルとして使えます。たとえば文字から ASCII コードへの対応を実装したいなら、文字の ASCII コード値をインデックスとし、対応する要素を配列の対応位置に格納できます。
  • 機械学習:ニューラルネットワークでは、ベクトル、行列、テンソル間の線形代数演算が大量に使われ、これらのデータはいずれも配列の形で構築されます。配列はニューラルネットワークプログラミングで最もよく使われるデータ構造です。
  • データ構造の実装:配列はスタック、キュー、ハッシュテーブル、ヒープ、グラフなどのデータ構造の実装に利用できます。たとえば、グラフの隣接行列表現は実際には 2 次元配列です。
","path":["第 4 章   配列と連結リスト","4.1   配列"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   連結リスト","text":"

メモリ空間はすべてのプログラムに共通の資源であり、複雑なシステム実行環境では、空きメモリがメモリの各所に散在している可能性があります。配列を格納するメモリ空間は連続していなければなりませんが、配列が非常に大きい場合、メモリはそのような大きな連続領域を提供できないことがあります。このとき、連結リストの柔軟性という利点が現れます。

連結リスト(linked list)は線形データ構造の一種であり、各要素はノードオブジェクトです。各ノードは「参照」によって接続されます。参照には次のノードのメモリアドレスが記録されており、これによって現在のノードから次のノードへアクセスできます。

連結リストの設計では、各ノードをメモリの各所に分散して格納でき、それらのメモリアドレスは連続している必要がありません。

図 4-5   連結リストの定義と格納方式

上図を見ると、連結リストの構成単位はノード(node)オブジェクトです。各ノードは 2 つのデータ、すなわちノードの「値」と次のノードを指す「参照」を含みます。

  • 連結リストの最初のノードを「先頭ノード」、最後のノードを「末尾ノード」と呼びます。
  • 末尾ノードが指す先は「空」であり、Java、C++、Python ではそれぞれ nullnullptrNone と表記します。
  • C、C++、Go、Rust などポインタをサポートする言語では、上記の「参照」は「ポインタ」に置き換えるべきです。

以下のコードが示すように、連結リストノード ListNode は値のほかに、追加で 1 つの参照(ポインタ)を保持する必要があります。そのため、同じデータ量であれば、連結リストは配列より多くのメモリ空間を消費します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class ListNode:\n    \"\"\"連結リストノードクラス\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val               # ノードの値\n        self.next: ListNode | None = None # 次のノードへの参照\n
/* 連結リストノード構造体 */\nstruct ListNode {\n    int val;         // ノードの値\n    ListNode *next;  // 次のノードへのポインタ\n    ListNode(int x) : val(x), next(nullptr) {}  // コンストラクタ\n};\n
/* 連結リストノードクラス */\nclass ListNode {\n    int val;        // ノードの値\n    ListNode next;  // 次のノードへの参照\n    ListNode(int x) { val = x; }  // コンストラクタ\n}\n
/* 連結リストノードクラス */\nclass ListNode(int x) {  //コンストラクタ\n    int val = x;         // ノードの値\n    ListNode? next;      // 次のノードへの参照\n}\n
/* 連結リストノード構造体 */\ntype ListNode struct {\n    Val  int       // ノードの値\n    Next *ListNode // 次のノードへのポインタ\n}\n\n// NewListNode コンストラクタ。新しい連結リストを作成する\nfunc NewListNode(val int) *ListNode {\n    return &ListNode{\n        Val:  val,\n        Next: nil,\n    }\n}\n
/* 連結リストノードクラス */\nclass ListNode {\n    var val: Int // ノードの値\n    var next: ListNode? // 次のノードへの参照\n\n    init(x: Int) { // コンストラクタ\n        val = x\n    }\n}\n
/* 連結リストノードクラス */\nclass ListNode {\n    constructor(val, next) {\n        this.val = (val === undefined ? 0 : val);       // ノードの値\n        this.next = (next === undefined ? null : next); // 次のノードへの参照\n    }\n}\n
/* 連結リストノードクラス */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    constructor(val?: number, next?: ListNode | null) {\n        this.val = val === undefined ? 0 : val;        // ノードの値\n        this.next = next === undefined ? null : next;  // 次のノードへの参照\n    }\n}\n
/* 連結リストノードクラス */\nclass ListNode {\n  int val; // ノードの値\n  ListNode? next; // 次のノードへの参照\n  ListNode(this.val, [this.next]); // コンストラクタ\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n/* 連結リストノードクラス */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // ノードの値\n    next: Option<Rc<RefCell<ListNode>>>, // 次のノードへのポインタ\n}\n
/* 連結リストノード構造体 */\ntypedef struct ListNode {\n    int val;               // ノードの値\n    struct ListNode *next; // 次のノードへのポインタ\n} ListNode;\n\n/* コンストラクタ */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    return node;\n}\n
/* 連結リストノードクラス */\n// コンストラクタ\nclass ListNode(x: Int) {\n    val _val: Int = x          // ノードの値\n    val next: ListNode? = null // 次のノードへの参照\n}\n
# 連結リストノードクラス\nclass ListNode\n  attr_accessor :val  # ノードの値\n  attr_accessor :next # 次のノードへの参照\n\n  def initialize(val=0, next_node=nil)\n    @val = val\n    @next = next_node\n  end\nend\n
","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#421","level":2,"title":"4.2.1   連結リストの基本操作","text":"","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#1","level":3,"title":"1.   連結リストの初期化","text":"

連結リストの構築は 2 つの手順に分かれます。第 1 に各ノードオブジェクトを初期化し、第 2 にノード間の参照関係を構築します。初期化が完了したら、連結リストの先頭ノードから出発し、参照で next をたどってすべてのノードに順にアクセスできます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
# 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化\n# 各ノードを初期化\nn0 = ListNode(1)\nn1 = ListNode(3)\nn2 = ListNode(2)\nn3 = ListNode(5)\nn4 = ListNode(4)\n# ノード間の参照を構築\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
linked_list.cpp
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nListNode* n0 = new ListNode(1);\nListNode* n1 = new ListNode(3);\nListNode* n2 = new ListNode(2);\nListNode* n3 = new ListNode(5);\nListNode* n4 = new ListNode(4);\n// ノード間の参照を構築\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
linked_list.java
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nListNode n0 = new ListNode(1);\nListNode n1 = new ListNode(3);\nListNode n2 = new ListNode(2);\nListNode n3 = new ListNode(5);\nListNode n4 = new ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.cs
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nListNode n0 = new(1);\nListNode n1 = new(3);\nListNode n2 = new(2);\nListNode n3 = new(5);\nListNode n4 = new(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.go
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nn0 := NewListNode(1)\nn1 := NewListNode(3)\nn2 := NewListNode(2)\nn3 := NewListNode(5)\nn4 := NewListNode(4)\n// ノード間の参照を構築\nn0.Next = n1\nn1.Next = n2\nn2.Next = n3\nn3.Next = n4\n
linked_list.swift
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nlet n0 = ListNode(x: 1)\nlet n1 = ListNode(x: 3)\nlet n2 = ListNode(x: 2)\nlet n3 = ListNode(x: 5)\nlet n4 = ListNode(x: 4)\n// ノード間の参照を構築\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
linked_list.js
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.ts
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.dart
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\\\n// 各ノードを初期化\nListNode n0 = ListNode(1);\nListNode n1 = ListNode(3);\nListNode n2 = ListNode(2);\nListNode n3 = ListNode(5);\nListNode n4 = ListNode(4);\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.rs
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nlet n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));\nlet n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));\nlet n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));\nlet n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));\nlet n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));\n\n// ノード間の参照を構築\nn0.borrow_mut().next = Some(n1.clone());\nn1.borrow_mut().next = Some(n2.clone());\nn2.borrow_mut().next = Some(n3.clone());\nn3.borrow_mut().next = Some(n4.clone());\n
linked_list.c
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nListNode* n0 = newListNode(1);\nListNode* n1 = newListNode(3);\nListNode* n2 = newListNode(2);\nListNode* n3 = newListNode(5);\nListNode* n4 = newListNode(4);\n// ノード間の参照を構築\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
linked_list.kt
/* 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化 */\n// 各ノードを初期化\nval n0 = ListNode(1)\nval n1 = ListNode(3)\nval n2 = ListNode(2)\nval n3 = ListNode(5)\nval n4 = ListNode(4)\n// ノード間の参照を構築\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.rb
# 連結リスト 1 -> 3 -> 2 -> 5 -> 4 を初期化\n# 各ノードを初期化\nn0 = ListNode.new(1)\nn1 = ListNode.new(3)\nn2 = ListNode.new(2)\nn3 = ListNode.new(5)\nn4 = ListNode.new(4)\n# ノード間の参照を構築\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
可視化実行

https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

配列全体は 1 つの変数であり、たとえば配列 nums には nums[0]nums[1] などの要素が含まれます。一方、連結リストは複数の独立したノードオブジェクトで構成されます。通常、先頭ノードを連結リストの代名詞として扱います。たとえば上記のコードの連結リストは n0 と表せます。

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2","level":3,"title":"2.   ノードの挿入","text":"

連結リストへのノード挿入は非常に簡単です。下図に示すように、隣り合う 2 つのノード n0n1 の間に新しいノード P を挿入したいとします。このとき 2 つのノードの参照(ポインタ)を変更するだけでよく、時間計算量は \\(O(1)\\) です。

これに対して、配列に要素を挿入する時間計算量は \\(O(n)\\) であり、データ量が大きい場合の効率は低くなります。

図 4-6   連結リストへのノード挿入例

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def insert(n0: ListNode, P: ListNode):\n    \"\"\"連結リストでノード n0 の後ろにノード P を挿入する\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
linked_list.cpp
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
linked_list.java
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.cs
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.go
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
linked_list.swift
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
linked_list.js
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.ts
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nfunction insert(n0: ListNode, P: ListNode): void {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.dart
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
linked_list.rs
/* 連結リストでノード n0 の後ろにノード P を挿入する */\n#[allow(non_snake_case)]\npub fn insert<T>(n0: &Rc<RefCell<ListNode<T>>>, P: Rc<RefCell<ListNode<T>>>) {\n    let n1 = n0.borrow_mut().next.take();\n    P.borrow_mut().next = n1;\n    n0.borrow_mut().next = Some(P);\n}\n
linked_list.c
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
linked_list.kt
/* 連結リストでノード n0 の後ろにノード P を挿入する */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
linked_list.rb
### 連結リストのノード n0 の後にノード _p を挿入 ###\n# Ruby の `p` は組み込み関数で、`P` は定数なので、代わりに `_p` を使える\ndef insert(n0, _p)\n  n1 = n0.next\n  _p.next = n1\n  n0.next = _p\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#3","level":3,"title":"3.   ノードの削除","text":"

下図に示すように、連結リストでのノード削除も非常に簡単で、1 つのノードの参照(ポインタ)を変更するだけで済みます。

なお、削除操作が完了した後もノード P は依然として n1 を指していますが、実際にはこの連結リストをたどっても P へはアクセスできません。つまり、P はすでにこの連結リストには属していません。

図 4-7   連結リストのノード削除

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def remove(n0: ListNode):\n    \"\"\"連結リストでノード n0 の直後のノードを削除する\"\"\"\n    if not n0.next:\n        return\n    # n0 -> P -> n1\n    P = n0.next\n    n1 = P.next\n    n0.next = n1\n
linked_list.cpp
/* 連結リストでノード n0 の直後のノードを削除する */\nvoid remove(ListNode *n0) {\n    if (n0->next == nullptr)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // メモリを解放する\n    delete P;\n}\n
linked_list.java
/* 連結リストでノード n0 の直後のノードを削除する */\nvoid remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.cs
/* 連結リストでノード n0 の直後のノードを削除する */\nvoid Remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode? n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.go
/* 連結リストでノード n0 の直後のノードを削除する */\nfunc removeItem(n0 *ListNode) {\n    if n0.Next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    P := n0.Next\n    n1 := P.Next\n    n0.Next = n1\n}\n
linked_list.swift
/* 連結リストでノード n0 の直後のノードを削除する */\nfunc remove(n0: ListNode) {\n    if n0.next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    let P = n0.next\n    let n1 = P?.next\n    n0.next = n1\n}\n
linked_list.js
/* 連結リストでノード n0 の直後のノードを削除する */\nfunction remove(n0) {\n    if (!n0.next) return;\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.ts
/* 連結リストでノード n0 の直後のノードを削除する */\nfunction remove(n0: ListNode): void {\n    if (!n0.next) {\n        return;\n    }\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.dart
/* 連結リストでノード n0 の直後のノードを削除する */\nvoid remove(ListNode n0) {\n  if (n0.next == null) return;\n  // n0 -> P -> n1\n  ListNode P = n0.next!;\n  ListNode? n1 = P.next;\n  n0.next = n1;\n}\n
linked_list.rs
/* 連結リストでノード n0 の直後のノードを削除する */\n#[allow(non_snake_case)]\npub fn remove<T>(n0: &Rc<RefCell<ListNode<T>>>) {\n    // n0 -> P -> n1\n    let P = n0.borrow_mut().next.take();\n    if let Some(node) = P {\n        let n1 = node.borrow_mut().next.take();\n        n0.borrow_mut().next = n1;\n    }\n}\n
linked_list.c
/* 連結リストでノード n0 の直後のノードを削除する */\n// 注意: stdio.h が remove 識別子を使用している\nvoid removeItem(ListNode *n0) {\n    if (!n0->next)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // メモリを解放する\n    free(P);\n}\n
linked_list.kt
/* 連結リストでノード n0 の直後のノードを削除する */\nfun remove(n0: ListNode?) {\n    if (n0?.next == null)\n        return\n    // n0 -> P -> n1\n    val p = n0.next\n    val n1 = p?.next\n    n0.next = n1\n}\n
linked_list.rb
### 連結リストのノード n0 の直後のノードを削除 ###\ndef remove(n0)\n  return if n0.next.nil?\n\n  # n0 -> remove_node -> n1\n  remove_node = n0.next\n  n1 = remove_node.next\n  n0.next = n1\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#4","level":3,"title":"4.   ノードへのアクセス","text":"

**連結リストでノードにアクセスする効率は低い**です。前節で述べたように、配列では任意の要素へ \\(O(1)\\) 時間でアクセスできます。これに対して連結リストでは、プログラムは先頭ノードから出発し、1 つずつ後ろへたどって目的のノードを見つける必要があります。つまり、連結リストの第 \\(i\\) ノードにアクセスするには \\(i - 1\\) 回のループが必要であり、時間計算量は \\(O(n)\\) です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def access(head: ListNode, index: int) -> ListNode | None:\n    \"\"\"連結リスト内で index 番目のノードにアクセス\"\"\"\n    for _ in range(index):\n        if not head:\n            return None\n        head = head.next\n    return head\n
linked_list.cpp
/* 連結リスト内で index 番目のノードにアクセス */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == nullptr)\n            return nullptr;\n        head = head->next;\n    }\n    return head;\n}\n
linked_list.java
/* 連結リスト内で index 番目のノードにアクセス */\nListNode access(ListNode head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.cs
/* 連結リスト内で index 番目のノードにアクセス */\nListNode? Access(ListNode? head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.go
/* 連結リスト内で index 番目のノードにアクセス */\nfunc access(head *ListNode, index int) *ListNode {\n    for i := 0; i < index; i++ {\n        if head == nil {\n            return nil\n        }\n        head = head.Next\n    }\n    return head\n}\n
linked_list.swift
/* 連結リスト内で index 番目のノードにアクセス */\nfunc access(head: ListNode, index: Int) -> ListNode? {\n    var head: ListNode? = head\n    for _ in 0 ..< index {\n        if head == nil {\n            return nil\n        }\n        head = head?.next\n    }\n    return head\n}\n
linked_list.js
/* 連結リスト内で index 番目のノードにアクセス */\nfunction access(head, index) {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.ts
/* 連結リスト内で index 番目のノードにアクセス */\nfunction access(head: ListNode | null, index: number): ListNode | null {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.dart
/* 連結リスト内で index 番目のノードにアクセス */\nListNode? access(ListNode? head, int index) {\n  for (var i = 0; i < index; i++) {\n    if (head == null) return null;\n    head = head.next;\n  }\n  return head;\n}\n
linked_list.rs
/* 連結リスト内で index 番目のノードにアクセス */\npub fn access<T>(head: Rc<RefCell<ListNode<T>>>, index: i32) -> Option<Rc<RefCell<ListNode<T>>>> {\n    fn dfs<T>(\n        head: Option<&Rc<RefCell<ListNode<T>>>>,\n        index: i32,\n    ) -> Option<Rc<RefCell<ListNode<T>>>> {\n        if index <= 0 {\n            return head.cloned();\n        }\n\n        if let Some(node) = head {\n            dfs(node.borrow().next.as_ref(), index - 1)\n        } else {\n            None\n        }\n    }\n\n    dfs(Some(head).as_ref(), index)\n}\n
linked_list.c
/* 連結リスト内で index 番目のノードにアクセス */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == NULL)\n            return NULL;\n        head = head->next;\n    }\n    return head;\n}\n
linked_list.kt
/* 連結リスト内で index 番目のノードにアクセス */\nfun access(head: ListNode?, index: Int): ListNode? {\n    var h = head\n    for (i in 0..<index) {\n        if (h == null)\n            return null\n        h = h.next\n    }\n    return h\n}\n
linked_list.rb
### 連結リスト内の index 番目のノードにアクセス ###\ndef access(head, index)\n  for i in 0...index\n    return nil if head.nil?\n    head = head.next\n  end\n\n  head\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#5","level":3,"title":"5.   ノードの探索","text":"

連結リストを走査し、その中から値が target のノードを探し、そのノードの連結リスト内でのインデックスを出力します。この処理も線形探索に属します。コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def find(head: ListNode, target: int) -> int:\n    \"\"\"連結リストで値が target の最初のノードを探す\"\"\"\n    index = 0\n    while head:\n        if head.val == target:\n            return index\n        head = head.next\n        index += 1\n    return -1\n
linked_list.cpp
/* 連結リストで値が target の最初のノードを探す */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head != nullptr) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.java
/* 連結リストで値が target の最初のノードを探す */\nint find(ListNode head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.cs
/* 連結リストで値が target の最初のノードを探す */\nint Find(ListNode? head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.go
/* 連結リストで値が target の最初のノードを探す */\nfunc findNode(head *ListNode, target int) int {\n    index := 0\n    for head != nil {\n        if head.Val == target {\n            return index\n        }\n        head = head.Next\n        index++\n    }\n    return -1\n}\n
linked_list.swift
/* 連結リストで値が target の最初のノードを探す */\nfunc find(head: ListNode, target: Int) -> Int {\n    var head: ListNode? = head\n    var index = 0\n    while head != nil {\n        if head?.val == target {\n            return index\n        }\n        head = head?.next\n        index += 1\n    }\n    return -1\n}\n
linked_list.js
/* 連結リストで値が target の最初のノードを探す */\nfunction find(head, target) {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
linked_list.ts
/* 連結リストで値が target の最初のノードを探す */\nfunction find(head: ListNode | null, target: number): number {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
linked_list.dart
/* 連結リストで値が target の最初のノードを探す */\nint find(ListNode? head, int target) {\n  int index = 0;\n  while (head != null) {\n    if (head.val == target) {\n      return index;\n    }\n    head = head.next;\n    index++;\n  }\n  return -1;\n}\n
linked_list.rs
/* 連結リストで値が target の最初のノードを探す */\npub fn find<T: PartialEq>(head: Rc<RefCell<ListNode<T>>>, target: T) -> i32 {\n    fn find<T: PartialEq>(head: Option<&Rc<RefCell<ListNode<T>>>>, target: T, idx: i32) -> i32 {\n        if let Some(node) = head {\n            if node.borrow().val == target {\n                return idx;\n            }\n            return find(node.borrow().next.as_ref(), target, idx + 1);\n        } else {\n            -1\n        }\n    }\n\n    find(Some(head).as_ref(), target, 0)\n}\n
linked_list.c
/* 連結リストで値が target の最初のノードを探す */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.kt
/* 連結リストで値が target の最初のノードを探す */\nfun find(head: ListNode?, target: Int): Int {\n    var index = 0\n    var h = head\n    while (h != null) {\n        if (h._val == target)\n            return index\n        h = h.next\n        index++\n    }\n    return -1\n}\n
linked_list.rb
### 連結リストで値が target の最初のノードを探す ###\ndef find(head, target)\n  index = 0\n  while head\n    return index if head.val == target\n    head = head.next\n    index += 1\n  end\n\n  -1\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#422-vs","level":2,"title":"4.2.2   配列 vs. 連結リスト","text":"

次の表は、配列と連結リストの各種特徴と操作効率をまとめたものです。両者は互いに逆の格納戦略を採用しているため、各種性質や操作効率にも対照的な特徴が現れます。

表 4-1   配列と連結リストの効率比較

配列 連結リスト 格納方式 連続したメモリ空間 分散したメモリ空間 容量拡張 長さは不変 柔軟に拡張可能 メモリ効率 要素のメモリ消費は少ないが、空間を無駄にする可能性がある 要素のメモリ消費が多い 要素へのアクセス \\(O(1)\\) \\(O(n)\\) 要素の追加 \\(O(n)\\) \\(O(1)\\) 要素の削除 \\(O(n)\\) \\(O(1)\\)","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#423","level":2,"title":"4.2.3   一般的な連結リストの種類","text":"

下図に示すように、一般的な連結リストの種類は 3 つあります。

  • 単方向連結リスト:前述した通常の連結リストのことです。単方向連結リストのノードは、値と次のノードを指す参照の 2 つのデータを含みます。最初のノードを先頭ノード、最後のノードを末尾ノードと呼び、末尾ノードは空 None を指します。
  • 循環連結リスト:単方向連結リストの末尾ノードを先頭ノードへ向けると(先頭と末尾をつなぐと)、循環連結リストが得られます。循環連結リストでは、任意のノードを先頭ノードとみなせます。
  • 双方向連結リスト:単方向連結リストと比べて、双方向連結リストは 2 方向の参照を記録します。双方向連結リストのノード定義には、後続ノード(次のノード)と前駆ノード(前のノード)を指す参照(ポインタ)が含まれます。単方向連結リストより柔軟で、2 方向に連結リストを走査できますが、そのぶん多くのメモリ空間を必要とします。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class ListNode:\n    \"\"\"双方向連結リストノードクラス\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # ノードの値\n        self.next: ListNode | None = None  # 後続ノードへの参照\n        self.prev: ListNode | None = None  # 前駆ノードへの参照\n
/* 双方向連結リストノード構造体 */\nstruct ListNode {\n    int val;         // ノードの値\n    ListNode *next;  // 後続ノードへのポインタ\n    ListNode *prev;  // 前駆ノードへのポインタ\n    ListNode(int x) : val(x), next(nullptr), prev(nullptr) {}  // コンストラクタ\n};\n
/* 双方向連結リストノードクラス */\nclass ListNode {\n    int val;        // ノードの値\n    ListNode next;  // 後続ノードへの参照\n    ListNode prev;  // 前駆ノードへの参照\n    ListNode(int x) { val = x; }  // コンストラクタ\n}\n
/* 双方向連結リストノードクラス */\nclass ListNode(int x) {  // コンストラクタ\n    int val = x;    // ノードの値\n    ListNode next;  // 後続ノードへの参照\n    ListNode prev;  // 前駆ノードへの参照\n}\n
/* 双方向連結リストノード構造体 */\ntype DoublyListNode struct {\n    Val  int             // ノードの値\n    Next *DoublyListNode // 後続ノードへのポインタ\n    Prev *DoublyListNode // 前駆ノードへのポインタ\n}\n\n// NewDoublyListNode の初期化\nfunc NewDoublyListNode(val int) *DoublyListNode {\n    return &DoublyListNode{\n        Val:  val,\n        Next: nil,\n        Prev: nil,\n    }\n}\n
/* 双方向連結リストノードクラス */\nclass ListNode {\n    var val: Int // ノードの値\n    var next: ListNode? // 後続ノードへの参照\n    var prev: ListNode? // 前駆ノードへの参照\n\n    init(x: Int) { // コンストラクタ\n        val = x\n    }\n}\n
/* 双方向連結リストノードクラス */\nclass ListNode {\n    constructor(val, next, prev) {\n        this.val = val  ===  undefined ? 0 : val;        // ノードの値\n        this.next = next  ===  undefined ? null : next;  // 後続ノードへの参照\n        this.prev = prev  ===  undefined ? null : prev;  // 前駆ノードへの参照\n    }\n}\n
/* 双方向連結リストノードクラス */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    prev: ListNode | null;\n    constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {\n        this.val = val  ===  undefined ? 0 : val;        // ノードの値\n        this.next = next  ===  undefined ? null : next;  // 後続ノードへの参照\n        this.prev = prev  ===  undefined ? null : prev;  // 前駆ノードへの参照\n    }\n}\n
/* 双方向連結リストノードクラス */\nclass ListNode {\n    int val;        // ノードの値\n    ListNode? next;  // 後続ノードへの参照\n    ListNode? prev;  // 前駆ノードへの参照\n    ListNode(this.val, [this.next, this.prev]);  // コンストラクタ\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 双方向連結リストノード型 */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // ノードの値\n    next: Option<Rc<RefCell<ListNode>>>, // 後続ノードへのポインタ\n    prev: Option<Rc<RefCell<ListNode>>>, // 前駆ノードへのポインタ\n}\n\n/* コンストラクタ */\nimpl ListNode {\n    fn new(val: i32) -> Self {\n        ListNode {\n            val,\n            next: None,\n            prev: None,\n        }\n    }\n}\n
/* 双方向連結リストノード構造体 */\ntypedef struct ListNode {\n    int val;               // ノードの値\n    struct ListNode *next; // 後続ノードへのポインタ\n    struct ListNode *prev; // 前駆ノードへのポインタ\n} ListNode;\n\n/* コンストラクタ */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    node->prev = NULL;\n    return node;\n}\n
/* 双方向連結リストノードクラス */\n// コンストラクタ\nclass ListNode(x: Int) {\n    val _val: Int = x           // ノードの値\n    val next: ListNode? = null  // 後続ノードへの参照\n    val prev: ListNode? = null  // 前駆ノードへの参照\n}\n
# 双方向連結リストノードクラス\nclass ListNode\n  attr_accessor :val    # ノードの値\n  attr_accessor :next   # 後続ノードへの参照\n  attr_accessor :prev   # 前駆ノードへの参照\n\n  def initialize(val=0, next_node=nil, prev_node=nil)\n    @val = val\n    @next = next_node\n    @prev = prev_node\n  end\nend\n

図 4-8   一般的な連結リストの種類

","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#424","level":2,"title":"4.2.4   連結リストの典型的な応用","text":"

単方向連結リストは、スタック、キュー、ハッシュテーブル、グラフなどのデータ構造の実装によく用いられます。

  • スタックとキュー:挿入と削除の両方の操作を連結リストの一端で行うと、その性質は後入れ先出しとなり、スタックに対応します。挿入を連結リストの一端で行い、削除をもう一端で行うと、その性質は先入れ先出しとなり、キューに対応します。
  • ハッシュテーブル:連鎖アドレス法はハッシュ衝突を解決する主流の方式の 1 つであり、この方式では、衝突したすべての要素が 1 つの連結リストに格納されます。
  • グラフ:隣接リストはグラフを表現する一般的な方法の 1 つであり、グラフの各頂点は 1 つの連結リストに関連付けられます。連結リスト内の各要素は、その頂点に接続されたほかの頂点を表します。

双方向連結リストは、前後の要素をすばやく見つける必要がある場面でよく用いられます。

  • 高度なデータ構造:たとえば赤黒木や B 木では、ノードの親ノードへアクセスする必要があります。これは、ノード内に親ノードを指す参照を保持することで実現でき、双方向連結リストに似ています。
  • ブラウザ履歴:Web ブラウザでユーザーが進むボタンや戻るボタンをクリックしたとき、ブラウザはユーザーが訪れた前後のページを知る必要があります。双方向連結リストの性質によって、この操作は簡単になります。
  • LRU アルゴリズム:キャッシュ淘汰(LRU)アルゴリズムでは、最近最も使用されていないデータをすばやく見つける必要があり、さらにノードの高速な追加と削除も必要です。そのため、双方向連結リストが非常に適しています。

循環連結リストは、オペレーティングシステムのリソーススケジューリングのように、周期的な操作が必要な場面でよく用いられます。

  • ラウンドロビン時間片スケジューリングアルゴリズム:オペレーティングシステムにおいて、ラウンドロビン時間片スケジューリングは一般的な CPU スケジューリングアルゴリズムであり、一連のプロセスを循環的に処理する必要があります。各プロセスには 1 つの時間片が割り当てられ、その時間片を使い切ると、CPU は次のプロセスへ切り替わります。この循環操作は、循環連結リストで実現できます。
  • データバッファ:一部のデータバッファ実装でも、循環連結リストが使われることがあります。たとえば音声・動画プレーヤーでは、データストリームを複数のバッファブロックに分割して循環連結リストへ格納し、シームレス再生を実現できます。
","path":["第 4 章   配列と連結リスト","4.2   連結リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   リスト","text":"

リスト(list)は抽象的なデータ構造の概念であり、要素の順序付き集合を表す。要素のアクセス、更新、追加、削除、走査などの操作をサポートし、利用者は容量制限の問題を考慮する必要がない。リストは連結リストまたは配列に基づいて実装できる。

  • 連結リストは本質的にリストと見なすことができ、要素の追加・削除・参照・更新をサポートし、柔軟に動的拡張できる。
  • 配列も要素の追加・削除・参照・更新をサポートするが、長さが不変であるため、長さ制限のあるリストとしか見なせない。

配列でリストを実装する場合、長さが不変である性質によってリストの実用性が低下する。これは、通常は事前にどれだけのデータを格納する必要があるかを決められず、適切なリスト長を選びにくいためである。長さが小さすぎると利用要件を満たせない可能性が高く、大きすぎるとメモリ空間の浪費を招く。

この問題を解決するために、動的配列(dynamic array)を用いてリストを実装できる。これは配列の各種利点を引き継ぎつつ、プログラム実行中に動的な拡張を行える。

実際には、多くのプログラミング言語の標準ライブラリが提供するリストは動的配列に基づいて実装されている。たとえば、Python の list 、Java の ArrayList 、C++ の vector 、C# の List などである。以降の議論では、「リスト」と「動的配列」を同じ概念として扱う。

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#431","level":2,"title":"4.3.1   リストの基本操作","text":"","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#1","level":3,"title":"1.   リストの初期化","text":"

通常は「初期値なし」と「初期値あり」の 2 つの初期化方法を用いる。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# リストを初期化\n# 初期値なし\nnums1: list[int] = []\n# 初期値あり\nnums: list[int] = [1, 3, 2, 5, 4]\n
list.cpp
/* リストを初期化 */\n// なお、C++ では vector が本稿でいう nums に相当する\n// 初期値なし\nvector<int> nums1;\n// 初期値あり\nvector<int> nums = { 1, 3, 2, 5, 4 };\n
list.java
/* リストを初期化 */\n// 初期値なし\nList<Integer> nums1 = new ArrayList<>();\n// 初期値あり(配列の要素型は int[] のラッパークラスである Integer[] である必要があることに注意)\nInteger[] numbers = new Integer[] { 1, 3, 2, 5, 4 };\nList<Integer> nums = new ArrayList<>(Arrays.asList(numbers));\n
list.cs
/* リストを初期化 */\n// 初期値なし\nList<int> nums1 = [];\n// 初期値あり\nint[] numbers = [1, 3, 2, 5, 4];\nList<int> nums = [.. numbers];\n
list_test.go
/* リストを初期化 */\n// 初期値なし\nnums1 := []int{}\n// 初期値あり\nnums := []int{1, 3, 2, 5, 4}\n
list.swift
/* リストを初期化 */\n// 初期値なし\nlet nums1: [Int] = []\n// 初期値あり\nvar nums = [1, 3, 2, 5, 4]\n
list.js
/* リストを初期化 */\n// 初期値なし\nconst nums1 = [];\n// 初期値あり\nconst nums = [1, 3, 2, 5, 4];\n
list.ts
/* リストを初期化 */\n// 初期値なし\nconst nums1: number[] = [];\n// 初期値あり\nconst nums: number[] = [1, 3, 2, 5, 4];\n
list.dart
/* リストを初期化 */\n// 初期値なし\nList<int> nums1 = [];\n// 初期値あり\nList<int> nums = [1, 3, 2, 5, 4];\n
list.rs
/* リストを初期化 */\n// 初期値なし\nlet nums1: Vec<i32> = Vec::new();\n// 初期値あり\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* リストを初期化 */\n// 初期値なし\nvar nums1 = listOf<Int>()\n// 初期値あり\nvar numbers = arrayOf(1, 3, 2, 5, 4)\nvar nums = numbers.toMutableList()\n
list.rb
# リストを初期化\n# 初期値なし\nnums1 = []\n# 初期値あり\nnums = [1, 3, 2, 5, 4]\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20%23%20%E6%97%A0%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#2","level":3,"title":"2.   要素へのアクセス","text":"

リストの本質は配列であるため、要素へのアクセスと更新は \\(O(1)\\) 時間で行え、効率が高い。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# 要素にアクセス\nnum: int = nums[1]  # インデックス 1 の要素にアクセス\n\n# 要素を更新\nnums[1] = 0    # インデックス 1 の要素を 0 に更新\n
list.cpp
/* 要素にアクセス */\nint num = nums[1];  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0;  // インデックス 1 の要素を 0 に更新\n
list.java
/* 要素にアクセス */\nint num = nums.get(1);  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums.set(1, 0);  // インデックス 1 の要素を 0 に更新\n
list.cs
/* 要素にアクセス */\nint num = nums[1];  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0;  // インデックス 1 の要素を 0 に更新\n
list_test.go
/* 要素にアクセス */\nnum := nums[1]  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0     // インデックス 1 の要素を 0 に更新\n
list.swift
/* 要素にアクセス */\nlet num = nums[1] // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0 // インデックス 1 の要素を 0 に更新\n
list.js
/* 要素にアクセス */\nconst num = nums[1];  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0;  // インデックス 1 の要素を 0 に更新\n
list.ts
/* 要素にアクセス */\nconst num: number = nums[1];  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0;  // インデックス 1 の要素を 0 に更新\n
list.dart
/* 要素にアクセス */\nint num = nums[1];  // インデックス 1 の要素にアクセス\n\n/* 要素を更新 */\nnums[1] = 0;  // インデックス 1 の要素を 0 に更新\n
list.rs
/* 要素にアクセス */\nlet num: i32 = nums[1];  // インデックス 1 の要素にアクセス\n/* 要素を更新 */\nnums[1] = 0;             // インデックス 1 の要素を 0 に更新\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* 要素にアクセス */\nval num = nums[1]       // インデックス 1 の要素にアクセス\n/* 要素を更新 */\nnums[1] = 0             // インデックス 1 の要素を 0 に更新\n
list.rb
# 要素にアクセス\nnum = nums[1] # インデックス 1 の要素にアクセス\n# 要素を更新\nnums[1] = 0 # インデックス 1 の要素を 0 に更新\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%E8%AE%BF%E9%97%AE%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%0A%0A%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%E5%B0%86%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%E6%9B%B4%E6%96%B0%E4%B8%BA%200&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#3","level":3,"title":"3.   要素の挿入と削除","text":"

配列と比べて、リストでは要素を自由に追加・削除できる。リスト末尾への要素追加の時間計算量は \\(O(1)\\) だが、要素の挿入と削除の効率は依然として配列と同じで、時間計算量は \\(O(n)\\) である。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# リストをクリア\nnums.clear()\n\n# 末尾に要素を追加\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n# 途中に要素を挿入\nnums.insert(3, 6)  # インデックス 3 に数値 6 を挿入\n\n# 要素を削除\nnums.pop(3)        # インデックス 3 の要素を削除\n
list.cpp
/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.push_back(1);\nnums.push_back(3);\nnums.push_back(2);\nnums.push_back(5);\nnums.push_back(4);\n\n/* 途中に要素を挿入 */\nnums.insert(nums.begin() + 3, 6);  // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.erase(nums.begin() + 3);      // インデックス 3 の要素を削除\n
list.java
/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 途中に要素を挿入 */\nnums.add(3, 6);  // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.remove(3);  // インデックス 3 の要素を削除\n
list.cs
/* リストをクリア */\nnums.Clear();\n\n/* 末尾に要素を追加 */\nnums.Add(1);\nnums.Add(3);\nnums.Add(2);\nnums.Add(5);\nnums.Add(4);\n\n/* 途中に要素を挿入 */\nnums.Insert(3, 6);  // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.RemoveAt(3);  // インデックス 3 の要素を削除\n
list_test.go
/* リストをクリア */\nnums = nil\n\n/* 末尾に要素を追加 */\nnums = append(nums, 1)\nnums = append(nums, 3)\nnums = append(nums, 2)\nnums = append(nums, 5)\nnums = append(nums, 4)\n\n/* 途中に要素を挿入 */\nnums = append(nums[:3], append([]int{6}, nums[3:]...)...) // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums = append(nums[:3], nums[4:]...) // インデックス 3 の要素を削除\n
list.swift
/* リストをクリア */\nnums.removeAll()\n\n/* 末尾に要素を追加 */\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n/* 途中に要素を挿入 */\nnums.insert(6, at: 3) // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.remove(at: 3) // インデックス 3 の要素を削除\n
list.js
/* リストをクリア */\nnums.length = 0;\n\n/* 末尾に要素を追加 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 途中に要素を挿入 */\nnums.splice(3, 0, 6); // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.splice(3, 1);  // インデックス 3 の要素を削除\n
list.ts
/* リストをクリア */\nnums.length = 0;\n\n/* 末尾に要素を追加 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 途中に要素を挿入 */\nnums.splice(3, 0, 6); // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.splice(3, 1);  // インデックス 3 の要素を削除\n
list.dart
/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 途中に要素を挿入 */\nnums.insert(3, 6); // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.removeAt(3); // インデックス 3 の要素を削除\n
list.rs
/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 途中に要素を挿入 */\nnums.insert(3, 6);  // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.remove(3);    // インデックス 3 の要素を削除\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* リストをクリア */\nnums.clear();\n\n/* 末尾に要素を追加 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 途中に要素を挿入 */\nnums.add(3, 6);  // インデックス 3 に数値 6 を挿入\n\n/* 要素を削除 */\nnums.remove(3);  // インデックス 3 の要素を削除\n
list.rb
# リストをクリア\nnums.clear\n\n# 末尾に要素を追加\nnums << 1\nnums << 3\nnums << 2\nnums << 5\nnums << 4\n\n# 途中に要素を挿入\nnums.insert(3, 6) # インデックス 3 に数値 6 を挿入\n\n# 要素を削除\nnums.delete_at(3) # インデックス 3 の要素を削除\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B8%85%E7%A9%BA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.clear%28%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E5%B0%BE%E9%83%A8%E6%B7%BB%E5%8A%A0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.append%281%29%0A%20%20%20%20nums.append%283%29%0A%20%20%20%20nums.append%282%29%0A%20%20%20%20nums.append%285%29%0A%20%20%20%20nums.append%284%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.insert%283,%206%29%20%20%23%20%E5%9C%A8%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E6%8F%92%E5%85%A5%E6%95%B0%E5%AD%97%206%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.pop%283%29%20%20%20%20%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#4","level":3,"title":"4.   リストの走査","text":"

配列と同様に、リストもインデックスに基づいて走査することも、各要素を直接走査することもできる。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# インデックスでリストを走査\ncount = 0\nfor i in range(len(nums)):\n    count += nums[i]\n\n# リストの要素を直接走査\nfor num in nums:\n    count += num\n
list.cpp
/* インデックスでリストを走査 */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums[i];\n}\n\n/* リストの要素を直接走査 */\ncount = 0;\nfor (int num : nums) {\n    count += num;\n}\n
list.java
/* インデックスでリストを走査 */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums.get(i);\n}\n\n/* リストの要素を直接走査 */\nfor (int num : nums) {\n    count += num;\n}\n
list.cs
/* インデックスでリストを走査 */\nint count = 0;\nfor (int i = 0; i < nums.Count; i++) {\n    count += nums[i];\n}\n\n/* リストの要素を直接走査 */\ncount = 0;\nforeach (int num in nums) {\n    count += num;\n}\n
list_test.go
/* インデックスでリストを走査 */\ncount := 0\nfor i := 0; i < len(nums); i++ {\n    count += nums[i]\n}\n\n/* リストの要素を直接走査 */\ncount = 0\nfor _, num := range nums {\n    count += num\n}\n
list.swift
/* インデックスでリストを走査 */\nvar count = 0\nfor i in nums.indices {\n    count += nums[i]\n}\n\n/* リストの要素を直接走査 */\ncount = 0\nfor num in nums {\n    count += num\n}\n
list.js
/* インデックスでリストを走査 */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* リストの要素を直接走査 */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
list.ts
/* インデックスでリストを走査 */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* リストの要素を直接走査 */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
list.dart
/* インデックスでリストを走査 */\nint count = 0;\nfor (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* リストの要素を直接走査 */\ncount = 0;\nfor (var num in nums) {\n    count += num;\n}\n
list.rs
// インデックスでリストを走査\nlet mut _count = 0;\nfor i in 0..nums.len() {\n    _count += nums[i];\n}\n\n// リストの要素を直接走査\n_count = 0;\nfor num in &nums {\n    _count += num;\n}\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* インデックスでリストを走査 */\nvar count = 0\nfor (i in nums.indices) {\n    count += nums[i]\n}\n\n/* リストの要素を直接走査 */\nfor (num in nums) {\n    count += num\n}\n
list.rb
# インデックスでリストを走査\ncount = 0\nfor i in 0...nums.length\n    count += nums[i]\nend\n\n# リストの要素を直接走査\ncount = 0\nfor num in nums\n    count += num\nend\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%80%9A%E8%BF%87%E7%B4%A2%E5%BC%95%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%0A%20%20%20%20%23%20%E7%9B%B4%E6%8E%A5%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#5","level":3,"title":"5.   リストの連結","text":"

新しいリスト nums1 が与えられたとき、それを元のリストの末尾に連結できる。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# 2 つのリストを連結\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1  # リスト nums1 を nums の後ろに連結\n
list.cpp
/* 2 つのリストを連結 */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// リスト nums1 を nums の後ろに連結\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n
list.java
/* 2 つのリストを連結 */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1);  // リスト nums1 を nums の後ろに連結\n
list.cs
/* 2 つのリストを連結 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1);  // リスト nums1 を nums の後ろに連結\n
list_test.go
/* 2 つのリストを連結 */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...)  // リスト nums1 を nums の後ろに連結\n
list.swift
/* 2 つのリストを連結 */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // リスト nums1 を nums の後ろに連結\n
list.js
/* 2 つのリストを連結 */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // リスト nums1 を nums の後ろに連結\n
list.ts
/* 2 つのリストを連結 */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // リスト nums1 を nums の後ろに連結\n
list.dart
/* 2 つのリストを連結 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1);  // リスト nums1 を nums の後ろに連結\n
list.rs
/* 2 つのリストを連結 */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* 2 つのリストを連結 */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1)  // リスト nums1 を nums の後ろに連結\n
list.rb
# 2 つのリストを連結\nnums1 = [6, 8, 7, 10, 9]\nnums += nums1\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8B%BC%E6%8E%A5%E4%B8%A4%E4%B8%AA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums1%20%3D%20%5B6,%208,%207,%2010,%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%E5%B0%86%E5%88%97%E8%A1%A8%20nums1%20%E6%8B%BC%E6%8E%A5%E5%88%B0%20nums%20%E4%B9%8B%E5%90%8E&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#6","level":3,"title":"6.   リストをソート","text":"

リストのソートが完了すると、配列系アルゴリズム問題でよく問われる「二分探索」や「両ポインタ」アルゴリズムを使えるようになる。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# リストをソート\nnums.sort()  # ソート後、リスト要素は小さい順に並ぶ\n
list.cpp
/* リストをソート */\nsort(nums.begin(), nums.end());  // ソート後、リスト要素は小さい順に並ぶ\n
list.java
/* リストをソート */\nCollections.sort(nums);  // ソート後、リスト要素は小さい順に並ぶ\n
list.cs
/* リストをソート */\nnums.Sort(); // ソート後、リスト要素は小さい順に並ぶ\n
list_test.go
/* リストをソート */\nsort.Ints(nums)  // ソート後、リスト要素は小さい順に並ぶ\n
list.swift
/* リストをソート */\nnums.sort() // ソート後、リスト要素は小さい順に並ぶ\n
list.js
/* リストをソート */\nnums.sort((a, b) => a - b);  // ソート後、リスト要素は小さい順に並ぶ\n
list.ts
/* リストをソート */\nnums.sort((a, b) => a - b);  // ソート後、リスト要素は小さい順に並ぶ\n
list.dart
/* リストをソート */\nnums.sort(); // ソート後、リスト要素は小さい順に並ぶ\n
list.rs
/* リストをソート */\nnums.sort(); // ソート後、リスト要素は小さい順に並ぶ\n
list.c
// C には組み込みの動的配列がない\n
list.kt
/* リストをソート */\nnums.sort() // ソート後、リスト要素は小さい順に並ぶ\n
list.rb
# リストをソート\nnums = nums.sort { |a, b| a <=> b } # ソート後、リスト要素は小さい順に並ぶ\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8E%92%E5%BA%8F%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.sort%28%29%20%20%23%20%E6%8E%92%E5%BA%8F%E5%90%8E%EF%BC%8C%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%E4%BB%8E%E5%B0%8F%E5%88%B0%E5%A4%A7%E6%8E%92%E5%88%97&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#432","level":2,"title":"4.3.2   リストの実装","text":"

多くのプログラミング言語にはリストが組み込まれており、たとえば Java、C++、Python などがある。それらの実装は比較的複雑で、初期容量や拡張倍率など各種パラメータの設定もよく考えられている。興味があればソースコードを参照して学べる。

リストの動作原理への理解を深めるため、ここでは簡易版のリストを実装し、以下の 3 つの設計ポイントを含める。

  • 初期容量:妥当な配列の初期容量を選ぶ。この例では 10 を初期容量として選ぶ。
  • 要素数の記録:size という変数を宣言して、現在のリスト要素数を記録し、要素の挿入と削除に応じてリアルタイムに更新する。この変数により、リスト末尾の位置を特定し、拡張が必要かどうかを判断できる。
  • 拡張機構:要素を挿入する時点でリスト容量がいっぱいなら、拡張が必要になる。まず拡張倍率に応じてより大きな配列を作成し、次に現在の配列の全要素を順に新しい配列へ移す。この例では、配列を毎回以前の 2 倍に拡張する。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_list.py
class MyList:\n    \"\"\"リストクラス\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self._capacity: int = 10  # リスト容量\n        self._arr: list[int] = [0] * self._capacity  # 配列(リスト要素を格納)\n        self._size: int = 0  # リストの長さ(現在の要素数)\n        self._extend_ratio: int = 2  # リスト拡張時の増加倍率\n\n    def size(self) -> int:\n        \"\"\"リストの長さを取得(現在の要素数)\"\"\"\n        return self._size\n\n    def capacity(self) -> int:\n        \"\"\"リスト容量を取得する\"\"\"\n        return self._capacity\n\n    def get(self, index: int) -> int:\n        \"\"\"要素にアクセス\"\"\"\n        # インデックスが範囲外なら例外を送出する。以下同様\n        if index < 0 or index >= self._size:\n            raise IndexError(\"インデックスが範囲外です\")\n        return self._arr[index]\n\n    def set(self, num: int, index: int):\n        \"\"\"要素を更新\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"インデックスが範囲外です\")\n        self._arr[index] = num\n\n    def add(self, num: int):\n        \"\"\"末尾に要素を追加\"\"\"\n        # 要素数が容量を超えると、拡張機構が発動する\n        if self.size() == self.capacity():\n            self.extend_capacity()\n        self._arr[self._size] = num\n        self._size += 1\n\n    def insert(self, num: int, index: int):\n        \"\"\"中間に要素を挿入\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"インデックスが範囲外です\")\n        # 要素数が容量を超えると、拡張機構が発動する\n        if self._size == self.capacity():\n            self.extend_capacity()\n        # index 以降の要素をすべて 1 つ後ろへずらす\n        for j in range(self._size - 1, index - 1, -1):\n            self._arr[j + 1] = self._arr[j]\n        self._arr[index] = num\n        # 要素数を更新\n        self._size += 1\n\n    def remove(self, index: int) -> int:\n        \"\"\"要素を削除\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"インデックスが範囲外です\")\n        num = self._arr[index]\n        # インデックス index より後の要素をすべて 1 つ前に移動する\n        for j in range(index, self._size - 1):\n            self._arr[j] = self._arr[j + 1]\n        # 要素数を更新\n        self._size -= 1\n        # 削除された要素を返す\n        return num\n\n    def extend_capacity(self):\n        \"\"\"リストの拡張\"\"\"\n        # 元の配列の `_extend_ratio` 倍の長さを持つ新しい配列を作成し、元の配列を新しい配列にコピーする\n        self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1)\n        # リストの容量を更新\n        self._capacity = len(self._arr)\n\n    def to_array(self) -> list[int]:\n        \"\"\"有効長のリストを返す\"\"\"\n        return self._arr[: self._size]\n
my_list.cpp
/* リストクラス */\nclass MyList {\n  private:\n    int *arr;             // 配列(リスト要素を格納)\n    int arrCapacity = 10; // リスト容量\n    int arrSize = 0;      // リストの長さ(現在の要素数)\n    int extendRatio = 2;   // リスト拡張時の増加倍率\n\n  public:\n    /* コンストラクタ */\n    MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* デストラクタメソッド */\n    ~MyList() {\n        delete[] arr;\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    int size() {\n        return arrSize;\n    }\n\n    /* リスト容量を取得する */\n    int capacity() {\n        return arrCapacity;\n    }\n\n    /* 要素にアクセス */\n    int get(int index) {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= size())\n            throw out_of_range(\"インデックスが範囲外\");\n        return arr[index];\n    }\n\n    /* 要素を更新 */\n    void set(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"インデックスが範囲外\");\n        arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    void add(int num) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size() == capacity())\n            extendCapacity();\n        arr[size()] = num;\n        // 要素数を更新\n        arrSize++;\n    }\n\n    /* 中間に要素を挿入 */\n    void insert(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"インデックスが範囲外\");\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size() == capacity())\n            extendCapacity();\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (int j = size() - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // 要素数を更新\n        arrSize++;\n    }\n\n    /* 要素を削除 */\n    int remove(int index) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"インデックスが範囲外\");\n        int num = arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (int j = index; j < size() - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // 要素数を更新\n        arrSize--;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    void extendCapacity() {\n        // 元の配列の `extendRatio` 倍の長さを持つ新しい配列を作成する\n        int newCapacity = capacity() * extendRatio;\n        int *tmp = arr;\n        arr = new int[newCapacity];\n        // 元の配列の全要素を新しい配列にコピー\n        for (int i = 0; i < size(); i++) {\n            arr[i] = tmp[i];\n        }\n        // メモリを解放する\n        delete[] tmp;\n        arrCapacity = newCapacity;\n    }\n\n    /* 出力用にリストを Vector に変換 */\n    vector<int> toVector() {\n        // 有効長の範囲内のリスト要素のみを変換\n        vector<int> vec(size());\n        for (int i = 0; i < size(); i++) {\n            vec[i] = arr[i];\n        }\n        return vec;\n    }\n};\n
my_list.java
/* リストクラス */\nclass MyList {\n    private int[] arr; // 配列(リスト要素を格納)\n    private int capacity = 10; // リスト容量\n    private int size = 0; // リストの長さ(現在の要素数)\n    private int extendRatio = 2; // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    public MyList() {\n        arr = new int[capacity];\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    public int size() {\n        return size;\n    }\n\n    /* リスト容量を取得する */\n    public int capacity() {\n        return capacity;\n    }\n\n    /* 要素にアクセス */\n    public int get(int index) {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n        return arr[index];\n    }\n\n    /* 要素を更新 */\n    public void set(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n        arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    public void add(int num) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size == capacity())\n            extendCapacity();\n        arr[size] = num;\n        // 要素数を更新\n        size++;\n    }\n\n    /* 中間に要素を挿入 */\n    public void insert(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size == capacity())\n            extendCapacity();\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (int j = size - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // 要素数を更新\n        size++;\n    }\n\n    /* 要素を削除 */\n    public int remove(int index) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"インデックスが範囲外です\");\n        int num = arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (int j = index; j < size - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // 要素数を更新\n        size--;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    public void extendCapacity() {\n        // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n        arr = Arrays.copyOf(arr, capacity() * extendRatio);\n        // リストの容量を更新\n        capacity = arr.length;\n    }\n\n    /* リストを配列に変換する */\n    public int[] toArray() {\n        int size = size();\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] arr = new int[size];\n        for (int i = 0; i < size; i++) {\n            arr[i] = get(i);\n        }\n        return arr;\n    }\n}\n
my_list.cs
/* リストクラス */\nclass MyList {\n    private int[] arr;           // 配列(リスト要素を格納)\n    private int arrCapacity = 10;    // リスト容量\n    private int arrSize = 0;         // リストの長さ(現在の要素数)\n    private readonly int extendRatio = 2;  // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    public MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    public int Size() {\n        return arrSize;\n    }\n\n    /* リスト容量を取得する */\n    public int Capacity() {\n        return arrCapacity;\n    }\n\n    /* 要素にアクセス */\n    public int Get(int index) {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"インデックスが範囲外です\");\n        return arr[index];\n    }\n\n    /* 要素を更新 */\n    public void Set(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"インデックスが範囲外です\");\n        arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    public void Add(int num) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        arr[arrSize] = num;\n        // 要素数を更新\n        arrSize++;\n    }\n\n    /* 中間に要素を挿入 */\n    public void Insert(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"インデックスが範囲外です\");\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (int j = arrSize - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // 要素数を更新\n        arrSize++;\n    }\n\n    /* 要素を削除 */\n    public int Remove(int index) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"インデックスが範囲外です\");\n        int num = arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (int j = index; j < arrSize - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // 要素数を更新\n        arrSize--;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    public void ExtendCapacity() {\n        // `arrCapacity * extendRatio` の長さを持つ配列を新規作成し、元の配列を新しい配列にコピーする\n        Array.Resize(ref arr, arrCapacity * extendRatio);\n        // リストの容量を更新\n        arrCapacity = arr.Length;\n    }\n\n    /* リストを配列に変換する */\n    public int[] ToArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] arr = new int[arrSize];\n        for (int i = 0; i < arrSize; i++) {\n            arr[i] = Get(i);\n        }\n        return arr;\n    }\n}\n
my_list.go
/* リストクラス */\ntype myList struct {\n    arrCapacity int\n    arr         []int\n    arrSize     int\n    extendRatio int\n}\n\n/* コンストラクタ */\nfunc newMyList() *myList {\n    return &myList{\n        arrCapacity: 10,              // リスト容量\n        arr:         make([]int, 10), // 配列(リスト要素を格納)\n        arrSize:     0,               // リストの長さ(現在の要素数)\n        extendRatio: 2,               // リスト拡張時の増加倍率\n    }\n}\n\n/* リストの長さを取得(現在の要素数) */\nfunc (l *myList) size() int {\n    return l.arrSize\n}\n\n/* リスト容量を取得する */\nfunc (l *myList) capacity() int {\n    return l.arrCapacity\n}\n\n/* 要素にアクセス */\nfunc (l *myList) get(index int) int {\n    // インデックスが範囲外なら例外を送出する。以下同様\n    if index < 0 || index >= l.arrSize {\n        panic(\"インデックスが範囲外です\")\n    }\n    return l.arr[index]\n}\n\n/* 要素を更新 */\nfunc (l *myList) set(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"インデックスが範囲外です\")\n    }\n    l.arr[index] = num\n}\n\n/* 末尾に要素を追加 */\nfunc (l *myList) add(num int) {\n    // 要素数が容量を超えると、拡張機構が発動する\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    l.arr[l.arrSize] = num\n    // 要素数を更新\n    l.arrSize++\n}\n\n/* 中間に要素を挿入 */\nfunc (l *myList) insert(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"インデックスが範囲外です\")\n    }\n    // 要素数が容量を超えると、拡張機構が発動する\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    // index 以降の要素をすべて 1 つ後ろへずらす\n    for j := l.arrSize - 1; j >= index; j-- {\n        l.arr[j+1] = l.arr[j]\n    }\n    l.arr[index] = num\n    // 要素数を更新\n    l.arrSize++\n}\n\n/* 要素を削除 */\nfunc (l *myList) remove(index int) int {\n    if index < 0 || index >= l.arrSize {\n        panic(\"インデックスが範囲外です\")\n    }\n    num := l.arr[index]\n    // インデックス index より後の要素をすべて 1 つ前に移動する\n    for j := index; j < l.arrSize-1; j++ {\n        l.arr[j] = l.arr[j+1]\n    }\n    // 要素数を更新\n    l.arrSize--\n    // 削除された要素を返す\n    return num\n}\n\n/* リストの拡張 */\nfunc (l *myList) extendCapacity() {\n    // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n    l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...)\n    // リストの容量を更新\n    l.arrCapacity = len(l.arr)\n}\n\n/* 有効長のリストを返す */\nfunc (l *myList) toArray() []int {\n    // 有効長の範囲内のリスト要素のみを変換\n    return l.arr[:l.arrSize]\n}\n
my_list.swift
/* リストクラス */\nclass MyList {\n    private var arr: [Int] // 配列(リスト要素を格納)\n    private var _capacity: Int // リスト容量\n    private var _size: Int // リストの長さ(現在の要素数)\n    private let extendRatio: Int // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    init() {\n        _capacity = 10\n        _size = 0\n        extendRatio = 2\n        arr = Array(repeating: 0, count: _capacity)\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    func size() -> Int {\n        _size\n    }\n\n    /* リスト容量を取得する */\n    func capacity() -> Int {\n        _capacity\n    }\n\n    /* 要素にアクセス */\n    func get(index: Int) -> Int {\n        // インデックスが範囲外ならエラーを投げる。以下同様\n        if index < 0 || index >= size() {\n            fatalError(\"インデックスが範囲外\")\n        }\n        return arr[index]\n    }\n\n    /* 要素を更新 */\n    func set(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"インデックスが範囲外\")\n        }\n        arr[index] = num\n    }\n\n    /* 末尾に要素を追加 */\n    func add(num: Int) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if size() == capacity() {\n            extendCapacity()\n        }\n        arr[size()] = num\n        // 要素数を更新\n        _size += 1\n    }\n\n    /* 中間に要素を挿入 */\n    func insert(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"インデックスが範囲外\")\n        }\n        // 要素数が容量を超えると、拡張機構が発動する\n        if size() == capacity() {\n            extendCapacity()\n        }\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for j in (index ..< size()).reversed() {\n            arr[j + 1] = arr[j]\n        }\n        arr[index] = num\n        // 要素数を更新\n        _size += 1\n    }\n\n    /* 要素を削除 */\n    @discardableResult\n    func remove(index: Int) -> Int {\n        if index < 0 || index >= size() {\n            fatalError(\"インデックスが範囲外\")\n        }\n        let num = arr[index]\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for j in index ..< (size() - 1) {\n            arr[j] = arr[j + 1]\n        }\n        // 要素数を更新\n        _size -= 1\n        // 削除された要素を返す\n        return num\n    }\n\n    /* リストの拡張 */\n    func extendCapacity() {\n        // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n        arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1))\n        // リストの容量を更新\n        _capacity = arr.count\n    }\n\n    /* リストを配列に変換する */\n    func toArray() -> [Int] {\n        Array(arr.prefix(size()))\n    }\n}\n
my_list.js
/* リストクラス */\nclass MyList {\n    #arr = new Array(); // 配列(リスト要素を格納)\n    #capacity = 10; // リスト容量\n    #size = 0; // リストの長さ(現在の要素数)\n    #extendRatio = 2; // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    constructor() {\n        this.#arr = new Array(this.#capacity);\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    size() {\n        return this.#size;\n    }\n\n    /* リスト容量を取得する */\n    capacity() {\n        return this.#capacity;\n    }\n\n    /* 要素にアクセス */\n    get(index) {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です');\n        return this.#arr[index];\n    }\n\n    /* 要素を更新 */\n    set(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です');\n        this.#arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    add(num) {\n        // 長さが容量に等しい場合は拡張が必要\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // 新しい要素をリストの末尾に追加する\n        this.#arr[this.#size] = num;\n        this.#size++;\n    }\n\n    /* 中間に要素を挿入 */\n    insert(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です');\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (let j = this.#size - 1; j >= index; j--) {\n            this.#arr[j + 1] = this.#arr[j];\n        }\n        // 要素数を更新\n        this.#arr[index] = num;\n        this.#size++;\n    }\n\n    /* 要素を削除 */\n    remove(index) {\n        if (index < 0 || index >= this.#size) throw new Error('インデックスが範囲外です');\n        let num = this.#arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (let j = index; j < this.#size - 1; j++) {\n            this.#arr[j] = this.#arr[j + 1];\n        }\n        // 要素数を更新\n        this.#size--;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    extendCapacity() {\n        // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n        this.#arr = this.#arr.concat(\n            new Array(this.capacity() * (this.#extendRatio - 1))\n        );\n        // リストの容量を更新\n        this.#capacity = this.#arr.length;\n    }\n\n    /* リストを配列に変換する */\n    toArray() {\n        let size = this.size();\n        // 有効長の範囲内のリスト要素のみを変換\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
my_list.ts
/* リストクラス */\nclass MyList {\n    private arr: Array<number>; // 配列(リスト要素を格納)\n    private _capacity: number = 10; // リスト容量\n    private _size: number = 0; // リストの長さ(現在の要素数)\n    private extendRatio: number = 2; // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    constructor() {\n        this.arr = new Array(this._capacity);\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    public size(): number {\n        return this._size;\n    }\n\n    /* リスト容量を取得する */\n    public capacity(): number {\n        return this._capacity;\n    }\n\n    /* 要素にアクセス */\n    public get(index: number): number {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です');\n        return this.arr[index];\n    }\n\n    /* 要素を更新 */\n    public set(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です');\n        this.arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    public add(num: number): void {\n        // 長さが容量に等しい場合は拡張が必要\n        if (this._size === this._capacity) this.extendCapacity();\n        // 新しい要素をリストの末尾に追加する\n        this.arr[this._size] = num;\n        this._size++;\n    }\n\n    /* 中間に要素を挿入 */\n    public insert(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です');\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (this._size === this._capacity) {\n            this.extendCapacity();\n        }\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (let j = this._size - 1; j >= index; j--) {\n            this.arr[j + 1] = this.arr[j];\n        }\n        // 要素数を更新\n        this.arr[index] = num;\n        this._size++;\n    }\n\n    /* 要素を削除 */\n    public remove(index: number): number {\n        if (index < 0 || index >= this._size) throw new Error('インデックスが範囲外です');\n        let num = this.arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (let j = index; j < this._size - 1; j++) {\n            this.arr[j] = this.arr[j + 1];\n        }\n        // 要素数を更新\n        this._size--;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    public extendCapacity(): void {\n        // `size` の長さを持つ配列を新規作成し、元の配列を新しい配列にコピーする\n        this.arr = this.arr.concat(\n            new Array(this.capacity() * (this.extendRatio - 1))\n        );\n        // リストの容量を更新\n        this._capacity = this.arr.length;\n    }\n\n    /* リストを配列に変換する */\n    public toArray(): number[] {\n        let size = this.size();\n        // 有効長の範囲内のリスト要素のみを変換\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
my_list.dart
/* リストクラス */\nclass MyList {\n  late List<int> _arr; // 配列(リスト要素を格納)\n  int _capacity = 10; // リスト容量\n  int _size = 0; // リストの長さ(現在の要素数)\n  int _extendRatio = 2; // リスト拡張時の増加倍率\n\n  /* コンストラクタ */\n  MyList() {\n    _arr = List.filled(_capacity, 0);\n  }\n\n  /* リストの長さを取得(現在の要素数) */\n  int size() => _size;\n\n  /* リスト容量を取得する */\n  int capacity() => _capacity;\n\n  /* 要素にアクセス */\n  int get(int index) {\n    if (index >= _size) throw RangeError('インデックスが範囲外です');\n    return _arr[index];\n  }\n\n  /* 要素を更新 */\n  void set(int index, int _num) {\n    if (index >= _size) throw RangeError('インデックスが範囲外です');\n    _arr[index] = _num;\n  }\n\n  /* 末尾に要素を追加 */\n  void add(int _num) {\n    // 要素数が容量を超えると、拡張機構が発動する\n    if (_size == _capacity) extendCapacity();\n    _arr[_size] = _num;\n    // 要素数を更新\n    _size++;\n  }\n\n  /* 中間に要素を挿入 */\n  void insert(int index, int _num) {\n    if (index >= _size) throw RangeError('インデックスが範囲外です');\n    // 要素数が容量を超えると、拡張機構が発動する\n    if (_size == _capacity) extendCapacity();\n    // index 以降の要素をすべて 1 つ後ろへずらす\n    for (var j = _size - 1; j >= index; j--) {\n      _arr[j + 1] = _arr[j];\n    }\n    _arr[index] = _num;\n    // 要素数を更新\n    _size++;\n  }\n\n  /* 要素を削除 */\n  int remove(int index) {\n    if (index >= _size) throw RangeError('インデックスが範囲外です');\n    int _num = _arr[index];\n    // インデックス index より後の要素をすべて 1 つ前に移動する\n    for (var j = index; j < _size - 1; j++) {\n      _arr[j] = _arr[j + 1];\n    }\n    // 要素数を更新\n    _size--;\n    // 削除された要素を返す\n    return _num;\n  }\n\n  /* リストの拡張 */\n  void extendCapacity() {\n    // 元の配列の `_extendRatio` 倍の長さを持つ新しい配列を作成する\n    final _newNums = List.filled(_capacity * _extendRatio, 0);\n    // 元の配列を新しい配列にコピー\n    List.copyRange(_newNums, 0, _arr);\n    // `_arr` の参照を更新\n    _arr = _newNums;\n    // リストの容量を更新\n    _capacity = _arr.length;\n  }\n\n  /* リストを配列に変換する */\n  List<int> toArray() {\n    List<int> arr = [];\n    for (var i = 0; i < _size; i++) {\n      arr.add(get(i));\n    }\n    return arr;\n  }\n}\n
my_list.rs
/* リストクラス */\n#[allow(dead_code)]\nstruct MyList {\n    arr: Vec<i32>,       // 配列(リスト要素を格納)\n    capacity: usize,     // リスト容量\n    size: usize,         // リストの長さ(現在の要素数)\n    extend_ratio: usize, // リスト拡張時の増加倍率\n}\n\n#[allow(unused, unused_comparisons)]\nimpl MyList {\n    /* コンストラクタ */\n    pub fn new(capacity: usize) -> Self {\n        let mut vec = vec![0; capacity];\n        Self {\n            arr: vec,\n            capacity,\n            size: 0,\n            extend_ratio: 2,\n        }\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    pub fn size(&self) -> usize {\n        return self.size;\n    }\n\n    /* リスト容量を取得する */\n    pub fn capacity(&self) -> usize {\n        return self.capacity;\n    }\n\n    /* 要素にアクセス */\n    pub fn get(&self, index: usize) -> i32 {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if index >= self.size {\n            panic!(\"インデックスが範囲外です\")\n        };\n        return self.arr[index];\n    }\n\n    /* 要素を更新 */\n    pub fn set(&mut self, index: usize, num: i32) {\n        if index >= self.size {\n            panic!(\"インデックスが範囲外です\")\n        };\n        self.arr[index] = num;\n    }\n\n    /* 末尾に要素を追加 */\n    pub fn add(&mut self, num: i32) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        self.arr[self.size] = num;\n        // 要素数を更新\n        self.size += 1;\n    }\n\n    /* 中間に要素を挿入 */\n    pub fn insert(&mut self, index: usize, num: i32) {\n        if index >= self.size() {\n            panic!(\"インデックスが範囲外です\")\n        };\n        // 要素数が容量を超えると、拡張機構が発動する\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for j in (index..self.size).rev() {\n            self.arr[j + 1] = self.arr[j];\n        }\n        self.arr[index] = num;\n        // 要素数を更新\n        self.size += 1;\n    }\n\n    /* 要素を削除 */\n    pub fn remove(&mut self, index: usize) -> i32 {\n        if index >= self.size() {\n            panic!(\"インデックスが範囲外です\")\n        };\n        let num = self.arr[index];\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for j in index..self.size - 1 {\n            self.arr[j] = self.arr[j + 1];\n        }\n        // 要素数を更新\n        self.size -= 1;\n        // 削除された要素を返す\n        return num;\n    }\n\n    /* リストの拡張 */\n    pub fn extend_capacity(&mut self) {\n        // 元の配列の extend_ratio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n        let new_capacity = self.capacity * self.extend_ratio;\n        self.arr.resize(new_capacity, 0);\n        // リストの容量を更新\n        self.capacity = new_capacity;\n    }\n\n    /* リストを配列に変換する */\n    pub fn to_array(&self) -> Vec<i32> {\n        // 有効長の範囲内のリスト要素のみを変換\n        let mut arr = Vec::new();\n        for i in 0..self.size {\n            arr.push(self.get(i));\n        }\n        arr\n    }\n}\n
my_list.c
/* リストクラス */\ntypedef struct {\n    int *arr;        // 配列(リスト要素を格納)\n    int capacity;    // リスト容量\n    int size;        // リストのサイズ\n    int extendRatio; // リストが拡張されるたびの倍率\n} MyList;\n\n/* コンストラクタ */\nMyList *newMyList() {\n    MyList *nums = malloc(sizeof(MyList));\n    nums->capacity = 10;\n    nums->arr = malloc(sizeof(int) * nums->capacity);\n    nums->size = 0;\n    nums->extendRatio = 2;\n    return nums;\n}\n\n/* デストラクタ */\nvoid delMyList(MyList *nums) {\n    free(nums->arr);\n    free(nums);\n}\n\n/* リストの長さを取得 */\nint size(MyList *nums) {\n    return nums->size;\n}\n\n/* リスト容量を取得する */\nint capacity(MyList *nums) {\n    return nums->capacity;\n}\n\n/* 要素にアクセス */\nint get(MyList *nums, int index) {\n    assert(index >= 0 && index < nums->size);\n    return nums->arr[index];\n}\n\n/* 要素を更新 */\nvoid set(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < nums->size);\n    nums->arr[index] = num;\n}\n\n/* 末尾に要素を追加 */\nvoid add(MyList *nums, int num) {\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // 容量を拡張\n    }\n    nums->arr[size(nums)] = num;\n    nums->size++;\n}\n\n/* 中間に要素を挿入 */\nvoid insert(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < size(nums));\n    // 要素数が容量を超えると、拡張機構が発動する\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // 容量を拡張\n    }\n    for (int i = size(nums); i > index; --i) {\n        nums->arr[i] = nums->arr[i - 1];\n    }\n    nums->arr[index] = num;\n    nums->size++;\n}\n\n/* 要素を削除 */\n// 注意: stdio.h が remove 識別子を使用している\nint removeItem(MyList *nums, int index) {\n    assert(index >= 0 && index < size(nums));\n    int num = nums->arr[index];\n    for (int i = index; i < size(nums) - 1; i++) {\n        nums->arr[i] = nums->arr[i + 1];\n    }\n    nums->size--;\n    return num;\n}\n\n/* リストの拡張 */\nvoid extendCapacity(MyList *nums) {\n    // 先に領域を確保する\n    int newCapacity = capacity(nums) * nums->extendRatio;\n    int *extend = (int *)malloc(sizeof(int) * newCapacity);\n    int *temp = nums->arr;\n\n    // 古いデータを新しいデータにコピー\n    for (int i = 0; i < size(nums); i++)\n        extend[i] = nums->arr[i];\n\n    // 古いデータを解放する\n    free(temp);\n\n    // 新しいデータに更新\n    nums->arr = extend;\n    nums->capacity = newCapacity;\n}\n\n/* 出力用にリストを Array に変換 */\nint *toArray(MyList *nums) {\n    return nums->arr;\n}\n
my_list.kt
/* リストクラス */\nclass MyList {\n    private var arr: IntArray = intArrayOf() // 配列(リスト要素を格納)\n    private var capacity: Int = 10 // リスト容量\n    private var size: Int = 0 // リストの長さ(現在の要素数)\n    private var extendRatio: Int = 2 // リスト拡張時の増加倍率\n\n    /* コンストラクタ */\n    init {\n        arr = IntArray(capacity)\n    }\n\n    /* リストの長さを取得(現在の要素数) */\n    fun size(): Int {\n        return size\n    }\n\n    /* リスト容量を取得する */\n    fun capacity(): Int {\n        return capacity\n    }\n\n    /* 要素にアクセス */\n    fun get(index: Int): Int {\n        // インデックスが範囲外なら例外を送出する。以下同様\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"インデックスが範囲外\")\n        return arr[index]\n    }\n\n    /* 要素を更新 */\n    fun set(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"インデックスが範囲外\")\n        arr[index] = num\n    }\n\n    /* 末尾に要素を追加 */\n    fun add(num: Int) {\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size == capacity())\n            extendCapacity()\n        arr[size] = num\n        // 要素数を更新\n        size++\n    }\n\n    /* 中間に要素を挿入 */\n    fun insert(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"インデックスが範囲外\")\n        // 要素数が容量を超えると、拡張機構が発動する\n        if (size == capacity())\n            extendCapacity()\n        // index 以降の要素をすべて 1 つ後ろへずらす\n        for (j in size - 1 downTo index)\n            arr[j + 1] = arr[j]\n        arr[index] = num\n        // 要素数を更新\n        size++\n    }\n\n    /* 要素を削除 */\n    fun remove(index: Int): Int {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"インデックスが範囲外\")\n        val num = arr[index]\n        // インデックス index より後の要素をすべて 1 つ前に移動する\n        for (j in index..<size - 1)\n            arr[j] = arr[j + 1]\n        // 要素数を更新\n        size--\n        // 削除された要素を返す\n        return num\n    }\n\n    /* リストの拡張 */\n    fun extendCapacity() {\n        // 元の配列の extendRatio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n        arr = arr.copyOf(capacity() * extendRatio)\n        // リストの容量を更新\n        capacity = arr.size\n    }\n\n    /* リストを配列に変換する */\n    fun toArray(): IntArray {\n        val size = size()\n        // 有効長の範囲内のリスト要素のみを変換\n        val arr = IntArray(size)\n        for (i in 0..<size) {\n            arr[i] = get(i)\n        }\n        return arr\n    }\n}\n
my_list.rb
### リストクラス ###\nclass MyList\n  attr_reader :size       # リストの長さを取得(現在の要素数)\n  attr_reader :capacity   # リスト容量を取得する\n\n  ### コンストラクタ ###\n  def initialize\n    @capacity = 10\n    @size = 0\n    @extend_ratio = 2\n    @arr = Array.new(capacity)\n  end\n\n  ### 要素にアクセス ###\n  def get(index)\n    # インデックスが範囲外なら例外を送出する。以下同様\n    raise IndexError, \"インデックスが範囲外です\" if index < 0 || index >= size\n    @arr[index]\n  end\n\n  ### 要素にアクセス ###\n  def set(index, num)\n    raise IndexError, \"インデックスが範囲外です\" if index < 0 || index >= size\n    @arr[index] = num\n  end\n\n  ### 末尾に要素を追加 ###\n  def add(num)\n    # 要素数が容量を超えると、拡張機構が発動する\n    extend_capacity if size == capacity\n    @arr[size] = num\n\n    # 要素数を更新\n    @size += 1\n  end\n\n  ### 途中に要素を挿入 ###\n  def insert(index, num)\n    raise IndexError, \"インデックスが範囲外です\" if index < 0 || index >= size\n\n    # 要素数が容量を超えると、拡張機構が発動する\n    extend_capacity if size == capacity\n\n    # index 以降の要素をすべて 1 つ後ろへずらす\n    for j in (size - 1).downto(index)\n      @arr[j + 1] = @arr[j]\n    end\n    @arr[index] = num\n\n    # 要素数を更新\n    @size += 1\n  end\n\n  ### 要素の削除 ###\n  def remove(index)\n    raise IndexError, \"インデックスが範囲外です\" if index < 0 || index >= size\n    num = @arr[index]\n\n    # インデックス index より後の要素をすべて 1 つ前に移動する\n    for j in index...size\n      @arr[j] = @arr[j + 1]\n    end\n\n    # 要素数を更新\n    @size -= 1\n\n    # 削除された要素を返す\n    num\n  end\n\n  ### リストの容量拡張 ###\n  def extend_capacity\n    # 元の配列の extend_ratio 倍の長さを持つ新しい配列を作成し、元の配列をコピーする\n    arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1))\n    # リストの容量を更新\n    @capacity = arr.length\n  end\n\n  ### リストを配列に変換 ###\n  def to_array\n    sz = size\n    # 有効長の範囲内のリスト要素のみを変換\n    arr = Array.new(sz)\n    for i in 0...sz\n      arr[i] = get(i)\n    end\n    arr\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 4 章   配列と連結リスト","4.3   リスト"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/","level":1,"title":"4.4   メモリとキャッシュ *","text":"

本章の前二節では、配列と連結リストという二つの基礎的かつ重要なデータ構造を扱いました。これらはそれぞれ「連続格納」と「分散格納」という二つの物理構造を表しています。

実際には、物理構造はプログラムにおけるメモリとキャッシュの利用効率を大きく左右し、ひいてはアルゴリズムプログラム全体の性能に影響します。

","path":["第 4 章   配列と連結リスト","4.4   メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#441","level":2,"title":"4.4.1   コンピュータの記憶装置","text":"

コンピュータには三種類の記憶装置があります。ハードディスク(hard disk)、メモリ(random-access memory, RAM)、キャッシュ(cache memory)です。以下の表は、これらがコンピュータシステムで担う役割と性能上の特徴を示しています。

表 4-2   コンピュータの記憶装置

ハードディスク メモリ キャッシュ 用途 OS、プログラム、ファイルなどを長期保存 実行中のプログラムや処理中のデータを一時保存 頻繁にアクセスされるデータや命令を保存し、CPU のメモリアクセス回数を減らす 揮発性 電源断後もデータは失われない 電源断後にデータは失われる 電源断後にデータは失われる 容量 大きい、TB 級 小さい、GB 級 非常に小さい、MB 級 速度 遅い、数百〜数千 MB/s 速い、数十 GB/s 非常に速い、数十〜数百 GB/s 価格(人民元) 比較的安価、数角〜数元 / GB 比較的高価、数十〜数百元 / GB 非常に高価、CPU と一体で価格設定される

コンピュータの記憶システムは、下図のようなピラミッド構造として捉えられます。ピラミッドの頂点に近い記憶装置ほど速度は速く、容量は小さく、コストは高くなります。この多層構造は偶然ではなく、コンピュータ科学者やエンジニアによる熟慮の末の設計です。

  • ハードディスクはメモリで置き換えにくい。まず、メモリ内のデータは電源断後に失われるため、長期保存には向きません。次に、メモリのコストはハードディスクの数十倍であり、消費者市場で広く普及しにくいという問題があります。
  • キャッシュは大容量と高速性を両立しにくい。L1、L2、L3 キャッシュの容量が段階的に増えるにつれて、物理サイズは大きくなり、CPU コアとの物理的距離も遠くなります。その結果、データ転送時間が増え、要素アクセスの遅延も大きくなります。現在の技術では、多層キャッシュ構造が容量、速度、コストの最適なバランスです。

図 4-9   コンピュータの記憶システム

Tip

コンピュータの記憶階層は、速度、容量、コストの三者間にある巧妙なバランスを体現しています。実際、このようなトレードオフはあらゆる工業分野に広く存在しており、異なる利点と制約のあいだで最適な均衡点を見つけることが求められます。

要するに、ハードディスクは大量データの長期保存に、メモリはプログラム実行中に処理しているデータの一時保存に、キャッシュは頻繁にアクセスされるデータや命令の保存に用いられ、プログラム実行効率を高めます。三者は協調して動作し、コンピュータシステムの高効率な運用を支えています。

次の図に示すように、プログラム実行時にはデータがハードディスクからメモリへ読み込まれ、CPU の計算に使われます。キャッシュは CPU の一部と見なせ、メモリからデータを賢く読み込むことで、CPU に高速なデータ読み出しを提供し、プログラムの実行効率を大きく高め、低速なメモリへの依存を減らします。

図 4-10   ハードディスク、メモリ、キャッシュ間のデータの流れ

","path":["第 4 章   配列と連結リスト","4.4   メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#442","level":2,"title":"4.4.2   データ構造のメモリ効率","text":"

メモリ空間の利用という観点では、配列と連結リストにはそれぞれ利点と制約があります。

一方で、メモリは有限であり、同じメモリ領域を複数のプログラムで共有することはできません。そのため、データ構造にはできるだけ効率よく空間を使うことが求められます。配列の要素は密に並んでおり、連結リストのノード間参照(ポインタ)を保持する追加領域が不要なため、空間効率は高くなります。しかし、配列は十分な連続メモリを一度に確保する必要があり、メモリ浪費を招くことがありますし、拡張時にも追加の時間と空間コストがかかります。これに対して連結リストは「ノード」単位で動的にメモリを割り当て・解放でき、より高い柔軟性を備えています。

他方で、プログラムの実行中には、メモリの確保と解放を繰り返すにつれて、空きメモリの断片化はますます進み、メモリ利用効率の低下を招きます。配列は連続した格納方式を取るため、比較的メモリ断片化を起こしにくい構造です。反対に、連結リストの要素は分散して格納されるため、頻繁な挿入や削除を行うと、より断片化を招きやすくなります。

","path":["第 4 章   配列と連結リスト","4.4   メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#443","level":2,"title":"4.4.3   データ構造のキャッシュ効率","text":"

キャッシュは容量こそメモリよりはるかに小さいものの、速度はメモリよりずっと速く、プログラム実行速度において極めて重要な役割を果たします。キャッシュ容量には限りがあり、頻繁にアクセスされる一部のデータしか保持できません。そのため、CPU がアクセスしようとするデータがキャッシュ内に存在しない場合、キャッシュミス(cache miss)が発生し、CPU は低速なメモリから必要なデータを読み込まなければなりません。

当然ながら、「キャッシュミス」が少ないほど、CPU のデータ読み書き効率は高くなり、プログラム性能も向上します。CPU がキャッシュからデータを正常に取得できた割合をキャッシュヒット率(cache hit rate)と呼び、この指標は通常、キャッシュ効率の評価に用いられます。

できるだけ高い効率を実現するため、キャッシュは次のようなデータ読み込みの仕組みを採用しています。

  • キャッシュライン:キャッシュはデータを 1 バイト単位で保存・読み込みするのではなく、キャッシュライン単位で扱います。1 バイト単位の転送と比べて、キャッシュライン単位のほうが効率的です。
  • プリフェッチ機構:プロセッサはデータアクセスのパターン(たとえば順次アクセス、一定ステップ幅のスキップアクセスなど)を予測し、そのパターンに応じてデータをキャッシュへ読み込むことで、ヒット率を高めます。
  • 空間的局所性:あるデータがアクセスされた場合、その近傍のデータも近いうちにアクセスされる可能性があります。そのため、キャッシュはあるデータを読み込む際に、その周辺のデータもあわせて読み込み、ヒット率を高めます。
  • 時間的局所性:あるデータがアクセスされた場合、そのデータは近い将来に再びアクセスされる可能性が高いです。キャッシュはこの性質を利用し、最近アクセスしたデータを保持することでヒット率を高めます。

実際には、配列と連結リストではキャッシュの利用効率が異なり、主に次の点に表れます。

  • 使用空間:連結リストの要素は配列要素より多くの空間を占めるため、キャッシュに収まる有効データ量は少なくなります。
  • キャッシュライン:連結リストのデータはメモリの各所に分散しており、キャッシュは「ライン単位で読み込む」ため、無効データまで読み込む割合が高くなります。
  • プリフェッチ機構:配列のほうが連結リストよりもデータアクセスのパターンを「予測しやすく」、システムが次に読み込まれるデータを推測しやすくなります。
  • 空間的局所性:配列はまとまったメモリ空間に格納されるため、読み込まれたデータの近くにあるデータも、まもなくアクセスされる可能性が高くなります。

全体として、配列はより高いキャッシュヒット率を持つため、操作効率では通常、連結リストより優れています。このため、アルゴリズム問題を解く際には、配列ベースで実装されたデータ構造のほうが好まれることが多くなります。

注意すべきなのは、**キャッシュ効率が高いからといって、配列があらゆる状況で連結リストより優れているとは限らない**という点です。実際にどのデータ構造を選ぶかは、具体的な要件に応じて決めるべきです。たとえば、配列と連結リストはいずれも「スタック」データ構造を実装できますが(次章で詳しく説明します)、適した場面は異なります。

  • アルゴリズム問題に取り組むときは、一般に配列ベースのスタックを選ぶ傾向があります。より高い操作効率とランダムアクセス能力を備えており、その代償は配列用に一定量のメモリを事前確保することだけです。
  • データ量が非常に大きく、動的性が高く、スタックの想定サイズを見積もりにくい場合は、連結リストベースのスタックのほうが適しています。連結リストなら大量のデータをメモリの異なる場所に分散して保存でき、配列拡張による追加コストも回避できます。
","path":["第 4 章   配列と連結リスト","4.4   メモリとキャッシュ *"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/","level":1,"title":"4.5   まとめ","text":"","path":["第 4 章   配列と連結リスト","4.5   まとめ"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • 配列と連結リストは 2 種類の基本的なデータ構造であり、それぞれコンピュータメモリにおけるデータの 2 つの格納方式、すなわち連続領域への格納と分散領域への格納を表す。両者の特徴は相互補完的である。
  • 配列はランダムアクセスをサポートし、使用メモリも少ない。一方で、要素の挿入と削除の効率は低く、初期化後に長さを変更できない。
  • 連結リストは参照(ポインタ)を変更することでノードの挿入と削除を効率的に行え、長さも柔軟に調整できる。一方で、ノードへのアクセス効率は低く、メモリ使用量も多い。一般的な連結リストには単方向連結リスト、循環連結リスト、双方向連結リストがある。
  • リストは、追加・削除・検索・更新をサポートする順序付き要素集合であり、通常は動的配列に基づいて実装される。配列の利点を保ちながら、長さを柔軟に調整できる。
  • リストの登場により配列の実用性は大幅に高まったが、一部のメモリ領域が無駄になる可能性がある。
  • プログラムの実行時、データは主にメモリに格納される。配列はより高いメモリ空間効率を提供でき、連結リストはメモリ利用の面でより柔軟である。
  • キャッシュは、キャッシュライン、プリフェッチ機構、空間局所性と時間局所性といったデータ読み込み機構を通じて CPU に高速なデータアクセスを提供し、プログラムの実行効率を大きく向上させる。
  • 配列はキャッシュヒット率が高いため、通常は連結リストよりも高効率である。データ構造を選択する際は、具体的な要件や場面に応じて適切に選ぶべきである。
","path":["第 4 章   配列と連結リスト","4.5   まとめ"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:配列をスタックに格納する場合とヒープに格納する場合では、時間効率と空間効率に影響がありますか?

スタック上とヒープ上の配列はいずれも連続したメモリ領域に格納されるため、データ操作の効率は基本的に同じである。ただし、スタックとヒープにはそれぞれ特徴があり、以下の違いが生じる。

  1. 確保と解放の効率:スタックは比較的小さなメモリ領域で、確保はコンパイラによって自動的に行われる。一方、ヒープメモリは相対的に大きく、コード内で動的に確保できる反面、断片化しやすい。そのため、ヒープ上での確保と解放は通常スタック上より遅い。
  2. サイズ制限:スタックメモリは比較的小さく、ヒープのサイズは一般に利用可能メモリに制限される。そのため、ヒープは大きな配列の格納により適している。
  3. 柔軟性:スタック上の配列サイズはコンパイル時に確定している必要があるが、ヒープ上の配列サイズは実行時に動的に決定できる。

Q:なぜ配列では同じ型の要素が求められるのに、連結リストでは同じ型であることが強調されないのですか?

連結リストはノードで構成され、ノード同士は参照(ポインタ)で接続されている。各ノードには intdoublestringobject など、異なる型のデータを格納できる。

これに対して、配列要素は同じ型でなければならない。そうでなければ、オフセットを計算して対応する要素位置を取得できないからである。たとえば、配列に intlong の 2 種類が同時に含まれていて、各要素がそれぞれ 4 バイトと 8 バイトを占める場合、配列内に 2 種類の「要素長」が存在するため、次の式ではオフセットを計算できない。

# 要素のメモリアドレス = 配列のメモリアドレス(先頭要素のメモリアドレス) + 要素長 * 要素インデックス\n

Q:ノード P を削除した後、P.nextNone に設定する必要はありますか?

P.next を変更しなくてもよい。この連結リストの観点では、先頭ノードから末尾ノードまでたどっても、もはや P に出会うことはない。つまり、ノード P はすでに連結リストから削除されており、この時点で P がどこを指していても、この連結リストには影響しない。

データ構造とアルゴリズム(問題を解くとき)の観点では、切り離さなくても問題はなく、プログラムのロジックが正しいことを保証すればよい。標準ライブラリの観点では、切り離したほうがより安全で、ロジックも明確である。切り離さない場合、削除されたノードが適切に回収されなかったとすると、後続ノードのメモリ回収に影響する可能性がある。

Q:連結リストでの挿入と削除の時間計算量は \\(O(1)\\) です。しかし、追加や削除の前には要素を探すのに \\(O(n)\\) の時間が必要です。では、なぜ時間計算量は \\(O(n)\\) ではないのですか?

要素を先に探してから削除するのであれば、時間計算量が \\(O(n)\\) であるのは確かである。しかし、連結リストの \\(O(1)\\) での追加・削除という利点は、ほかの用途で生かせる。たとえば、両端キューは連結リストで実装するのに適しており、先頭ノードと末尾ノードを常に指すポインタ変数を維持すれば、各挿入・削除操作はどれも \\(O(1)\\) になる。

Q:図「連結リストの定義と格納方式」で、薄青色のノードポインタ部分は 1 つのメモリアドレスを占めているのですか? それともノード値と半分ずつなのでしょうか?

この模式図は定性的な表現にすぎず、定量的な表現は具体的な状況に応じて分析する必要がある。

  • ノード値が占める領域は型によって異なり、たとえば intlongdouble、インスタンスオブジェクトなどがある。
  • ポインタ変数が占めるメモリ空間の大きさは、使用する OS やコンパイル環境によって異なり、多くは 8 バイトまたは 4 バイトである。

Q:リストの末尾への要素追加は常に \\(O(1)\\) ですか?

要素を追加する際にリスト長を超える場合は、先にリストを拡張してから追加する必要がある。システムは新しいメモリ領域を確保し、元のリストの全要素をそこへ移動するため、このとき時間計算量は \\(O(n)\\) になる。

Q:「リストの登場により配列の実用性は大きく向上したが、一部のメモリ空間が無駄になる可能性がある」というのは、容量、長さ、拡張倍率のような追加変数が占めるメモリのことですか?

ここでいう空間の無駄には主に 2 つの意味がある。一方では、リストには初期長が設定されるが、必ずしもそれだけ必要とは限らない。もう一方では、頻繁な拡張を防ぐため、拡張時には通常ある係数、たとえば \\(\\times 1.5\\) を掛ける。このため、多くの空きスロットが生じ、通常それらを完全に埋めることはできない。

Q:Python で n = [1, 2, 3] を初期化した後、この 3 つの要素のアドレスは連続しています。しかし m = [2, 1, 3] を初期化すると、各要素の id は連続しておらず、それぞれ n 内の同じ値と一致していることがわかります。これらの要素のアドレスが連続していないなら、m も配列なのですか?

仮にリスト要素を連結リストのノード n = [n1, n2, n3, n4, n5] に置き換えたとしても、通常この 5 つのノードオブジェクトもメモリ上の各所に分散して格納される。それでも、与えられたリストインデックスに対して、私たちは依然として \\(O(1)\\) 時間でノードのメモリアドレスを取得し、対応するノードにアクセスできる。これは、配列に格納されているのがノードそのものではなく、ノードへの参照だからである。

多くの言語と異なり、Python では数値もオブジェクトとしてラップされており、リストに格納されているのは数値そのものではなく、数値への参照である。そのため、2 つの配列内の同じ数値が同一の id を持つことがあり、しかもそれらの数値のメモリアドレスは連続している必要がない。

Q:C++ STL の std::list はすでに双方向連結リストを実装していますが、アルゴリズム本ではあまり直接使われないようです。何か制約があるのでしょうか?

一方では、私たちは多くの場合、アルゴリズムの実装に配列を好み、必要なときにだけ連結リストを使う。その主な理由は 2 つある。

  • 空間オーバーヘッド:各要素には 2 つの追加ポインタ(前の要素用と次の要素用)が必要なため、std::list は通常 std::vector より多くの空間を消費する。
  • キャッシュ非効率:データが連続して格納されていないため、std::list はキャッシュの利用効率が低い。一般には、std::vector のほうが性能がよい。

もう一方では、連結リストを使う必要がある代表的な場面は主に二分木とグラフである。スタックやキューについては、連結リストではなく、たいてい言語が提供する stackqueue を使う。

Q:res = [[0]] * n という操作で 2 次元リストを生成した場合、それぞれの [0] は独立していますか?

独立していない。この 2 次元リストでは、すべての [0] は実際には同一オブジェクトへの参照である。そのうちの 1 つを変更すると、対応するすべての要素が一緒に変化することがわかる。

2 次元リスト内の各 [0] を独立させたい場合は、res = [[0] for _ in range(n)] を使って実現できる。この方式の原理は、独立した [0] リストオブジェクトを \\(n\\) 個初期化していることにある。

Q:res = [0] * n という操作で生成されたリストでは、それぞれの整数 0 は独立していますか?

このリストでは、すべての整数 0 が同一オブジェクトへの参照である。これは、Python が小さな整数(通常は -5 から 256)に対してキャッシュプール機構を採用し、オブジェクトの再利用を最大化して性能を向上させているためである。

それらは同じオブジェクトを指しているが、それでもリスト内の各要素は独立して変更できる。これは、Python の整数が「イミュータブルオブジェクト」だからである。ある要素を変更するとき、実際には別のオブジェクトへの参照に切り替わるのであって、元のオブジェクトそのものを変更しているわけではない。

しかし、リスト要素が「ミュータブルオブジェクト」(たとえばリスト、辞書、クラスインスタンスなど)である場合は、ある要素を変更するとそのオブジェクト自体が直接変更され、そのオブジェクトを参照しているすべての要素に同じ変化が生じる。

","path":["第 4 章   配列と連結リスト","4.5   まとめ"],"tags":[]},{"location":"chapter_backtracking/","level":1,"title":"第 13 章   バックトラッキング","text":"

Abstract

私たちは迷宮の探検者のように、前へ進む道で困難に出会うことがあります。

バックトラッキングの力によってやり直しができ、試行を重ね、最後には光へ通じる出口を見つけられます。

","path":["第 13 章   バックトラッキング"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"章の内容","text":"
  • 13.1   バックトラッキングアルゴリズム
  • 13.2   全順列問題
  • 13.3   部分和問題
  • 13.4   n クイーン問題
  • 13.5   まとめ
","path":["第 13 章   バックトラッキング"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   バックトラッキングアルゴリズム","text":"

バックトラッキングアルゴリズム(backtracking algorithm)は、総当たりによって問題を解く手法です。その中核となる考え方は、初期状態から出発し、あり得るすべての解を力任せに探索し、正しい解に到達したらそれを記録し、解を見つけるか、考えられるすべての選択を試しても解が見つからなくなるまで続ける、というものです。

バックトラッキングアルゴリズムでは、通常「深さ優先探索」を用いて解空間をたどります。「二分木」の章で述べたように、前順・中順・後順走査はいずれも深さ優先探索に属します。ここでは前順走査を使ってバックトラッキング問題を構成し、その仕組みを段階的に理解していきます。

例題1

1 本の二分木が与えられたとき、値が \\(7\\) のノードをすべて探索して記録し、そのノードのリストを返してください。

この問題では、この木を前順走査し、現在のノードの値が \\(7\\) かどうかを判定します。該当する場合は、そのノードの値を結果リスト res に追加します。関連する処理は下図と次のコードのとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
def pre_order(root: TreeNode):\n    \"\"\"前順走査:例題 1\"\"\"\n    if root is None:\n        return\n    if root.val == 7:\n        # 解を記録\n        res.append(root)\n    pre_order(root.left)\n    pre_order(root.right)\n
preorder_traversal_i_compact.cpp
/* 前順走査:例題 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    if (root->val == 7) {\n        // 解を記録\n        res.push_back(root);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
preorder_traversal_i_compact.java
/* 前順走査:例題 1 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // 解を記録\n        res.add(root);\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n}\n
preorder_traversal_i_compact.cs
/* 前順走査:例題 1 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // 解を記録\n        res.Add(root);\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n
preorder_traversal_i_compact.go
/* 前順走査:例題 1 */\nfunc preOrderI(root *TreeNode, res *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    if (root.Val).(int) == 7 {\n        // 解を記録\n        *res = append(*res, root)\n    }\n    preOrderI(root.Left, res)\n    preOrderI(root.Right, res)\n}\n
preorder_traversal_i_compact.swift
/* 前順走査:例題 1 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    if root.val == 7 {\n        // 解を記録\n        res.append(root)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n
preorder_traversal_i_compact.js
/* 前順走査:例題 1 */\nfunction preOrder(root, res) {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // 解を記録\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.ts
/* 前順走査:例題 1 */\nfunction preOrder(root: TreeNode | null, res: TreeNode[]): void {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // 解を記録\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.dart
/* 前順走査:例題 1 */\nvoid preOrder(TreeNode? root, List<TreeNode> res) {\n  if (root == null) {\n    return;\n  }\n  if (root.val == 7) {\n    // 解を記録\n    res.add(root);\n  }\n  preOrder(root.left, res);\n  preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.rs
/* 前順走査:例題 1 */\nfn pre_order(res: &mut Vec<Rc<RefCell<TreeNode>>>, root: Option<&Rc<RefCell<TreeNode>>>) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        if node.borrow().val == 7 {\n            // 解を記録\n            res.push(node.clone());\n        }\n        pre_order(res, node.borrow().left.as_ref());\n        pre_order(res, node.borrow().right.as_ref());\n    }\n}\n
preorder_traversal_i_compact.c
/* 前順走査:例題 1 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    if (root->val == 7) {\n        // 解を記録\n        res[resSize++] = root;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
preorder_traversal_i_compact.kt
/* 前順走査:例題 1 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    if (root._val == 7) {\n        // 解を記録\n        res!!.add(root)\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n}\n
preorder_traversal_i_compact.rb
### 前順走査:例題1 ###\ndef pre_order(root)\n  return unless root\n\n  # 解を記録\n  $res << root if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\nend\n
コードの可視化

全画面で見る >

図 13-1   前順走査でノードを探索する

","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1311","level":2,"title":"13.1.1   試行と戻る","text":"

バックトラッキングアルゴリズムと呼ばれるのは、解空間を探索する際に「試行」と「戻る」という戦略を取るためです。探索中に、ある状態から先へ進めない、または条件を満たす解を得られないと分かった場合、アルゴリズムは直前の選択を取り消して前の状態へ戻り、別の選択肢を試します。

例題1では、各ノードへの訪問が 1 回の「試行」に対応し、葉ノードを越えるか親ノードへ戻る return は「戻る」を表します。

ここで強調しておきたいのは、**戻るとは関数の return だけを指すわけではない**という点です。これを説明するために、例題1を少し拡張します。

例題2

二分木の中で値が \\(7\\) のノードをすべて探索し、根ノードからそれらのノードまでの経路を返してください。

例題1のコードを土台に、訪問済みノードの経路を記録するためのリスト path を導入します。値が \\(7\\) のノードに到達したら、path をコピーして結果リスト res に追加します。走査が完了すると、res にはすべての解が保存されています。コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
def pre_order(root: TreeNode):\n    \"\"\"前順走査:例題 2\"\"\"\n    if root is None:\n        return\n    # 試す\n    path.append(root)\n    if root.val == 7:\n        # 解を記録\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # バックトラック\n    path.pop()\n
preorder_traversal_ii_compact.cpp
/* 前順走査:例題 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    // 試す\n    path.push_back(root);\n    if (root->val == 7) {\n        // 解を記録\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // バックトラック\n    path.pop_back();\n}\n
preorder_traversal_ii_compact.java
/* 前順走査:例題 2 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    // 試す\n    path.add(root);\n    if (root.val == 7) {\n        // 解を記録\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // バックトラック\n    path.remove(path.size() - 1);\n}\n
preorder_traversal_ii_compact.cs
/* 前順走査:例題 2 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    // 試す\n    path.Add(root);\n    if (root.val == 7) {\n        // 解を記録\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // バックトラック\n    path.RemoveAt(path.Count - 1);\n}\n
preorder_traversal_ii_compact.go
/* 前順走査:例題 2 */\nfunc preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    // 試す\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // 解を記録\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderII(root.Left, res, path)\n    preOrderII(root.Right, res, path)\n    // バックトラック\n    *path = (*path)[:len(*path)-1]\n}\n
preorder_traversal_ii_compact.swift
/* 前順走査:例題 2 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 試す\n    path.append(root)\n    if root.val == 7 {\n        // 解を記録\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // バックトラック\n    path.removeLast()\n}\n
preorder_traversal_ii_compact.js
/* 前順走査:例題 2 */\nfunction preOrder(root, path, res) {\n    if (root === null) {\n        return;\n    }\n    // 試す\n    path.push(root);\n    if (root.val === 7) {\n        // 解を記録\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // バックトラック\n    path.pop();\n}\n
preorder_traversal_ii_compact.ts
/* 前順走査:例題 2 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    if (root === null) {\n        return;\n    }\n    // 試す\n    path.push(root);\n    if (root.val === 7) {\n        // 解を記録\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // バックトラック\n    path.pop();\n}\n
preorder_traversal_ii_compact.dart
/* 前順走査:例題 2 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null) {\n    return;\n  }\n\n  // 試す\n  path.add(root);\n  if (root.val == 7) {\n    // 解を記録\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // バックトラック\n  path.removeLast();\n}\n
preorder_traversal_ii_compact.rs
/* 前順走査:例題 2 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        // 試す\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // 解を記録\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // バックトラック\n        path.pop();\n    }\n}\n
preorder_traversal_ii_compact.c
/* 前順走査:例題 2 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    // 試す\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // 解を記録\n        for (int i = 0; i < pathSize; ++i) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // バックトラック\n    pathSize--;\n}\n
preorder_traversal_ii_compact.kt
/* 前順走査:例題 2 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    // 試す\n    path!!.add(root)\n    if (root._val == 7) {\n        // 解を記録\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // バックトラック\n    path!!.removeAt(path!!.size - 1)\n}\n
preorder_traversal_ii_compact.rb
### 前順走査:例題2 ###\ndef pre_order(root)\n  return unless root\n\n  # 試す\n  $path << root\n\n  # 解を記録\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # バックトラック\n  $path.pop\nend\n
コードの可視化

全画面で見る >

各「試行」で現在のノードを path に追加して経路を記録し、「戻る」前にはそのノードを path から取り除き、**今回の試行前の状態を復元する**必要があります。

次の図に示す過程を見ると、試行と戻るは「前進」と「取り消し」として理解できます。この 2 つの操作は互いに逆向きです。

<1><2><3><4><5><6><7><8><9><10><11>

図 13-2   試行と戻る

","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1312","level":2,"title":"13.1.2   枝刈り","text":"

複雑なバックトラッキング問題には、通常 1 つ以上の制約条件が含まれます。制約条件は多くの場合「枝刈り」に利用できます。

例題3

二分木の中で値が \\(7\\) のノードをすべて探索し、根ノードからそれらのノードまでの経路を返してください。ただし、経路には値が \\(3\\) のノードを含めてはいけません。

上の制約条件を満たすために、枝刈り操作を追加する必要があります。探索中に値が \\(3\\) のノードに出会った場合は、そこで早めに return し、それ以上探索を続けません。コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
def pre_order(root: TreeNode):\n    \"\"\"前順走査:例題 3\"\"\"\n    # 枝刈り\n    if root is None or root.val == 3:\n        return\n    # 試す\n    path.append(root)\n    if root.val == 7:\n        # 解を記録\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # バックトラック\n    path.pop()\n
preorder_traversal_iii_compact.cpp
/* 前順走査:例題 3 */\nvoid preOrder(TreeNode *root) {\n    // 枝刈り\n    if (root == nullptr || root->val == 3) {\n        return;\n    }\n    // 試す\n    path.push_back(root);\n    if (root->val == 7) {\n        // 解を記録\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // バックトラック\n    path.pop_back();\n}\n
preorder_traversal_iii_compact.java
/* 前順走査:例題 3 */\nvoid preOrder(TreeNode root) {\n    // 枝刈り\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // 試す\n    path.add(root);\n    if (root.val == 7) {\n        // 解を記録\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // バックトラック\n    path.remove(path.size() - 1);\n}\n
preorder_traversal_iii_compact.cs
/* 前順走査:例題 3 */\nvoid PreOrder(TreeNode? root) {\n    // 枝刈り\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // 試す\n    path.Add(root);\n    if (root.val == 7) {\n        // 解を記録\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // バックトラック\n    path.RemoveAt(path.Count - 1);\n}\n
preorder_traversal_iii_compact.go
/* 前順走査:例題 3 */\nfunc preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    // 枝刈り\n    if root == nil || root.Val == 3 {\n        return\n    }\n    // 試す\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // 解を記録\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderIII(root.Left, res, path)\n    preOrderIII(root.Right, res, path)\n    // バックトラック\n    *path = (*path)[:len(*path)-1]\n}\n
preorder_traversal_iii_compact.swift
/* 前順走査:例題 3 */\nfunc preOrder(root: TreeNode?) {\n    // 枝刈り\n    guard let root = root, root.val != 3 else {\n        return\n    }\n    // 試す\n    path.append(root)\n    if root.val == 7 {\n        // 解を記録\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // バックトラック\n    path.removeLast()\n}\n
preorder_traversal_iii_compact.js
/* 前順走査:例題 3 */\nfunction preOrder(root, path, res) {\n    // 枝刈り\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // 試す\n    path.push(root);\n    if (root.val === 7) {\n        // 解を記録\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // バックトラック\n    path.pop();\n}\n
preorder_traversal_iii_compact.ts
/* 前順走査:例題 3 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // 枝刈り\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // 試す\n    path.push(root);\n    if (root.val === 7) {\n        // 解を記録\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // バックトラック\n    path.pop();\n}\n
preorder_traversal_iii_compact.dart
/* 前順走査:例題 3 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null || root.val == 3) {\n    return;\n  }\n\n  // 試す\n  path.add(root);\n  if (root.val == 7) {\n    // 解を記録\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // バックトラック\n  path.removeLast();\n}\n
preorder_traversal_iii_compact.rs
/* 前順走査:例題 3 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    // 枝刈り\n    if root.is_none() || root.as_ref().unwrap().borrow().val == 3 {\n        return;\n    }\n    if let Some(node) = root {\n        // 試す\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // 解を記録\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // バックトラック\n        path.pop();\n    }\n}\n
preorder_traversal_iii_compact.c
/* 前順走査:例題 3 */\nvoid preOrder(TreeNode *root) {\n    // 枝刈り\n    if (root == NULL || root->val == 3) {\n        return;\n    }\n    // 試す\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // 解を記録\n        for (int i = 0; i < pathSize; i++) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // バックトラック\n    pathSize--;\n}\n
preorder_traversal_iii_compact.kt
/* 前順走査:例題 3 */\nfun preOrder(root: TreeNode?) {\n    // 枝刈り\n    if (root == null || root._val == 3) {\n        return\n    }\n    // 試す\n    path!!.add(root)\n    if (root._val == 7) {\n        // 解を記録\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // バックトラック\n    path!!.removeAt(path!!.size - 1)\n}\n
preorder_traversal_iii_compact.rb
### 前順走査:例題3 ###\ndef pre_order(root)\n  # 枝刈り\n  return if !root || root.val == 3\n\n  # 試す\n  $path.append(root)\n\n  # 解を記録\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # バックトラック\n  $path.pop\nend\n
コードの可視化

全画面で見る >

「枝刈り」は非常にイメージしやすい名称です。次の図のように、探索中に**制約条件を満たさない探索分岐を切り落とす**ことで、多くの無意味な試行を避け、探索効率を高められます。

図 13-3   制約条件にもとづく枝刈り

","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1313","level":2,"title":"13.1.3   フレームワークコード","text":"

次に、バックトラッキングにおける「試行・戻る・枝刈り」の本体部分を抽出し、汎用性の高いコードフレームワークへまとめてみます。

以下のフレームワークコードでは、state は問題の現在状態、choices はその状態で取り得る選択肢を表します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def backtrack(state: State, choices: list[choice], res: list[state]):\n    \"\"\"バックトラッキングアルゴリズムのフレームワーク\"\"\"\n    # 解かどうかを判定\n    if is_solution(state):\n        # 解を記録\n        record_solution(state, res)\n        # これ以上探索しない\n        return\n    # すべての選択肢を走査\n    for choice in choices:\n        # 枝刈り: 選択が妥当かを判定\n        if is_valid(state, choice):\n            # 試行: 選択を行い、状態を更新\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # 戻る: 選択を取り消し、前の状態に戻す\n            undo_choice(state, choice)\n
/* バックトラッキングアルゴリズムのフレームワーク */\nvoid backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for (Choice choice : choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nvoid backtrack(State state, List<Choice> choices, List<State> res) {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for (Choice choice : choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nvoid Backtrack(State state, List<Choice> choices, List<State> res) {\n    // 解かどうかを判定\n    if (IsSolution(state)) {\n        // 解を記録\n        RecordSolution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    foreach (Choice choice in choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (IsValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            MakeChoice(state, choice);\n            Backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            UndoChoice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfunc backtrack(state *State, choices []Choice, res *[]State) {\n    // 解かどうかを判定\n    if isSolution(state) {\n        // 解を記録\n        recordSolution(state, res)\n        // これ以上探索しない\n        return\n    }\n    // すべての選択肢を走査\n    for _, choice := range choices {\n        // 枝刈り: 選択が妥当かを判定\n        if isValid(state, choice) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice)\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfunc backtrack(state: inout State, choices: [Choice], res: inout [State]) {\n    // 解かどうかを判定\n    if isSolution(state: state) {\n        // 解を記録\n        recordSolution(state: state, res: &res)\n        // これ以上探索しない\n        return\n    }\n    // すべての選択肢を走査\n    for choice in choices {\n        // 枝刈り: 選択が妥当かを判定\n        if isValid(state: state, choice: choice) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state: &state, choice: choice)\n            backtrack(state: &state, choices: choices, res: &res)\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfunction backtrack(state, choices, res) {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for (let choice of choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfunction backtrack(state: State, choices: Choice[], res: State[]): void {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for (let choice of choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nvoid backtrack(State state, List<Choice>, List<State> res) {\n  // 解かどうかを判定\n  if (isSolution(state)) {\n    // 解を記録\n    recordSolution(state, res);\n    // これ以上探索しない\n    return;\n  }\n  // すべての選択肢を走査\n  for (Choice choice in choices) {\n    // 枝刈り: 選択が妥当かを判定\n    if (isValid(state, choice)) {\n      // 試行: 選択を行い、状態を更新\n      makeChoice(state, choice);\n      backtrack(state, choices, res);\n      // 戻る: 選択を取り消し、前の状態に戻す\n      undoChoice(state, choice);\n    }\n  }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {\n    // 解かどうかを判定\n    if is_solution(state) {\n        // 解を記録\n        record_solution(state, res);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for choice in choices {\n        // 枝刈り: 選択が妥当かを判定\n        if is_valid(state, choice) {\n            // 試行: 選択を行い、状態を更新\n            make_choice(state, choice);\n            backtrack(state, choices, res);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undo_choice(state, choice);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nvoid backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res, numRes);\n        // これ以上探索しない\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < numChoices; i++) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, &choices[i])) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, &choices[i]);\n            backtrack(state, choices, numChoices, res, numRes);\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, &choices[i]);\n        }\n    }\n}\n
/* バックトラッキングアルゴリズムのフレームワーク */\nfun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {\n    // 解かどうかを判定\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res)\n        // これ以上探索しない\n        return\n    }\n    // すべての選択肢を走査\n    for (choice in choices) {\n        // 枝刈り: 選択が妥当かを判定\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // 戻る: 選択を取り消し、前の状態に戻す\n            undoChoice(state, choice)\n        }\n    }\n}\n
### バックトラッキングアルゴリズムのフレームワーク ###\ndef backtrack(state, choices, res)\n    # 解かどうかを判定\n    if is_solution?(state)\n        # 解を記録\n        record_solution(state, res)\n        return\n    end\n\n    # すべての選択肢を走査\n    for choice in choices\n        # 枝刈り: 選択が妥当かを判定\n        if is_valid?(state, choice)\n            # 試行: 選択を行い、状態を更新\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # 戻る: 選択を取り消し、前の状態に戻す\n            undo_choice(state, choice)\n        end\n    end\nend\n

次に、このフレームワークコードを用いて例題3を解きます。状態 state はノードの走査経路、選択肢 choices は現在のノードの左子ノードと右子ノード、結果 res は経路のリストです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_template.py
def is_solution(state: list[TreeNode]) -> bool:\n    \"\"\"現在の状態が解かどうかを判定\"\"\"\n    return state and state[-1].val == 7\n\ndef record_solution(state: list[TreeNode], res: list[list[TreeNode]]):\n    \"\"\"解を記録\"\"\"\n    res.append(list(state))\n\ndef is_valid(state: list[TreeNode], choice: TreeNode) -> bool:\n    \"\"\"現在の状態で、この選択が有効かどうかを判定\"\"\"\n    return choice is not None and choice.val != 3\n\ndef make_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"状態を更新\"\"\"\n    state.append(choice)\n\ndef undo_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"状態を元に戻す\"\"\"\n    state.pop()\n\ndef backtrack(\n    state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]]\n):\n    \"\"\"バックトラッキング:例題 3\"\"\"\n    # 解かどうかを確認\n    if is_solution(state):\n        # 解を記録\n        record_solution(state, res)\n    # すべての選択肢を走査\n    for choice in choices:\n        # 枝刈り:選択が妥当かを確認する\n        if is_valid(state, choice):\n            # 試行: 選択を行い、状態を更新\n            make_choice(state, choice)\n            # 次の選択へ進む\n            backtrack(state, [choice.left, choice.right], res)\n            # バックトラック:選択を取り消し、前の状態に戻す\n            undo_choice(state, choice)\n
preorder_traversal_iii_template.cpp
/* 現在の状態が解かどうかを判定 */\nbool isSolution(vector<TreeNode *> &state) {\n    return !state.empty() && state.back()->val == 7;\n}\n\n/* 解を記録 */\nvoid recordSolution(vector<TreeNode *> &state, vector<vector<TreeNode *>> &res) {\n    res.push_back(state);\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nbool isValid(vector<TreeNode *> &state, TreeNode *choice) {\n    return choice != nullptr && choice->val != 3;\n}\n\n/* 状態を更新 */\nvoid makeChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.push_back(choice);\n}\n\n/* 状態を元に戻す */\nvoid undoChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.pop_back();\n}\n\n/* バックトラッキング:例題 3 */\nvoid backtrack(vector<TreeNode *> &state, vector<TreeNode *> &choices, vector<vector<TreeNode *>> &res) {\n    // 解かどうかを確認\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n    }\n    // すべての選択肢を走査\n    for (TreeNode *choice : choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            // 次の選択へ進む\n            vector<TreeNode *> nextChoices{choice->left, choice->right};\n            backtrack(state, nextChoices, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.java
/* 現在の状態が解かどうかを判定 */\nboolean isSolution(List<TreeNode> state) {\n    return !state.isEmpty() && state.get(state.size() - 1).val == 7;\n}\n\n/* 解を記録 */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.add(new ArrayList<>(state));\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nboolean isValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* 状態を更新 */\nvoid makeChoice(List<TreeNode> state, TreeNode choice) {\n    state.add(choice);\n}\n\n/* 状態を元に戻す */\nvoid undoChoice(List<TreeNode> state, TreeNode choice) {\n    state.remove(state.size() - 1);\n}\n\n/* バックトラッキング:例題 3 */\nvoid backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // 解かどうかを確認\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n    }\n    // すべての選択肢を走査\n    for (TreeNode choice : choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            // 次の選択へ進む\n            backtrack(state, Arrays.asList(choice.left, choice.right), res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.cs
/* 現在の状態が解かどうかを判定 */\nbool IsSolution(List<TreeNode> state) {\n    return state.Count != 0 && state[^1].val == 7;\n}\n\n/* 解を記録 */\nvoid RecordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.Add(new List<TreeNode>(state));\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nbool IsValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* 状態を更新 */\nvoid MakeChoice(List<TreeNode> state, TreeNode choice) {\n    state.Add(choice);\n}\n\n/* 状態を元に戻す */\nvoid UndoChoice(List<TreeNode> state, TreeNode choice) {\n    state.RemoveAt(state.Count - 1);\n}\n\n/* バックトラッキング:例題 3 */\nvoid Backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // 解かどうかを確認\n    if (IsSolution(state)) {\n        // 解を記録\n        RecordSolution(state, res);\n    }\n    // すべての選択肢を走査\n    foreach (TreeNode choice in choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (IsValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            MakeChoice(state, choice);\n            // 次の選択へ進む\n            Backtrack(state, [choice.left!, choice.right!], res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            UndoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.go
/* 現在の状態が解かどうかを判定 */\nfunc isSolution(state *[]*TreeNode) bool {\n    return len(*state) != 0 && (*state)[len(*state)-1].Val == 7\n}\n\n/* 解を記録 */\nfunc recordSolution(state *[]*TreeNode, res *[][]*TreeNode) {\n    *res = append(*res, append([]*TreeNode{}, *state...))\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfunc isValid(state *[]*TreeNode, choice *TreeNode) bool {\n    return choice != nil && choice.Val != 3\n}\n\n/* 状態を更新 */\nfunc makeChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = append(*state, choice)\n}\n\n/* 状態を元に戻す */\nfunc undoChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = (*state)[:len(*state)-1]\n}\n\n/* バックトラッキング:例題 3 */\nfunc backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) {\n    // 解かどうかを確認\n    if isSolution(state) {\n        // 解を記録\n        recordSolution(state, res)\n    }\n    // すべての選択肢を走査\n    for _, choice := range *choices {\n        // 枝刈り:選択が妥当かを確認する\n        if isValid(state, choice) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice)\n            // 次の選択へ進む\n            temp := make([]*TreeNode, 0)\n            temp = append(temp, choice.Left, choice.Right)\n            backtrackIII(state, &temp, res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state, choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.swift
/* 現在の状態が解かどうかを判定 */\nfunc isSolution(state: [TreeNode]) -> Bool {\n    !state.isEmpty && state.last!.val == 7\n}\n\n/* 解を記録 */\nfunc recordSolution(state: [TreeNode], res: inout [[TreeNode]]) {\n    res.append(state)\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfunc isValid(state: [TreeNode], choice: TreeNode?) -> Bool {\n    choice != nil && choice!.val != 3\n}\n\n/* 状態を更新 */\nfunc makeChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.append(choice)\n}\n\n/* 状態を元に戻す */\nfunc undoChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.removeLast()\n}\n\n/* バックトラッキング:例題 3 */\nfunc backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) {\n    // 解かどうかを確認\n    if isSolution(state: state) {\n        recordSolution(state: state, res: &res)\n    }\n    // すべての選択肢を走査\n    for choice in choices {\n        // 枝刈り:選択が妥当かを確認する\n        if isValid(state: state, choice: choice) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state: &state, choice: choice)\n            // 次の選択へ進む\n            backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.js
/* 現在の状態が解かどうかを判定 */\nfunction isSolution(state) {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* 解を記録 */\nfunction recordSolution(state, res) {\n    res.push([...state]);\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfunction isValid(state, choice) {\n    return choice !== null && choice.val !== 3;\n}\n\n/* 状態を更新 */\nfunction makeChoice(state, choice) {\n    state.push(choice);\n}\n\n/* 状態を元に戻す */\nfunction undoChoice(state) {\n    state.pop();\n}\n\n/* バックトラッキング:例題 3 */\nfunction backtrack(state, choices, res) {\n    // 解かどうかを確認\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n    }\n    // すべての選択肢を走査\n    for (const choice of choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            // 次の選択へ進む\n            backtrack(state, [choice.left, choice.right], res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state);\n        }\n    }\n}\n
preorder_traversal_iii_template.ts
/* 現在の状態が解かどうかを判定 */\nfunction isSolution(state: TreeNode[]): boolean {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* 解を記録 */\nfunction recordSolution(state: TreeNode[], res: TreeNode[][]): void {\n    res.push([...state]);\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfunction isValid(state: TreeNode[], choice: TreeNode): boolean {\n    return choice !== null && choice.val !== 3;\n}\n\n/* 状態を更新 */\nfunction makeChoice(state: TreeNode[], choice: TreeNode): void {\n    state.push(choice);\n}\n\n/* 状態を元に戻す */\nfunction undoChoice(state: TreeNode[]): void {\n    state.pop();\n}\n\n/* バックトラッキング:例題 3 */\nfunction backtrack(\n    state: TreeNode[],\n    choices: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // 解かどうかを確認\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res);\n    }\n    // すべての選択肢を走査\n    for (const choice of choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice);\n            // 次の選択へ進む\n            backtrack(state, [choice.left, choice.right], res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state);\n        }\n    }\n}\n
preorder_traversal_iii_template.dart
/* 現在の状態が解かどうかを判定 */\nbool isSolution(List<TreeNode> state) {\n  return state.isNotEmpty && state.last.val == 7;\n}\n\n/* 解を記録 */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n  res.add(List.from(state));\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nbool isValid(List<TreeNode> state, TreeNode? choice) {\n  return choice != null && choice.val != 3;\n}\n\n/* 状態を更新 */\nvoid makeChoice(List<TreeNode> state, TreeNode? choice) {\n  state.add(choice!);\n}\n\n/* 状態を元に戻す */\nvoid undoChoice(List<TreeNode> state, TreeNode? choice) {\n  state.removeLast();\n}\n\n/* バックトラッキング:例題 3 */\nvoid backtrack(\n  List<TreeNode> state,\n  List<TreeNode?> choices,\n  List<List<TreeNode>> res,\n) {\n  // 解かどうかを確認\n  if (isSolution(state)) {\n    // 解を記録\n    recordSolution(state, res);\n  }\n  // すべての選択肢を走査\n  for (TreeNode? choice in choices) {\n    // 枝刈り:選択が妥当かを確認する\n    if (isValid(state, choice)) {\n      // 試行: 選択を行い、状態を更新\n      makeChoice(state, choice);\n      // 次の選択へ進む\n      backtrack(state, [choice!.left, choice.right], res);\n      // バックトラック:選択を取り消し、前の状態に戻す\n      undoChoice(state, choice);\n    }\n  }\n}\n
preorder_traversal_iii_template.rs
/* 現在の状態が解かどうかを判定 */\nfn is_solution(state: &mut Vec<Rc<RefCell<TreeNode>>>) -> bool {\n    return !state.is_empty() && state.last().unwrap().borrow().val == 7;\n}\n\n/* 解を記録 */\nfn record_solution(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    res.push(state.clone());\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfn is_valid(_: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Option<&Rc<RefCell<TreeNode>>>) -> bool {\n    return choice.is_some() && choice.unwrap().borrow().val != 3;\n}\n\n/* 状態を更新 */\nfn make_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Rc<RefCell<TreeNode>>) {\n    state.push(choice);\n}\n\n/* 状態を元に戻す */\nfn undo_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, _: Rc<RefCell<TreeNode>>) {\n    state.pop();\n}\n\n/* バックトラッキング:例題 3 */\nfn backtrack(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    choices: &Vec<Option<&Rc<RefCell<TreeNode>>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    // 解かどうかを確認\n    if is_solution(state) {\n        // 解を記録\n        record_solution(state, res);\n    }\n    // すべての選択肢を走査\n    for &choice in choices.iter() {\n        // 枝刈り:選択が妥当かを確認する\n        if is_valid(state, choice) {\n            // 試行: 選択を行い、状態を更新\n            make_choice(state, choice.unwrap().clone());\n            // 次の選択へ進む\n            backtrack(\n                state,\n                &vec![\n                    choice.unwrap().borrow().left.as_ref(),\n                    choice.unwrap().borrow().right.as_ref(),\n                ],\n                res,\n            );\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undo_choice(state, choice.unwrap().clone());\n        }\n    }\n}\n
preorder_traversal_iii_template.c
/* 現在の状態が解かどうかを判定 */\nbool isSolution(void) {\n    return pathSize > 0 && path[pathSize - 1]->val == 7;\n}\n\n/* 解を記録 */\nvoid recordSolution(void) {\n    for (int i = 0; i < pathSize; i++) {\n        res[resSize][i] = path[i];\n    }\n    resSize++;\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nbool isValid(TreeNode *choice) {\n    return choice != NULL && choice->val != 3;\n}\n\n/* 状態を更新 */\nvoid makeChoice(TreeNode *choice) {\n    path[pathSize++] = choice;\n}\n\n/* 状態を元に戻す */\nvoid undoChoice(void) {\n    pathSize--;\n}\n\n/* バックトラッキング:例題 3 */\nvoid backtrack(TreeNode *choices[2]) {\n    // 解かどうかを確認\n    if (isSolution()) {\n        // 解を記録\n        recordSolution();\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < 2; i++) {\n        TreeNode *choice = choices[i];\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(choice);\n            // 次の選択へ進む\n            TreeNode *nextChoices[2] = {choice->left, choice->right};\n            backtrack(nextChoices);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice();\n        }\n    }\n}\n
preorder_traversal_iii_template.kt
/* 現在の状態が解かどうかを判定 */\nfun isSolution(state: MutableList<TreeNode?>): Boolean {\n    return state.isNotEmpty() && state[state.size - 1]?._val == 7\n}\n\n/* 解を記録 */\nfun recordSolution(state: MutableList<TreeNode?>?, res: MutableList<MutableList<TreeNode?>?>) {\n    res.add(state!!.toMutableList())\n}\n\n/* 現在の状態で、この選択が有効かどうかを判定 */\nfun isValid(state: MutableList<TreeNode?>?, choice: TreeNode?): Boolean {\n    return choice != null && choice._val != 3\n}\n\n/* 状態を更新 */\nfun makeChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.add(choice)\n}\n\n/* 状態を元に戻す */\nfun undoChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.removeLast()\n}\n\n/* バックトラッキング:例題 3 */\nfun backtrack(\n    state: MutableList<TreeNode?>,\n    choices: MutableList<TreeNode?>,\n    res: MutableList<MutableList<TreeNode?>?>\n) {\n    // 解かどうかを確認\n    if (isSolution(state)) {\n        // 解を記録\n        recordSolution(state, res)\n    }\n    // すべての選択肢を走査\n    for (choice in choices) {\n        // 枝刈り:選択が妥当かを確認する\n        if (isValid(state, choice)) {\n            // 試行: 選択を行い、状態を更新\n            makeChoice(state, choice)\n            // 次の選択へ進む\n            backtrack(state, mutableListOf(choice!!.left, choice.right), res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            undoChoice(state, choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.rb
### 現在の状態が解かどうかを判定 ###\ndef is_solution?(state)\n  !state.empty? && state.last.val == 7\nend\n\n### 解を記録 ###\ndef record_solution(state, res)\n  res << state.dup\nend\n\n### 現在の状態で、この選択が妥当かを判定 ###\ndef is_valid?(state, choice)\n  choice && choice.val != 3\nend\n\n### 状態を更新 ###\ndef make_choice(state, choice)\n  state << choice\nend\n\n### 状態を復元 ###\ndef undo_choice(state, choice)\n  state.pop\nend\n\n### バックトラッキング法:例題3 ###\ndef backtrack(state, choices, res)\n  # 解かどうかを確認\n  record_solution(state, res) if is_solution?(state)\n\n  # すべての選択肢を走査\n  for choice in choices\n    # 枝刈り:選択が妥当かを確認する\n    if is_valid?(state, choice)\n      # 試行: 選択を行い、状態を更新\n      make_choice(state, choice)\n      # 次の選択へ進む\n      backtrack(state, [choice.left, choice.right], res)\n      # バックトラック:選択を取り消し、前の状態に戻す\n      undo_choice(state, choice)\n    end\n  end\nend\n
コードの可視化

全画面で見る >

問題の条件より、値が \\(7\\) のノードを見つけた後も探索を続ける必要があります。そのため、解を記録した後の return 文は削除しなければなりません。次の図は、return 文を残す場合と削除する場合の探索過程を比較したものです。

図 13-4   return を残す場合と削除する場合の探索過程の比較

前順走査にもとづく実装と比べると、バックトラッキングアルゴリズムのフレームワークにもとづく実装はやや冗長に見えますが、汎用性に優れています。実際、多くのバックトラッキング問題はこのフレームワークで解けます。具体的な問題に応じて statechoices を定義し、各メソッドを実装すれば十分です。

","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   よく使われる用語","text":"

アルゴリズム問題をより明確に分析するために、バックトラッキングでよく使われる用語の意味を整理し、例題3に対応する例を次の表にまとめます。

表 13-1   よく使われるバックトラッキング用語

用語 定義 例題3 解(solution) 問題の特定の条件を満たす答えであり、1 つまたは複数存在し得る 根ノードからノード \\(7\\) までの、制約条件を満たすすべての経路 制約条件(constraint) 解の実現可能性を制限する条件であり、通常は枝刈りに用いられる 経路にノード \\(3\\) を含まないこと 状態(state) ある時点における問題の状況を表し、すでに行った選択を含む 現在までに訪問したノードの経路、すなわち path ノードリスト 試行(attempt) 利用可能な選択肢にもとづいて解空間を探索する過程であり、選択、状態更新、解判定を含む 左右の子ノードを再帰的に訪問し、ノードを path に追加し、値が \\(7\\) か判定する 戻る(backtracking) 制約条件を満たさない状態に出会ったとき、それまでの選択を取り消して前の状態へ戻ること 葉ノードを越えたとき、ノード訪問を終えたとき、値が \\(3\\) のノードに出会ったときに探索を終了し、関数から戻る 枝刈り(pruning) 問題の性質や制約条件にもとづき、無意味な探索経路を避ける方法であり、探索効率を高める 値が \\(3\\) のノードに出会ったら、それ以上探索しない

Tip

問題、解、状態などの概念は汎用的であり、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムにも共通して現れます。

","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1315","level":2,"title":"13.1.5   利点と限界","text":"

バックトラッキングアルゴリズムの本質は深さ優先探索です。条件を満たす解を見つけるまで、あり得るすべての解を試します。この方法の利点は、考えられるすべての解を見つけられることであり、適切な枝刈りを行えば高い効率を発揮します。

しかし、大規模または複雑な問題を扱う場合、バックトラッキングアルゴリズムの実行効率は受け入れがたいことがあります。

  • 時間:バックトラッキングアルゴリズムでは通常、状態空間のすべての可能性をたどる必要があり、時間計算量は指数時間や階乗時間に達することがあります。
  • 空間:再帰呼び出しの過程では現在の状態(たとえば経路や枝刈り用の補助変数など)を保持する必要があり、深さが大きいと空間使用量も大きくなります。

それでもなお、バックトラッキングアルゴリズムは一部の探索問題や制約充足問題に対する最良の解法です。この種の問題では、どの選択が有効な解を生むかを事前に予測できないため、可能な選択肢をすべてたどる必要があります。このときの鍵は**いかに効率を最適化するか**であり、代表的な方法は 2 つあります。

  • 枝刈り:解が生じないことが確実な経路を探索しないことで、時間と空間を節約する。
  • ヒューリスティック探索:探索中に何らかの戦略や推定値を導入し、有効な解を生みやすい経路を優先的に探索する。
","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316","level":2,"title":"13.1.6   バックトラッキングの典型例題","text":"

バックトラッキングアルゴリズムは、多くの探索問題、制約充足問題、組合せ最適化問題の解決に利用できます。

探索問題:この種の問題の目標は、特定の条件を満たす解を見つけることです。

  • 全順列問題:ある集合が与えられたとき、考えられるすべての順列を求める。
  • 部分和問題:ある集合と目標和が与えられたとき、和が目標値となるすべての部分集合を見つける。
  • ハノイの塔問題:3 本の柱と大きさの異なる複数の円盤が与えられたとき、すべての円盤を 1 本の柱から別の柱へ移動する。ただし 1 回に 1 枚しか動かせず、大きい円盤を小さい円盤の上に置いてはならない。

制約充足問題:この種の問題の目標は、すべての制約条件を満たす解を見つけることです。

  • \\(n\\) クイーン問題:\\(n \\times n\\) の盤面に \\(n\\) 個のクイーンを配置し、互いに攻撃し合わないようにする。
  • 数独:\\(9 \\times 9\\) のグリッドに数字 \\(1\\) ~ \\(9\\) を入れ、各行・各列・各 \\(3 \\times 3\\) の小区画で数字が重複しないようにする。
  • グラフ彩色問題:無向グラフが与えられたとき、隣接する頂点が同じ色にならないように、できるだけ少ない色で各頂点を彩色する。

組合せ最適化問題:この種の問題の目標は、組合せ空間の中で条件を満たす最適解を見つけることです。

  • 0-1 ナップサック問題:複数の品物とナップサックが与えられ、各品物には価値と重さがある。ナップサック容量の範囲内で総価値が最大になるように品物を選ぶ。
  • 巡回セールスマン問題:グラフ内のある頂点から出発し、他のすべての頂点をちょうど 1 回ずつ訪れて出発点へ戻るときの最短経路を求める。
  • 最大クリーク問題:無向グラフが与えられたとき、任意の 2 頂点間に辺が存在する最大の完全部分グラフを見つける。

多くの組合せ最適化問題では、バックトラッキングは最適な解法ではない点に注意してください。

  • 0-1 ナップサック問題は通常、より高い時間効率を得るために動的計画法で解く。
  • 巡回セールスマン問題は著名な NP-Hard 問題であり、よく用いられる解法には遺伝的アルゴリズムや蟻コロニー最適化などがある。
  • 最大クリーク問題はグラフ理論における古典的問題であり、貪欲法などのヒューリスティックで解ける。
","path":["第 13 章   バックトラッキング","13.1   バックトラッキングアルゴリズム"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/","level":1,"title":"13.4   n クイーン問題","text":"

Question

チェスのルールによれば、クイーンは同じ行、同じ列、または同じ斜線上にある駒を攻撃できます。\\(n\\) 個のクイーンと \\(n \\times n\\) サイズの盤面が与えられたとき、すべてのクイーンが互いに攻撃し合わない配置を求めます。

下図に示すように、\\(n = 4\\) のとき、2 つの解を見つけることができます。バックトラッキングの観点から見ると、\\(n \\times n\\) サイズの盤面には合計 \\(n^2\\) 個のマスがあり、これがすべての選択肢 choices を与えます。クイーンを 1 つずつ配置していく過程で、盤面の状態は絶えず変化し、その各時点の盤面が状態 state です。

図 13-15   4 クイーン問題の解

下図は本問題の 3 つの制約条件を示しています。複数のクイーンは同じ行、同じ列、同じ対角線上に置けません。なお、対角線には主対角線 \\ と副対角線 / の 2 種類があります。

図 13-16   n クイーン問題の制約条件

","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1","level":3,"title":"1.   行ごとの配置戦略","text":"

クイーンの数と盤面の行数はいずれも \\(n\\) なので、次の推論を容易に得られます:盤面の各行にはクイーンを 1 つだけ配置できます。

つまり、行ごとの配置戦略を採用できます:最初の行から始めて、各行に 1 つのクイーンを配置し、最後の行まで進みます。

下図は 4 クイーン問題における行ごとの配置過程を示しています。図の大きさの都合上、下図では 1 行目における検索分岐の 1 つだけを展開し、列制約と対角線制約を満たさない案はすべて枝刈りしています。

図 13-17   行ごとの配置戦略

本質的には、行ごとの配置戦略は枝刈りとして機能します。これにより、同じ行に複数のクイーンが現れるすべての探索分岐を回避できます。

","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2","level":3,"title":"2.   列と対角線の枝刈り","text":"

列制約を満たすために、長さ \\(n\\) のブール配列 cols を用いて、各列にクイーンがあるかどうかを記録できます。配置を決めるたびに、cols を使って既存のクイーンがある列を枝刈りし、バックトラッキングの中で cols の状態を動的に更新します。

Tip

注意として、行列の原点は左上にあり、行インデックスは上から下へ、列インデックスは左から右へ増加します。

では、対角線制約はどのように扱えばよいのでしょうか。盤面上のあるマスの行列インデックスを \\((row, col)\\) とし、行列内のある主対角線を選ぶと、その対角線上のすべてのマスで行インデックスから列インデックスを引いた値が等しいことが分かります。つまり、主対角線上のすべてのマスでは \\(row - col\\) が一定値になります。

つまり、2 つのマスが \\(row_1 - col_1 = row_2 - col_2\\) を満たすなら、それらは必ず同じ主対角線上にあります。この性質を利用して、下図の配列 diags1 により、各主対角線にクイーンがあるかどうかを記録できます。

同様に、副対角線上のすべてのマスでは \\(row + col\\) が一定値です。副対角線制約も配列 diags2 を使って処理できます。

図 13-18   列制約と対角線制約の処理

","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#3","level":3,"title":"3.   コード実装","text":"

注意として、\\(n\\) 次正方行列では \\(row - col\\) の範囲は \\([-n + 1, n - 1]\\) 、\\(row + col\\) の範囲は \\([0, 2n - 2]\\) です。したがって、主対角線と副対角線の本数はいずれも \\(2n - 1\\) であり、配列 diags1diags2 の長さもともに \\(2n - 1\\) です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby n_queens.py
def backtrack(\n    row: int,\n    n: int,\n    state: list[list[str]],\n    res: list[list[list[str]]],\n    cols: list[bool],\n    diags1: list[bool],\n    diags2: list[bool],\n):\n    \"\"\"バックトラッキング:N クイーン\"\"\"\n    # すべての行への配置が完了したら、解を記録する\n    if row == n:\n        res.append([list(row) for row in state])\n        return\n    # すべての列を走査\n    for col in range(n):\n        # このマスに対応する主対角線と副対角線を計算\n        diag1 = row - col + n - 1\n        diag2 = row + col\n        # 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if not cols[col] and not diags1[diag1] and not diags2[diag2]:\n            # 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\"\n            cols[col] = diags1[diag1] = diags2[diag2] = True\n            # 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            # 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\"\n            cols[col] = diags1[diag1] = diags2[diag2] = False\n\ndef n_queens(n: int) -> list[list[list[str]]]:\n    \"\"\"N クイーンを解く\"\"\"\n    # n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    state = [[\"#\" for _ in range(n)] for _ in range(n)]\n    cols = [False] * n  # 列にクイーンがあるか記録\n    diags1 = [False] * (2 * n - 1)  # 主対角線にクイーンがあるかを記録\n    diags2 = [False] * (2 * n - 1)  # 副対角線にクイーンがあるかを記録\n    res = []\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n
n_queens.cpp
/* バックトラッキング:N クイーン */\nvoid backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,\n               vector<bool> &diags1, vector<bool> &diags2) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row == n) {\n        res.push_back(state);\n        return;\n    }\n    // すべての列を走査\n    for (int col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nvector<vector<vector<string>>> nQueens(int n) {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    vector<vector<string>> state(n, vector<string>(n, \"#\"));\n    vector<bool> cols(n, false);           // 列にクイーンがあるか記録\n    vector<bool> diags1(2 * n - 1, false); // 主対角線にクイーンがあるかを記録\n    vector<bool> diags2(2 * n - 1, false); // 副対角線にクイーンがあるかを記録\n    vector<vector<vector<string>>> res;\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.java
/* バックトラッキング:N クイーン */\nvoid backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,\n        boolean[] cols, boolean[] diags1, boolean[] diags2) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row == n) {\n        List<List<String>> copyState = new ArrayList<>();\n        for (List<String> sRow : state) {\n            copyState.add(new ArrayList<>(sRow));\n        }\n        res.add(copyState);\n        return;\n    }\n    // すべての列を走査\n    for (int col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state.get(row).set(col, \"Q\");\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state.get(row).set(col, \"#\");\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nList<List<List<String>>> nQueens(int n) {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    List<List<String>> state = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<String> row = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            row.add(\"#\");\n        }\n        state.add(row);\n    }\n    boolean[] cols = new boolean[n]; // 列にクイーンがあるか記録\n    boolean[] diags1 = new boolean[2 * n - 1]; // 主対角線にクイーンがあるかを記録\n    boolean[] diags2 = new boolean[2 * n - 1]; // 副対角線にクイーンがあるかを記録\n    List<List<List<String>>> res = new ArrayList<>();\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.cs
/* バックトラッキング:N クイーン */\nvoid Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,\n        bool[] cols, bool[] diags1, bool[] diags2) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row == n) {\n        List<List<string>> copyState = [];\n        foreach (List<string> sRow in state) {\n            copyState.Add(new List<string>(sRow));\n        }\n        res.Add(copyState);\n        return;\n    }\n    // すべての列を走査\n    for (int col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            Backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nList<List<List<string>>> NQueens(int n) {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    List<List<string>> state = [];\n    for (int i = 0; i < n; i++) {\n        List<string> row = [];\n        for (int j = 0; j < n; j++) {\n            row.Add(\"#\");\n        }\n        state.Add(row);\n    }\n    bool[] cols = new bool[n]; // 列にクイーンがあるか記録\n    bool[] diags1 = new bool[2 * n - 1]; // 主対角線にクイーンがあるかを記録\n    bool[] diags2 = new bool[2 * n - 1]; // 副対角線にクイーンがあるかを記録\n    List<List<List<string>>> res = [];\n\n    Backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.go
/* バックトラッキング:N クイーン */\nfunc backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {\n    // すべての行への配置が完了したら、解を記録する\n    if row == n {\n        newState := make([][]string, len(*state))\n        for i, _ := range newState {\n            newState[i] = make([]string, len((*state)[0]))\n            copy(newState[i], (*state)[i])\n\n        }\n        *res = append(*res, newState)\n        return\n    }\n    // すべての列を走査\n    for col := 0; col < n; col++ {\n        // このマスに対応する主対角線と副対角線を計算\n        diag1 := row - col + n - 1\n        diag2 := row + col\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {\n            // 試行:そのマスにクイーンを置く\n            (*state)[row][col] = \"Q\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true\n            // 次の行に配置する\n            backtrack(row+1, n, state, res, cols, diags1, diags2)\n            // 戻す:そのマスを空きマスに戻す\n            (*state)[row][col] = \"#\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false\n        }\n    }\n}\n\n/* N クイーンを解く */\nfunc nQueens(n int) [][][]string {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    state := make([][]string, n)\n    for i := 0; i < n; i++ {\n        row := make([]string, n)\n        for i := 0; i < n; i++ {\n            row[i] = \"#\"\n        }\n        state[i] = row\n    }\n    // 列にクイーンがあるか記録\n    cols := make([]bool, n)\n    diags1 := make([]bool, 2*n-1)\n    diags2 := make([]bool, 2*n-1)\n    res := make([][][]string, 0)\n    backtrack(0, n, &state, &res, &cols, &diags1, &diags2)\n    return res\n}\n
n_queens.swift
/* バックトラッキング:N クイーン */\nfunc backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {\n    // すべての行への配置が完了したら、解を記録する\n    if row == n {\n        res.append(state)\n        return\n    }\n    // すべての列を走査\n    for col in 0 ..< n {\n        // このマスに対応する主対角線と副対角線を計算\n        let diag1 = row - col + n - 1\n        let diag2 = row + col\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\"\n            cols[col] = true\n            diags1[diag1] = true\n            diags2[diag2] = true\n            // 次の行に配置する\n            backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\"\n            cols[col] = false\n            diags1[diag1] = false\n            diags2[diag2] = false\n        }\n    }\n}\n\n/* N クイーンを解く */\nfunc nQueens(n: Int) -> [[[String]]] {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    var state = Array(repeating: Array(repeating: \"#\", count: n), count: n)\n    var cols = Array(repeating: false, count: n) // 列にクイーンがあるか記録\n    var diags1 = Array(repeating: false, count: 2 * n - 1) // 主対角線にクイーンがあるかを記録\n    var diags2 = Array(repeating: false, count: 2 * n - 1) // 副対角線にクイーンがあるかを記録\n    var res: [[[String]]] = []\n\n    backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n\n    return res\n}\n
n_queens.js
/* バックトラッキング:N クイーン */\nfunction backtrack(row, n, state, res, cols, diags1, diags2) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // すべての列を走査\n    for (let col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nfunction nQueens(n) {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // 列にクイーンがあるか記録\n    const diags1 = Array(2 * n - 1).fill(false); // 主対角線にクイーンがあるかを記録\n    const diags2 = Array(2 * n - 1).fill(false); // 副対角線にクイーンがあるかを記録\n    const res = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
n_queens.ts
/* バックトラッキング:N クイーン */\nfunction backtrack(\n    row: number,\n    n: number,\n    state: string[][],\n    res: string[][][],\n    cols: boolean[],\n    diags1: boolean[],\n    diags2: boolean[]\n): void {\n    // すべての行への配置が完了したら、解を記録する\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // すべての列を走査\n    for (let col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nfunction nQueens(n: number): string[][][] {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // 列にクイーンがあるか記録\n    const diags1 = Array(2 * n - 1).fill(false); // 主対角線にクイーンがあるかを記録\n    const diags2 = Array(2 * n - 1).fill(false); // 副対角線にクイーンがあるかを記録\n    const res: string[][][] = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
n_queens.dart
/* バックトラッキング:N クイーン */\nvoid backtrack(\n  int row,\n  int n,\n  List<List<String>> state,\n  List<List<List<String>>> res,\n  List<bool> cols,\n  List<bool> diags1,\n  List<bool> diags2,\n) {\n  // すべての行への配置が完了したら、解を記録する\n  if (row == n) {\n    List<List<String>> copyState = [];\n    for (List<String> sRow in state) {\n      copyState.add(List.from(sRow));\n    }\n    res.add(copyState);\n    return;\n  }\n  // すべての列を走査\n  for (int col = 0; col < n; col++) {\n    // このマスに対応する主対角線と副対角線を計算\n    int diag1 = row - col + n - 1;\n    int diag2 = row + col;\n    // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n    if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n      // 試行:そのマスにクイーンを置く\n      state[row][col] = \"Q\";\n      cols[col] = true;\n      diags1[diag1] = true;\n      diags2[diag2] = true;\n      // 次の行に配置する\n      backtrack(row + 1, n, state, res, cols, diags1, diags2);\n      // 戻す:そのマスを空きマスに戻す\n      state[row][col] = \"#\";\n      cols[col] = false;\n      diags1[diag1] = false;\n      diags2[diag2] = false;\n    }\n  }\n}\n\n/* N クイーンを解く */\nList<List<List<String>>> nQueens(int n) {\n  // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n  List<List<String>> state = List.generate(n, (index) => List.filled(n, \"#\"));\n  List<bool> cols = List.filled(n, false); // 列にクイーンがあるか記録\n  List<bool> diags1 = List.filled(2 * n - 1, false); // 主対角線にクイーンがあるかを記録\n  List<bool> diags2 = List.filled(2 * n - 1, false); // 副対角線にクイーンがあるかを記録\n  List<List<List<String>>> res = [];\n\n  backtrack(0, n, state, res, cols, diags1, diags2);\n\n  return res;\n}\n
n_queens.rs
/* バックトラッキング:N クイーン */\nfn backtrack(\n    row: usize,\n    n: usize,\n    state: &mut Vec<Vec<String>>,\n    res: &mut Vec<Vec<Vec<String>>>,\n    cols: &mut [bool],\n    diags1: &mut [bool],\n    diags2: &mut [bool],\n) {\n    // すべての行への配置が完了したら、解を記録する\n    if row == n {\n        res.push(state.clone());\n        return;\n    }\n    // すべての列を走査\n    for col in 0..n {\n        // このマスに対応する主対角線と副対角線を計算\n        let diag1 = row + n - 1 - col;\n        let diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);\n        }\n    }\n}\n\n/* N クイーンを解く */\nfn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    let mut state: Vec<Vec<String>> = vec![vec![\"#\".to_string(); n]; n];\n    let mut cols = vec![false; n]; // 列にクイーンがあるか記録\n    let mut diags1 = vec![false; 2 * n - 1]; // 主対角線にクイーンがあるかを記録\n    let mut diags2 = vec![false; 2 * n - 1]; // 副対角線にクイーンがあるかを記録\n    let mut res: Vec<Vec<Vec<String>>> = Vec::new();\n\n    backtrack(\n        0,\n        n,\n        &mut state,\n        &mut res,\n        &mut cols,\n        &mut diags1,\n        &mut diags2,\n    );\n\n    res\n}\n
n_queens.c
/* バックトラッキング:N クイーン */\nvoid backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE],\n               bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row == n) {\n        res[*resSize] = (char **)malloc(sizeof(char *) * n);\n        for (int i = 0; i < n; ++i) {\n            res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1));\n            strcpy(res[*resSize][i], state[i]);\n        }\n        (*resSize)++;\n        return;\n    }\n    // すべての列を走査\n    for (int col = 0; col < n; col++) {\n        // このマスに対応する主対角線と副対角線を計算\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* N クイーンを解く */\nchar ***nQueens(int n, int *returnSize) {\n    char state[MAX_SIZE][MAX_SIZE];\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    for (int i = 0; i < n; ++i) {\n        for (int j = 0; j < n; ++j) {\n            state[i][j] = '#';\n        }\n        state[i][n] = '\\0';\n    }\n    bool cols[MAX_SIZE] = {false};           // 列にクイーンがあるか記録\n    bool diags1[2 * MAX_SIZE - 1] = {false}; // 主対角線にクイーンがあるかを記録\n    bool diags2[2 * MAX_SIZE - 1] = {false}; // 副対角線にクイーンがあるかを記録\n\n    char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE);\n    *returnSize = 0;\n    backtrack(0, n, state, res, returnSize, cols, diags1, diags2);\n    return res;\n}\n
n_queens.kt
/* バックトラッキング:N クイーン */\nfun backtrack(\n    row: Int,\n    n: Int,\n    state: MutableList<MutableList<String>>,\n    res: MutableList<MutableList<MutableList<String>>?>,\n    cols: BooleanArray,\n    diags1: BooleanArray,\n    diags2: BooleanArray\n) {\n    // すべての行への配置が完了したら、解を記録する\n    if (row == n) {\n        val copyState = mutableListOf<MutableList<String>>()\n        for (sRow in state) {\n            copyState.add(sRow.toMutableList())\n        }\n        res.add(copyState)\n        return\n    }\n    // すべての列を走査\n    for (col in 0..<n) {\n        // このマスに対応する主対角線と副対角線を計算\n        val diag1 = row - col + n - 1\n        val diag2 = row + col\n        // 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 試行:そのマスにクイーンを置く\n            state[row][col] = \"Q\"\n            diags2[diag2] = true\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n            // 次の行に配置する\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            // 戻す:そのマスを空きマスに戻す\n            state[row][col] = \"#\"\n            diags2[diag2] = false\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n        }\n    }\n}\n\n/* N クイーンを解く */\nfun nQueens(n: Int): MutableList<MutableList<MutableList<String>>?> {\n    // n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n    val state = mutableListOf<MutableList<String>>()\n    for (i in 0..<n) {\n        val row = mutableListOf<String>()\n        for (j in 0..<n) {\n            row.add(\"#\")\n        }\n        state.add(row)\n    }\n    val cols = BooleanArray(n) // 列にクイーンがあるか記録\n    val diags1 = BooleanArray(2 * n - 1) // 主対角線にクイーンがあるかを記録\n    val diags2 = BooleanArray(2 * n - 1) // 副対角線にクイーンがあるかを記録\n    val res = mutableListOf<MutableList<MutableList<String>>?>()\n\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n}\n
n_queens.rb
### バックトラッキング法:Nクイーン ###\ndef backtrack(row, n, state, res, cols, diags1, diags2)\n  # すべての行への配置が完了したら、解を記録する\n  if row == n\n    res << state.map { |row| row.dup }\n    return\n  end\n\n  # すべての列を走査\n  for col in 0...n\n    # このマスに対応する主対角線と副対角線を計算\n    diag1 = row - col + n - 1\n    diag2 = row + col\n    # 枝刈り:そのマスの列、主対角線、副対角線にクイーンがあってはならない\n    if !cols[col] && !diags1[diag1] && !diags2[diag2]\n      # 試行:そのマスにクイーンを置く\n      state[row][col] = \"Q\"\n      cols[col] = diags1[diag1] = diags2[diag2] = true\n      # 次の行に配置する\n      backtrack(row + 1, n, state, res, cols, diags1, diags2)\n      # 戻す:そのマスを空きマスに戻す\n      state[row][col] = \"#\"\n      cols[col] = diags1[diag1] = diags2[diag2] = false\n    end\n  end\nend\n\n### Nクイーンを解く ###\ndef n_queens(n)\n  # n*n の盤面を初期化する。'Q' はクイーン、'#' は空きマスを表す\n  state = Array.new(n) { Array.new(n, \"#\") }\n  cols = Array.new(n, false) # 列にクイーンがあるか記録\n  diags1 = Array.new(2 * n - 1, false) # 主対角線にクイーンがあるかを記録\n  diags2 = Array.new(2 * n - 1, false) # 副対角線にクイーンがあるかを記録\n  res = []\n  backtrack(0, n, state, res, cols, diags1, diags2)\n\n  res\nend\n
コードの可視化

全画面で見る >

行ごとに \\(n\\) 回配置し、列制約を考慮すると、1 行目から最終行までの選択肢はそれぞれ \\(n\\)、\\(n-1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) 個となるため、時間計算量は \\(O(n!)\\) です。解を記録する際には、行列 state をコピーして res に追加する必要があり、このコピー操作には \\(O(n^2)\\) 時間を要します。したがって、全体の時間計算量は \\(O(n! \\cdot n^2)\\) です。実際には、対角線制約による枝刈りも探索空間を大きく縮小できるため、探索効率はしばしば上記の時間計算量より良くなります。

配列 state は \\(O(n^2)\\) の空間を使用し、配列 colsdiags1diags2 はいずれも \\(O(n)\\) の空間を使用します。最大再帰深さは \\(n\\) で、スタックフレーム空間として \\(O(n)\\) を使用します。したがって、空間計算量は \\(O(n^2)\\) です。

","path":["第 13 章   バックトラッキング","13.4   n クイーン問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/","level":1,"title":"13.2   全順列問題","text":"

全順列問題はバックトラッキングアルゴリズムの典型的な応用例です。これは、ある集合(配列や文字列など)が与えられたとき、その要素のあり得るすべての順列を求める問題です。

下表に、入力配列とそれに対応するすべての順列から成る例をいくつか示します。

表 13-2   全順列の例

入力配列 すべての順列 \\([1]\\) \\([1]\\) \\([1, 2]\\) \\([1, 2], [2, 1]\\) \\([1, 2, 3]\\) \\([1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]\\)","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1321","level":2,"title":"13.2.1   等しい要素がない場合","text":"

Question

重複要素を含まない整数配列を入力として受け取り、あり得るすべての順列を返します。

バックトラッキングアルゴリズムの観点から見ると、順列生成の過程は一連の選択の結果として捉えられます。入力配列が \\([1, 2, 3]\\) だとすると、最初に \\(1\\) を選び、次に \\(3\\) を選び、最後に \\(2\\) を選べば、順列 \\([1, 3, 2]\\) が得られます。戻る操作は 1 つの選択を取り消し、その後で別の選択を試し続けることを表します。

バックトラッキングコードの観点では、候補集合 choices は入力配列中のすべての要素であり、状態 state は現時点までに選ばれた要素です。各要素は 1 回しか選べないことに注意してください。したがって state 内の要素はすべて一意でなければなりません。

下図のように、探索過程は再帰木として展開できます。木の各ノードは現在の状態 state を表します。根ノードから始めて 3 ラウンドの選択を経て葉ノードに到達し、各葉ノードが 1 つの順列に対応します。

図 13-5   全順列の再帰木

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1","level":3,"title":"1.   重複選択の枝刈り","text":"

各要素が 1 回しか選ばれないようにするため、ブール配列 selected の導入を考えます。ここで selected[i]choices[i] がすでに選ばれているかどうかを表し、これに基づいて次の枝刈りを行います。

  • 選択 choice[i] を行った後、selected[i] を \\(\\text{True}\\) に設定し、その要素が選択済みであることを表します。
  • 選択肢リスト choices を走査するとき、すでに選ばれたノードはすべてスキップします。これが枝刈りです。

下図のように、1 回目に 1、2 回目に 3、3 回目に 2 を選ぶとします。このとき 2 回目では要素 1 の分岐を、3 回目では要素 1 と要素 3 の分岐を刈り取る必要があります。

図 13-6   全順列の枝刈り例

上図から、この枝刈りにより探索空間の大きさは \\(O(n^n)\\) から \\(O(n!)\\) へ削減されることがわかります。

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2","level":3,"title":"2.   コード実装","text":"

以上を整理できれば、フレームワークコードの「穴埋め」を行えます。全体のコードを短くするため、フレームワークコード中の各関数を個別には実装せず、これらを backtrack() 関数内に展開します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_i.py
def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"バックトラッキング:順列 I\"\"\"\n    # 状態の長さが要素数に等しければ、解を記録\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # すべての選択肢を走査\n    for i, choice in enumerate(choices):\n        # 枝刈り:要素の重複選択を許可しない\n        if not selected[i]:\n            # 試行: 選択を行い、状態を更新\n            selected[i] = True\n            state.append(choice)\n            # 次の選択へ進む\n            backtrack(state, choices, selected, res)\n            # バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = False\n            state.pop()\n\ndef permutations_i(nums: list[int]) -> list[list[int]]:\n    \"\"\"全順列 I\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
permutations_i.cpp
/* バックトラッキング:順列 I */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.push_back(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* 全順列 I */\nvector<vector<int>> permutationsI(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
permutations_i.java
/* バックトラッキング:順列 I */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.add(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* 全順列 I */\nList<List<Integer>> permutationsI(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
permutations_i.cs
/* バックトラッキング:順列 I */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.Add(choice);\n            // 次の選択へ進む\n            Backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* 全順列 I */\nList<List<int>> PermutationsI(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
permutations_i.go
/* バックトラッキング:順列 I */\nfunc backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // すべての選択肢を走査\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // 枝刈り:要素の重複選択を許可しない\n        if !(*selected)[i] {\n            // 試行: 選択を行い、状態を更新\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // 次の選択へ進む\n            backtrackI(state, choices, selected, res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* 全順列 I */\nfunc permutationsI(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackI(&state, &nums, &selected, &res)\n    return res\n}\n
permutations_i.swift
/* バックトラッキング:順列 I */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // すべての選択肢を走査\n    for (i, choice) in choices.enumerated() {\n        // 枝刈り:要素の重複選択を許可しない\n        if !selected[i] {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true\n            state.append(choice)\n            // 次の選択へ進む\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* 全順列 I */\nfunc permutationsI(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
permutations_i.js
/* バックトラッキング:順列 I */\nfunction backtrack(state, choices, selected, res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    choices.forEach((choice, i) => {\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全順列 I */\nfunction permutationsI(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_i.ts
/* バックトラッキング:順列 I */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    choices.forEach((choice, i) => {\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全順列 I */\nfunction permutationsI(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_i.dart
/* バックトラッキング:順列 I */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // 状態の長さが要素数に等しければ、解を記録\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // すべての選択肢を走査\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // 枝刈り:要素の重複選択を許可しない\n    if (!selected[i]) {\n      // 試行: 選択を行い、状態を更新\n      selected[i] = true;\n      state.add(choice);\n      // 次の選択へ進む\n      backtrack(state, choices, selected, res);\n      // バックトラック:選択を取り消し、前の状態に戻す\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* 全順列 I */\nList<List<int>> permutationsI(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
permutations_i.rs
/* バックトラッキング:順列 I */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // すべての選択肢を走査\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // 枝刈り:要素の重複選択を許可しない\n        if !selected[i] {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state.clone(), choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* 全順列 I */\nfn permutations_i(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new(); // 状態(部分集合)\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
permutations_i.c
/* バックトラッキング:順列 I */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true;\n            state[stateSize] = choice;\n            // 次の選択へ進む\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n        }\n    }\n}\n\n/* 全順列 I */\nint **permutationsI(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
permutations_i.kt
/* バックトラッキング:順列 I */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // すべての選択肢を走査\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // 枝刈り:要素の重複選択を許可しない\n        if (!selected[i]) {\n            // 試行: 選択を行い、状態を更新\n            selected[i] = true\n            state.add(choice)\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* 全順列 I */\nfun permutationsI(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
permutations_i.rb
### バックトラッキング法:全順列 I ###\ndef backtrack(state, choices, selected, res)\n  # 状態の長さが要素数に等しければ、解を記録\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # すべての選択肢を走査\n  choices.each_with_index do |choice, i|\n    # 枝刈り:要素の重複選択を許可しない\n    unless selected[i]\n      # 試行: 選択を行い、状態を更新\n      selected[i] = true\n      state << choice\n      # 次の選択へ進む\n      backtrack(state, choices, selected, res)\n      # バックトラック:選択を取り消し、前の状態に戻す\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### 全順列 I ###\ndef permutations_i(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
コードの可視化

全画面で見る >

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1322","level":2,"title":"13.2.2   等しい要素を考慮する場合","text":"

Question

整数配列を入力として受け取り、配列には重複要素が含まれる場合があります。重複しない順列をすべて返します。

入力配列が \\([1, 1, 2]\\) だと仮定します。2 つの重複する要素 \\(1\\) を区別しやすくするため、2 つ目の \\(1\\) を \\(\\hat{1}\\) と記します。

下図のように、上述の方法で生成される順列の半分は重複しています。

図 13-7   重複した順列

では、重複した順列をどのように取り除けばよいのでしょうか。最も直接的なのは、ハッシュ集合を用いて順列結果をそのまま重複排除する方法です。しかしこのやり方は十分に洗練されていません。なぜなら、重複順列を生成する探索分岐はそもそも不要であり、事前に見つけて枝刈りすべきだからです。そうすることで、アルゴリズム効率をさらに高められます。

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1_1","level":3,"title":"1.   等しい要素の枝刈り","text":"

下図を見ると、1 回目のラウンドでは \\(1\\) を選ぶことと \\(\\hat{1}\\) を選ぶことは等価であり、これら 2 つの選択の下で生成される順列はすべて重複します。したがって \\(\\hat{1}\\) を枝刈りすべきです。

同様に、1 回目で \\(2\\) を選んだ後では、2 回目のラウンドにおける \\(1\\) と \\(\\hat{1}\\) も重複分岐を生むため、2 回目の \\(\\hat{1}\\) も枝刈りすべきです。

本質的には、各ラウンドの選択において、等しい複数の要素が 1 回しか選ばれないようにすることが目標です。

図 13-8   重複順列の枝刈り

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2_1","level":3,"title":"2.   コード実装","text":"

前問のコードを土台として、各ラウンドの選択でハッシュ集合 duplicated を 1 つ用意し、そのラウンドですでに試した要素を記録して、重複要素を枝刈りすることを考えます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_ii.py
def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"バックトラッキング:順列 II\"\"\"\n    # 状態の長さが要素数に等しければ、解を記録\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # すべての選択肢を走査\n    duplicated = set[int]()\n    for i, choice in enumerate(choices):\n        # 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if not selected[i] and choice not in duplicated:\n            # 試行: 選択を行い、状態を更新\n            duplicated.add(choice)  # 選択済みの要素値を記録\n            selected[i] = True\n            state.append(choice)\n            # 次の選択へ進む\n            backtrack(state, choices, selected, res)\n            # バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = False\n            state.pop()\n\ndef permutations_ii(nums: list[int]) -> list[list[int]]:\n    \"\"\"全順列 II\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
permutations_ii.cpp
/* バックトラッキング:順列 II */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // すべての選択肢を走査\n    unordered_set<int> duplicated;\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && duplicated.find(choice) == duplicated.end()) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.emplace(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.push_back(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* 全順列 II */\nvector<vector<int>> permutationsII(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
permutations_ii.java
/* バックトラッキング:順列 II */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    Set<Integer> duplicated = new HashSet<Integer>();\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.add(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.add(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* 全順列 II */\nList<List<Integer>> permutationsII(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
permutations_ii.cs
/* バックトラッキング:順列 II */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    HashSet<int> duplicated = [];\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated.Contains(choice)) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.Add(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.Add(choice);\n            // 次の選択へ進む\n            Backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* 全順列 II */\nList<List<int>> PermutationsII(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
permutations_ii.go
/* バックトラッキング:順列 II */\nfunc backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // すべての選択肢を走査\n    duplicated := make(map[int]struct{}, 0)\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if _, ok := duplicated[choice]; !ok && !(*selected)[i] {\n            // 試す: 選択を行って状態を更新\n            // 選択済みの要素値を記録\n            duplicated[choice] = struct{}{}\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // 次の選択へ進む\n            backtrackII(state, choices, selected, res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* 全順列 II */\nfunc permutationsII(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackII(&state, &nums, &selected, &res)\n    return res\n}\n
permutations_ii.swift
/* バックトラッキング:順列 II */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // すべての選択肢を走査\n    var duplicated: Set<Int> = []\n    for (i, choice) in choices.enumerated() {\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if !selected[i], !duplicated.contains(choice) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.insert(choice) // 選択済みの要素値を記録\n            selected[i] = true\n            state.append(choice)\n            // 次の選択へ進む\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* 全順列 II */\nfunc permutationsII(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
permutations_ii.js
/* バックトラッキング:順列 II */\nfunction backtrack(state, choices, selected, res) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated.has(choice)) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.add(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全順列 II */\nfunction permutationsII(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_ii.ts
/* バックトラッキング:順列 II */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated.has(choice)) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.add(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全順列 II */\nfunction permutationsII(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_ii.dart
/* バックトラッキング:順列 II */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // 状態の長さが要素数に等しければ、解を記録\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // すべての選択肢を走査\n  Set<int> duplicated = {};\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n    if (!selected[i] && !duplicated.contains(choice)) {\n      // 試行: 選択を行い、状態を更新\n      duplicated.add(choice); // 選択済みの要素値を記録\n      selected[i] = true;\n      state.add(choice);\n      // 次の選択へ進む\n      backtrack(state, choices, selected, res);\n      // バックトラック:選択を取り消し、前の状態に戻す\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* 全順列 II */\nList<List<int>> permutationsII(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
permutations_ii.rs
/* バックトラッキング:順列 II */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // すべての選択肢を走査\n    let mut duplicated = HashSet::<i32>::new();\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if !selected[i] && !duplicated.contains(&choice) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.insert(choice); // 選択済みの要素値を記録\n            selected[i] = true;\n            state.push(choice);\n            // 次の選択へ進む\n            backtrack(state.clone(), choices, selected, res);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* 全順列 II */\nfn permutations_ii(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new();\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
permutations_ii.c
/* バックトラッキング:順列 II */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // すべての選択肢を走査\n    bool duplicated[MAX_SIZE] = {false};\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated[choice]) {\n            // 試行: 選択を行い、状態を更新\n            duplicated[choice] = true; // 選択済みの要素値を記録\n            selected[i] = true;\n            state[stateSize] = choice;\n            // 次の選択へ進む\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false;\n        }\n    }\n}\n\n/* 全順列 II */\nint **permutationsII(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
permutations_ii.kt
/* バックトラッキング:順列 II */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 状態の長さが要素数に等しければ、解を記録\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // すべての選択肢を走査\n    val duplicated = HashSet<Int>()\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // 試行: 選択を行い、状態を更新\n            duplicated.add(choice) // 選択済みの要素値を記録\n            selected[i] = true\n            state.add(choice)\n            // 次の選択へ進む\n            backtrack(state, choices, selected, res)\n            // バックトラック:選択を取り消し、前の状態に戻す\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* 全順列 II */\nfun permutationsII(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
permutations_ii.rb
### バックトラッキング法:全順列 II ###\ndef backtrack(state, choices, selected, res)\n  # 状態の長さが要素数に等しければ、解を記録\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # すべての選択肢を走査\n  duplicated = Set.new\n  choices.each_with_index do |choice, i|\n    # 枝刈り:要素の重複選択を許可せず、同値要素の重複選択も許可しない\n    if !selected[i] && !duplicated.include?(choice)\n      # 試行: 選択を行い、状態を更新\n      duplicated.add(choice)\n      selected[i] = true\n      state << choice\n      # 次の選択へ進む\n      backtrack(state, choices, selected, res)\n      # バックトラック:選択を取り消し、前の状態に戻す\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### 全順列 II ###\ndef permutations_ii(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
コードの可視化

全画面で見る >

要素どうしがすべて互いに異なると仮定すると、\\(n\\) 個の要素には全部で \\(n!\\) 通りの順列(階乗)があります。結果を記録する際には、長さ \\(n\\) のリストをコピーする必要があり、これに \\(O(n)\\) 時間を要します。したがって時間計算量は \\(O(n!n)\\) です。

再帰の最大深さは \\(n\\) であり、\\(O(n)\\) のスタックフレーム空間を使います。selected は \\(O(n)\\) 空間を使用します。同時刻に存在する duplicated は最大で \\(n\\) 個であり、\\(O(n^2)\\) 空間を要します。したがって空間計算量は \\(O(n^2)\\) です。

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3-2","level":3,"title":"3.   2 種類の枝刈りの比較","text":"

selectedduplicated はどちらも枝刈りに用いられますが、目的は異なる点に注意してください。

  • 重複選択の枝刈り:探索全体を通して selected は 1 つだけです。これは現在の状態にどの要素が含まれているかを記録し、ある要素が state に重複して現れるのを防ぎます。
  • 等しい要素の枝刈り:各ラウンドの選択、すなわち各回の backtrack 呼び出しには duplicated が含まれます。これはそのラウンドの走査(for ループ)でどの要素がすでに選ばれたかを記録し、等しい要素が 1 回しか選ばれないことを保証します。

下図は、2 つの枝刈り条件が有効になる範囲を示しています。木の各ノードは 1 つの選択を表し、根ノードから葉ノードまでの経路上の各ノードが 1 つの順列を構成することに注意してください。

図 13-9   2 種類の枝刈り条件の作用範囲

","path":["第 13 章   バックトラッキング","13.2   全順列問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/","level":1,"title":"13.3   部分和問題","text":"","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1331","level":2,"title":"13.3.1   重複しない要素の場合","text":"

Question

正整数配列 nums と目標の正整数 target が与えられたとき、要素の和が target に等しくなるすべての組合せを見つけてください。配列に重複要素はなく、各要素は複数回選択できます。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。

例えば、入力集合 \\(\\{3, 4, 5\\}\\) と目標整数 \\(9\\) に対する解は \\(\\{3, 3, 3\\}, \\{4, 5\\}\\) です。次の 2 点に注意してください。

  • 入力集合内の要素は何度でも繰り返し選択できます。
  • 部分集合では要素の順序を区別しません。例えば \\(\\{4, 5\\}\\) と \\(\\{5, 4\\}\\) は同じ部分集合です。
","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1","level":3,"title":"1.   全順列の解法を参考にする","text":"

全順列問題と同様に、部分集合の生成過程を一連の選択結果として捉え、選択の過程で「要素の和」を逐次更新できます。そして要素の和が target に等しくなった時点で、その部分集合を結果リストに記録します。

ただし全順列問題と異なるのは、**この問題では集合内の要素を無制限に選択できる**点です。そのため、要素がすでに選択されたかどうかを記録する selected ブール配列は不要です。全順列のコードに少し修正を加えると、まず次の解法コードが得られます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i_naive.py
def backtrack(\n    state: list[int],\n    target: int,\n    total: int,\n    choices: list[int],\n    res: list[list[int]],\n):\n    \"\"\"バックトラッキング:部分和 I\"\"\"\n    # 部分集合の和が target に等しければ、解を記録\n    if total == target:\n        res.append(list(state))\n        return\n    # すべての選択肢を走査\n    for i in range(len(choices)):\n        # 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if total + choices[i] > target:\n            continue\n        # 試行:選択を行い、要素と total を更新する\n        state.append(choices[i])\n        # 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res)\n        # バックトラック:選択を取り消し、前の状態に戻す\n        state.pop()\n\ndef subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"部分和 I を解く(重複部分集合を含む)\"\"\"\n    state = []  # 状態(部分集合)\n    total = 0  # 部分和\n    res = []  # 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res)\n    return res\n
subset_sum_i_naive.cpp
/* バックトラッキング:部分和 I */\nvoid backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total == target) {\n        res.push_back(state);\n        return;\n    }\n    // すべての選択肢を走査\n    for (size_t i = 0; i < choices.size(); i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.push_back(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop_back();\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nvector<vector<int>> subsetSumINaive(vector<int> &nums, int target) {\n    vector<int> state;       // 状態(部分集合)\n    int total = 0;           // 部分和\n    vector<vector<int>> res; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.java
/* バックトラッキング:部分和 I */\nvoid backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total == target) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choices.length; i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.add(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.remove(state.size() - 1);\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nList<List<Integer>> subsetSumINaive(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // 状態(部分集合)\n    int total = 0; // 部分和\n    List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.cs
/* バックトラッキング:部分和 I */\nvoid Backtrack(List<int> state, int target, int total, int[] choices, List<List<int>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total == target) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choices.Length; i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.Add(choices[i]);\n        // 次の選択へ進む\n        Backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nList<List<int>> SubsetSumINaive(int[] nums, int target) {\n    List<int> state = []; // 状態(部分集合)\n    int total = 0; // 部分和\n    List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n    Backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.go
/* バックトラッキング:部分和 I */\nfunc backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == total {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // すべての選択肢を走査\n    for i := 0; i < len(*choices); i++ {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if total+(*choices)[i] > target {\n            continue\n        }\n        // 試行:選択を行い、要素と total を更新する\n        *state = append(*state, (*choices)[i])\n        // 次の選択へ進む\n        backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfunc subsetSumINaive(nums []int, target int) [][]int {\n    state := make([]int, 0) // 状態(部分集合)\n    total := 0              // 部分和\n    res := make([][]int, 0) // 結果リスト(部分集合のリスト)\n    backtrackSubsetSumINaive(total, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_i_naive.swift
/* バックトラッキング:部分和 I */\nfunc backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) {\n    // 部分集合の和が target に等しければ、解を記録\n    if total == target {\n        res.append(state)\n        return\n    }\n    // すべての選択肢を走査\n    for i in choices.indices {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if total + choices[i] > target {\n            continue\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.append(choices[i])\n        // 次の選択へ進む\n        backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeLast()\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfunc subsetSumINaive(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // 状態(部分集合)\n    let total = 0 // 部分和\n    var res: [[Int]] = [] // 結果リスト(部分集合のリスト)\n    backtrack(state: &state, target: target, total: total, choices: nums, res: &res)\n    return res\n}\n
subset_sum_i_naive.js
/* バックトラッキング:部分和 I */\nfunction backtrack(state, target, total, choices, res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    for (let i = 0; i < choices.length; i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfunction subsetSumINaive(nums, target) {\n    const state = []; // 状態(部分集合)\n    const total = 0; // 部分和\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.ts
/* バックトラッキング:部分和 I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    total: number,\n    choices: number[],\n    res: number[][]\n): void {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    for (let i = 0; i < choices.length; i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfunction subsetSumINaive(nums: number[], target: number): number[][] {\n    const state = []; // 状態(部分集合)\n    const total = 0; // 部分和\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.dart
/* バックトラッキング:部分和 I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  int total,\n  List<int> choices,\n  List<List<int>> res,\n) {\n  // 部分集合の和が target に等しければ、解を記録\n  if (total == target) {\n    res.add(List.from(state));\n    return;\n  }\n  // すべての選択肢を走査\n  for (int i = 0; i < choices.length; i++) {\n    // 枝刈り:部分和が target を超える場合はその選択をスキップする\n    if (total + choices[i] > target) {\n      continue;\n    }\n    // 試行:選択を行い、要素と total を更新する\n    state.add(choices[i]);\n    // 次の選択へ進む\n    backtrack(state, target, total + choices[i], choices, res);\n    // バックトラック:選択を取り消し、前の状態に戻す\n    state.removeLast();\n  }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nList<List<int>> subsetSumINaive(List<int> nums, int target) {\n  List<int> state = []; // 状態(部分集合)\n  int total = 0; // 要素の合計\n  List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n  backtrack(state, target, total, nums, res);\n  return res;\n}\n
subset_sum_i_naive.rs
/* バックトラッキング:部分和 I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    total: i32,\n    choices: &[i32],\n    res: &mut Vec<Vec<i32>>,\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if total == target {\n        res.push(state.clone());\n        return;\n    }\n    // すべての選択肢を走査\n    for i in 0..choices.len() {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if total + choices[i] > target {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // 状態(部分集合)\n    let total = 0; // 部分和\n    let mut res = Vec::new(); // 結果リスト(部分集合のリスト)\n    backtrack(&mut state, target, total, nums, &mut res);\n    res\n}\n
subset_sum_i_naive.c
/* バックトラッキング:部分和 I */\nvoid backtrack(int target, int total, int *choices, int choicesSize) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total == target) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // すべての選択肢を走査\n    for (int i = 0; i < choicesSize; i++) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state[stateSize++] = choices[i];\n        // 次の選択へ進む\n        backtrack(target, total + choices[i], choices, choicesSize);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        stateSize--;\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nvoid subsetSumINaive(int *nums, int numsSize, int target) {\n    resSize = 0; // 解の個数を 0 に初期化する\n    backtrack(target, 0, nums, numsSize);\n}\n
subset_sum_i_naive.kt
/* バックトラッキング:部分和 I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    total: Int,\n    choices: IntArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (total == target) {\n        res.add(state.toMutableList())\n        return\n    }\n    // すべての選択肢を走査\n    for (i in choices.indices) {\n        // 枝刈り:部分和が target を超える場合はその選択をスキップする\n        if (total + choices[i] > target) {\n            continue\n        }\n        // 試行:選択を行い、要素と total を更新する\n        state.add(choices[i])\n        // 次の選択へ進む\n        backtrack(state, target, total + choices[i], choices, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* 部分和 I を解く(重複部分集合を含む) */\nfun subsetSumINaive(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // 状態(部分集合)\n    val total = 0 // 部分和\n    val res = mutableListOf<MutableList<Int>?>() // 結果リスト(部分集合のリスト)\n    backtrack(state, target, total, nums, res)\n    return res\n}\n
subset_sum_i_naive.rb
### バックトラッキング: 部分和 I ###\ndef backtrack(state, target, total, choices, res)\n  # 部分集合の和が target に等しければ、解を記録\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # すべての選択肢を走査\n  for i in 0...choices.length\n    # 枝刈り:部分和が target を超える場合はその選択をスキップする\n    next if total + choices[i] > target\n    # 試行:選択を行い、要素と total を更新する\n    state << choices[i]\n    # 次の選択へ進む\n    backtrack(state, target, total + choices[i], choices, res)\n    # バックトラック:選択を取り消し、前の状態に戻す\n    state.pop\n  end\nend\n\n### バックトラッキング: 部分和 I ###\ndef backtrack(state, target, total, choices, res)\n  # 部分集合の和が target に等しければ、解を記録\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # すべての選択肢を走査\n  for i in 0...choices.length\n    # 枝刈り:部分和が target を超える場合はその選択をスキップする\n    next if total + choices[i] > target\n    # 試行:選択を行い、要素と total を更新する\n    state << choices[i]\n    # 次の選択へ進む\n    backtrack(state, target, total + choices[i], choices, res)\n    # バックトラック:選択を取り消し、前の状態に戻す\n    state.pop\n  end\nend\n\n# ## 部分和 I を解く(重複する部分集合を含む)###\ndef subset_sum_i_naive(nums, target)\n  state = [] # 状態(部分集合)\n  total = 0 # 部分和\n  res = [] # 結果リスト(部分集合のリスト)\n  backtrack(state, target, total, nums, res)\n  res\nend\n
コードの可視化

全画面で見る >

上のコードに配列 \\([3, 4, 5]\\) と目標値 \\(9\\) を入力すると、出力は \\([3, 3, 3], [4, 5], [5, 4]\\) となります。和が \\(9\\) となる部分集合はすべて見つかっていますが、重複する部分集合 \\([4, 5]\\) と \\([5, 4]\\) が含まれています。

これは、探索過程では選択順を区別する一方で、部分集合では選択順を区別しないためです。次の図のように、先に \\(4\\) を選んでから \\(5\\) を選ぶ場合と、先に \\(5\\) を選んでから \\(4\\) を選ぶ場合は別の分岐ですが、対応する部分集合は同じです。

図 13-10   部分集合探索と境界超過の枝刈り

重複する部分集合を取り除くために、**直接的な方法として結果リストの重複を除去する**ことが考えられます。しかし、この方法は効率が低く、その理由は次の 2 点です。

  • 配列要素が多い場合、特に target が大きい場合には、探索過程で大量の重複部分集合が生成されます。
  • 部分集合(配列)同士の違いを比較するのは非常に時間がかかり、まず配列をソートし、その後に各要素を比較する必要があります。
","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2","level":3,"title":"2.   重複部分集合の枝刈り","text":"

**探索過程で枝刈りを行って重複を除去する**ことを考えます。次の図を観察すると、重複部分集合は配列要素を異なる順序で選択したときに生じます。例えば次のような状況です。

  1. 1 回目と 2 回目でそれぞれ \\(3\\) と \\(4\\) を選ぶと、これら 2 要素を含むすべての部分集合、すなわち \\([3, 4, \\dots]\\) が生成されます。
  2. その後、1 回目で \\(4\\) を選んだ場合、**2 回目では \\(3\\) をスキップすべき**です。というのも、この選択で生成される部分集合 \\([4, 3, \\dots]\\) は、手順 1. で生成された部分集合と完全に重複するからです。

探索過程では、各階層の選択は左から右へ順に試されるため、右側にある分岐ほど多く枝刈りされます。

  1. 最初の 2 回で \\(3\\) と \\(5\\) を選ぶと、部分集合 \\([3, 5, \\dots]\\) が生成されます。
  2. 最初の 2 回で \\(4\\) と \\(5\\) を選ぶと、部分集合 \\([4, 5, \\dots]\\) が生成されます。
  3. もし 1 回目で \\(5\\) を選ぶなら、**2 回目では \\(3\\) と \\(4\\) をスキップすべき**です。なぜなら、部分集合 \\([5, 3, \\dots]\\) と \\([5, 4, \\dots]\\) は、手順 1. と手順 2. で述べた部分集合と完全に重複するからです。

図 13-11   異なる選択順によって生じる重複部分集合

まとめると、入力配列 \\([x_1, x_2, \\dots, x_n]\\) が与えられ、探索過程における選択列を \\([x_{i_1}, x_{i_2}, \\dots, x_{i_m}]\\) とすると、この選択列は \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\) を満たす必要があります。この条件を満たさない選択列は重複を生むため、枝刈りすべきです。

","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#3","level":3,"title":"3.   コード実装","text":"

この枝刈りを実現するために、走査の開始位置を示す変数 start を初期化します。**選択 \\(x_{i}\\) を行った後、次のラウンドはインデックス \\(i\\) から走査を開始する**ように設定します。これにより、選択列が \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\) を満たし、部分集合の一意性が保証されます。

これに加えて、コードには次の 2 つの最適化も施しています。

  • 探索を始める前に、まず配列 nums をソートします。すべての選択肢を走査するとき、**部分集合の和が target を超えたら直ちにループを終了**します。後続の要素はさらに大きいため、その和も必ず target を超えるからです。
  • 要素和を保持する変数 total は省略し、**target から減算することで要素和を管理**します。target が \\(0\\) になったときに解を記録します。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i.py
def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"バックトラッキング:部分和 I\"\"\"\n    # 部分集合の和が target に等しければ、解を記録\n    if target == 0:\n        res.append(list(state))\n        return\n    # すべての選択肢を走査\n    # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for i in range(start, len(choices)):\n        # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0:\n            break\n        # 試す:選択を行い、target と start を更新\n        state.append(choices[i])\n        # 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res)\n        # バックトラック:選択を取り消し、前の状態に戻す\n        state.pop()\n\ndef subset_sum_i(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"部分和 I を解く\"\"\"\n    state = []  # 状態(部分集合)\n    nums.sort()  # nums をソート\n    start = 0  # 開始点を走査\n    res = []  # 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res)\n    return res\n
subset_sum_i.cpp
/* バックトラッキング:部分和 I */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (int i = start; i < choices.size(); i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push_back(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop_back();\n    }\n}\n\n/* 部分和 I を解く */\nvector<vector<int>> subsetSumI(vector<int> &nums, int target) {\n    vector<int> state;              // 状態(部分集合)\n    sort(nums.begin(), nums.end()); // nums をソート\n    int start = 0;                  // 開始点を走査\n    vector<vector<int>> res;        // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.java
/* バックトラッキング:部分和 I */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (int i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.add(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.remove(state.size() - 1);\n    }\n}\n\n/* 部分和 I を解く */\nList<List<Integer>> subsetSumI(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // 状態(部分集合)\n    Arrays.sort(nums); // nums をソート\n    int start = 0; // 開始点を走査\n    List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.cs
/* バックトラッキング:部分和 I */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (int i = start; i < choices.Length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.Add(choices[i]);\n        // 次の選択へ進む\n        Backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* 部分和 I を解く */\nList<List<int>> SubsetSumI(int[] nums, int target) {\n    List<int> state = []; // 状態(部分集合)\n    Array.Sort(nums); // nums をソート\n    int start = 0; // 開始点を走査\n    List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.go
/* バックトラッキング:部分和 I */\nfunc backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for i := start; i < len(*choices); i++ {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // 試す:選択を行い、target と start を更新\n        *state = append(*state, (*choices)[i])\n        // 次の選択へ進む\n        backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* 部分和 I を解く */\nfunc subsetSumI(nums []int, target int) [][]int {\n    state := make([]int, 0) // 状態(部分集合)\n    sort.Ints(nums)         // nums をソート\n    start := 0              // 開始点を走査\n    res := make([][]int, 0) // 結果リスト(部分集合のリスト)\n    backtrackSubsetSumI(start, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_i.swift
/* バックトラッキング:部分和 I */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for i in choices.indices.dropFirst(start) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0 {\n            break\n        }\n        // 試す:選択を行い、target と start を更新\n        state.append(choices[i])\n        // 次の選択へ進む\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeLast()\n    }\n}\n\n/* 部分和 I を解く */\nfunc subsetSumI(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // 状態(部分集合)\n    let nums = nums.sorted() // nums をソート\n    let start = 0 // 開始点を走査\n    var res: [[Int]] = [] // 結果リスト(部分集合のリスト)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
subset_sum_i.js
/* バックトラッキング:部分和 I */\nfunction backtrack(state, target, choices, start, res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (let i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く */\nfunction subsetSumI(nums, target) {\n    const state = []; // 状態(部分集合)\n    nums.sort((a, b) => a - b); // nums をソート\n    const start = 0; // 開始点を走査\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.ts
/* バックトラッキング:部分和 I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (let i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く */\nfunction subsetSumI(nums: number[], target: number): number[][] {\n    const state = []; // 状態(部分集合)\n    nums.sort((a, b) => a - b); // nums をソート\n    const start = 0; // 開始点を走査\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.dart
/* バックトラッキング:部分和 I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // 部分集合の和が target に等しければ、解を記録\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // すべての選択肢を走査\n  // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n  for (int i = start; i < choices.length; i++) {\n    // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n    // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // 試す:選択を行い、target と start を更新\n    state.add(choices[i]);\n    // 次の選択へ進む\n    backtrack(state, target - choices[i], choices, i, res);\n    // バックトラック:選択を取り消し、前の状態に戻す\n    state.removeLast();\n  }\n}\n\n/* 部分和 I を解く */\nList<List<int>> subsetSumI(List<int> nums, int target) {\n  List<int> state = []; // 状態(部分集合)\n  nums.sort(); // nums をソート\n  int start = 0; // 開始点を走査\n  List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
subset_sum_i.rs
/* バックトラッキング:部分和 I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for i in start..choices.len() {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0 {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 I を解く */\nfn subset_sum_i(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // 状態(部分集合)\n    nums.sort(); // nums をソート\n    let start = 0; // 開始点を走査\n    let mut res = Vec::new(); // 結果リスト(部分集合のリスト)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
subset_sum_i.c
/* バックトラッキング:部分和 I */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        for (int i = 0; i < stateSize; ++i) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (int i = start; i < choicesSize; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 試す:選択を行い、target と start を更新\n        state[stateSize] = choices[i];\n        stateSize++;\n        // 次の選択へ進む\n        backtrack(target - choices[i], choices, choicesSize, i);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        stateSize--;\n    }\n}\n\n/* 部分和 I を解く */\nvoid subsetSumI(int *nums, int numsSize, int target) {\n    qsort(nums, numsSize, sizeof(int), cmp); // nums をソート\n    int start = 0;                           // 開始点を走査\n    backtrack(target, nums, numsSize, start);\n}\n
subset_sum_i.kt
/* バックトラッキング:部分和 I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    for (i in start..<choices.size) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break\n        }\n        // 試す:選択を行い、target と start を更新\n        state.add(choices[i])\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* 部分和 I を解く */\nfun subsetSumI(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // 状態(部分集合)\n    nums.sort() // nums をソート\n    val start = 0 // 開始点を走査\n    val res = mutableListOf<MutableList<Int>?>() // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
subset_sum_i.rb
### バックトラッキング: 部分和 I ###\ndef backtrack(state, target, choices, start, res)\n  # 部分集合の和が target に等しければ、解を記録\n  if target.zero?\n    res << state.dup\n    return\n  end\n  # すべての選択肢を走査\n  # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n  for i in start...choices.length\n    # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n    # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n    break if target - choices[i] < 0\n    # 試す:選択を行い、target と start を更新\n    state << choices[i]\n    # 次の選択へ進む\n    backtrack(state, target - choices[i], choices, i, res)\n    # バックトラック:選択を取り消し、前の状態に戻す\n    state.pop\n  end\nend\n\n### 部分和 I を解く ###\ndef subset_sum_i(nums, target)\n  state = [] # 状態(部分集合)\n  nums.sort! # nums をソート\n  start = 0 # 開始点を走査\n  res = [] # 結果リスト(部分集合のリスト)\n  backtrack(state, target, nums, start, res)\n  res\nend\n
コードの可視化

全画面で見る >

次の図は、配列 \\([3, 4, 5]\\) と目標値 \\(9\\) を上のコードに入力したときの、全体のバックトラッキング過程を示しています。

図 13-12   部分和 I のバックトラッキング過程

","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1332","level":2,"title":"13.3.2   重複要素を考慮する場合","text":"

Question

正整数配列 nums と目標の正整数 target が与えられたとき、要素の和が target に等しくなるすべての組合せを見つけてください。与えられた配列には重複要素が含まれる可能性があり、各要素は 1 回しか選択できません。これらの組合せをリスト形式で返してください。リストに重複する組合せを含めてはなりません。

前問と比べると、この問題の入力配列には重複要素が含まれる可能性があります。そのため、新たな問題が生じます。例えば、配列 \\([4, \\hat{4}, 5]\\) と目標値 \\(9\\) が与えられると、既存コードの出力は \\([4, 5], [\\hat{4}, 5]\\) となり、重複部分集合が現れます。

この重複が生じる原因は、同じ値の要素があるラウンドで複数回選ばれてしまうことにあります。次の図では、1 回目には 3 つの選択肢があり、そのうち 2 つはどちらも \\(4\\) です。これにより 2 本の重複した探索分岐が生じ、重複部分集合が出力されます。同様に、2 回目の 2 つの \\(4\\) も重複部分集合を生みます。

図 13-13   等しい要素によって生じる重複部分集合

","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1_1","level":3,"title":"1.   等しい要素の枝刈り","text":"

この問題を解決するには、各ラウンドで等しい要素が 1 回しか選ばれないように制限する必要があります。実装方法は巧妙です。配列はすでにソートされているため、等しい要素は必ず隣り合っています。したがって、あるラウンドの選択で現在の要素が左隣の要素と等しいなら、それはすでに選ばれたことを意味するので、その要素を直接スキップします。

同時に、**この問題では各配列要素を 1 回しか選択できない**という制約もあります。幸い、この制約も変数 start を使って満たせます。すなわち、選択 \\(x_{i}\\) を行った後、次のラウンドはインデックス \\(i + 1\\) から後ろへ走査するよう設定します。これにより、重複部分集合を除去できるだけでなく、同じ要素を繰り返し選ぶことも防げます。

","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2_1","level":3,"title":"2.   コード実装","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_ii.py
def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"バックトラッキング:部分和 II\"\"\"\n    # 部分集合の和が target に等しければ、解を記録\n    if target == 0:\n        res.append(list(state))\n        return\n    # すべての選択肢を走査\n    # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    # 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for i in range(start, len(choices)):\n        # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0:\n            break\n        # 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if i > start and choices[i] == choices[i - 1]:\n            continue\n        # 試す:選択を行い、target と start を更新\n        state.append(choices[i])\n        # 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        # バックトラック:選択を取り消し、前の状態に戻す\n        state.pop()\n\ndef subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"部分和 II を解く\"\"\"\n    state = []  # 状態(部分集合)\n    nums.sort()  # nums をソート\n    start = 0  # 開始点を走査\n    res = []  # 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res)\n    return res\n
subset_sum_ii.cpp
/* バックトラッキング:部分和 II */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (int i = start; i < choices.size(); i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push_back(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop_back();\n    }\n}\n\n/* 部分和 II を解く */\nvector<vector<int>> subsetSumII(vector<int> &nums, int target) {\n    vector<int> state;              // 状態(部分集合)\n    sort(nums.begin(), nums.end()); // nums をソート\n    int start = 0;                  // 開始点を走査\n    vector<vector<int>> res;        // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.java
/* バックトラッキング:部分和 II */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (int i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.add(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.remove(state.size() - 1);\n    }\n}\n\n/* 部分和 II を解く */\nList<List<Integer>> subsetSumII(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // 状態(部分集合)\n    Arrays.sort(nums); // nums をソート\n    int start = 0; // 開始点を走査\n    List<List<Integer>> res = new ArrayList<>(); // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.cs
/* バックトラッキング:部分和 II */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (int i = start; i < choices.Length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.Add(choices[i]);\n        // 次の選択へ進む\n        Backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* 部分和 II を解く */\nList<List<int>> SubsetSumII(int[] nums, int target) {\n    List<int> state = []; // 状態(部分集合)\n    Array.Sort(nums); // nums をソート\n    int start = 0; // 開始点を走査\n    List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.go
/* バックトラッキング:部分和 II */\nfunc backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for i := start; i < len(*choices); i++ {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if i > start && (*choices)[i] == (*choices)[i-1] {\n            continue\n        }\n        // 試す:選択を行い、target と start を更新\n        *state = append(*state, (*choices)[i])\n        // 次の選択へ進む\n        backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* 部分和 II を解く */\nfunc subsetSumII(nums []int, target int) [][]int {\n    state := make([]int, 0) // 状態(部分集合)\n    sort.Ints(nums)         // nums をソート\n    start := 0              // 開始点を走査\n    res := make([][]int, 0) // 結果リスト(部分集合のリスト)\n    backtrackSubsetSumII(start, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_ii.swift
/* バックトラッキング:部分和 II */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for i in choices.indices.dropFirst(start) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0 {\n            break\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if i > start, choices[i] == choices[i - 1] {\n            continue\n        }\n        // 試す:選択を行い、target と start を更新\n        state.append(choices[i])\n        // 次の選択へ進む\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeLast()\n    }\n}\n\n/* 部分和 II を解く */\nfunc subsetSumII(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // 状態(部分集合)\n    let nums = nums.sorted() // nums をソート\n    let start = 0 // 開始点を走査\n    var res: [[Int]] = [] // 結果リスト(部分集合のリスト)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
subset_sum_ii.js
/* バックトラッキング:部分和 II */\nfunction backtrack(state, target, choices, start, res) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (let i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 II を解く */\nfunction subsetSumII(nums, target) {\n    const state = []; // 状態(部分集合)\n    nums.sort((a, b) => a - b); // nums をソート\n    const start = 0; // 開始点を走査\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.ts
/* バックトラッキング:部分和 II */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (let i = start; i < choices.length; i++) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 II を解く */\nfunction subsetSumII(nums: number[], target: number): number[][] {\n    const state = []; // 状態(部分集合)\n    nums.sort((a, b) => a - b); // nums をソート\n    const start = 0; // 開始点を走査\n    const res = []; // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.dart
/* バックトラッキング:部分和 II */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // 部分集合の和が target に等しければ、解を記録\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // すべての選択肢を走査\n  // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n  // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n  for (int i = start; i < choices.length; i++) {\n    // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n    // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n    if (i > start && choices[i] == choices[i - 1]) {\n      continue;\n    }\n    // 試す:選択を行い、target と start を更新\n    state.add(choices[i]);\n    // 次の選択へ進む\n    backtrack(state, target - choices[i], choices, i + 1, res);\n    // バックトラック:選択を取り消し、前の状態に戻す\n    state.removeLast();\n  }\n}\n\n/* 部分和 II を解く */\nList<List<int>> subsetSumII(List<int> nums, int target) {\n  List<int> state = []; // 状態(部分集合)\n  nums.sort(); // nums をソート\n  int start = 0; // 開始点を走査\n  List<List<int>> res = []; // 結果リスト(部分集合のリスト)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
subset_sum_ii.rs
/* バックトラッキング:部分和 II */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for i in start..choices.len() {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if target - choices[i] < 0 {\n            break;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if i > start && choices[i] == choices[i - 1] {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state.push(choices[i]);\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.pop();\n    }\n}\n\n/* 部分和 II を解く */\nfn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // 状態(部分集合)\n    nums.sort(); // nums をソート\n    let start = 0; // 開始点を走査\n    let mut res = Vec::new(); // 結果リスト(部分集合のリスト)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
subset_sum_ii.c
/* バックトラッキング:部分和 II */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (int i = start; i < choicesSize; i++) {\n        // 枝刈り 1: 部分集合の和が target を超えたら、そのままスキップする\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 試す:選択を行い、target と start を更新\n        state[stateSize] = choices[i];\n        stateSize++;\n        // 次の選択へ進む\n        backtrack(target - choices[i], choices, choicesSize, i + 1);\n        // バックトラック:選択を取り消し、前の状態に戻す\n        stateSize--;\n    }\n}\n\n/* 部分和 II を解く */\nvoid subsetSumII(int *nums, int numsSize, int target) {\n    // nums をソート\n    qsort(nums, numsSize, sizeof(int), cmp);\n    // バックトラッキングを開始\n    backtrack(target, nums, numsSize, 0);\n}\n
subset_sum_ii.kt
/* バックトラッキング:部分和 II */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 部分集合の和が target に等しければ、解を記録\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // すべての選択肢を走査\n    // 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n    // 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n    for (i in start..<choices.size) {\n        // 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n        // 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n        if (target - choices[i] < 0) {\n            break\n        }\n        // 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue\n        }\n        // 試す:選択を行い、target と start を更新\n        state.add(choices[i])\n        // 次の選択へ進む\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        // バックトラック:選択を取り消し、前の状態に戻す\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* 部分和 II を解く */\nfun subsetSumII(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // 状態(部分集合)\n    nums.sort() // nums をソート\n    val start = 0 // 開始点を走査\n    val res = mutableListOf<MutableList<Int>?>() // 結果リスト(部分集合のリスト)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
subset_sum_ii.rb
### バックトラッキング法:部分和 II ###\ndef backtrack(state, target, choices, start, res)\n  # 部分集合の和が target に等しければ、解を記録\n  if target.zero?\n    res << state.dup\n    return\n  end\n\n  # すべての選択肢を走査\n  # 枝刈り 2: start から走査し、重複する部分集合の生成を避ける\n  # 枝刈り 3: start から走査し、同じ要素の重複選択を避ける\n  for i in start...choices.length\n    # 枝刈り1:部分集合の和が target を超えたら、直ちにループを終了する\n    # 配列はソート済みで後続要素のほうが大きく、部分集合の和は必ず target を超えるため\n    break if target - choices[i] < 0\n    # 枝刈り4:この要素が左隣の要素と等しければ、その探索分岐は重複しているためスキップする\n    next if i > start && choices[i] == choices[i - 1]\n    # 試す:選択を行い、target と start を更新\n    state << choices[i]\n    # 次の選択へ進む\n    backtrack(state, target - choices[i], choices, i + 1, res)\n    # バックトラック:選択を取り消し、前の状態に戻す\n    state.pop\n  end\nend\n\n### 部分和 II を解く ###\ndef subset_sum_ii(nums, target)\n  state = [] # 状態(部分集合)\n  nums.sort! # nums をソート\n  start = 0 # 開始点を走査\n  res = [] # 結果リスト(部分集合のリスト)\n  backtrack(state, target, nums, start, res)\n  res\nend\n
コードの可視化

全画面で見る >

次の図は、配列 \\([4, 4, 5]\\) と目標値 \\(9\\) に対するバックトラッキング過程を示しており、全部で 4 種類の枝刈り操作が含まれています。図とコードコメントを対応させながら、探索全体の流れと、各枝刈り操作がどのように機能するかを理解してください。

図 13-14   部分和 II のバックトラッキング過程

","path":["第 13 章   バックトラッキング","13.3   部分和問題"],"tags":[]},{"location":"chapter_backtracking/summary/","level":1,"title":"13.5   まとめ","text":"","path":["第 13 章   バックトラッキング","13.5   まとめ"],"tags":[]},{"location":"chapter_backtracking/summary/#1","level":3,"title":"1.   重要なポイントの振り返り","text":"
  • バックトラッキングアルゴリズムの本質は全探索法であり、解空間を深さ優先で走査することで条件を満たす解を探索します。探索の過程で条件を満たす解に出会ったら記録し、すべての解を見つけるか探索が完了するまで続けます。
  • バックトラッキングアルゴリズムの探索過程は、試行と戻るという 2 つの部分から成ります。深さ優先探索によってさまざまな選択を試し、制約条件を満たさない状況に遭遇した場合は直前の選択を取り消して前の状態に戻り、ほかの選択を引き続き試します。試行と戻るは互いに逆方向の操作です。
  • バックトラッキング問題には通常複数の制約条件が含まれており、それらを枝刈りに利用できます。枝刈りによって不要な探索分岐を早期に打ち切り、探索効率を大幅に高められます。
  • バックトラッキングアルゴリズムは主に探索問題と制約充足問題の解決に用いられます。組合せ最適化問題もバックトラッキングで解けますが、より高効率またはより適した解法が存在することが少なくありません。
  • 全順列問題の目的は、与えられた集合要素のすべての可能な並べ方を探索することです。各要素が選択済みかどうかを配列で記録し、同じ要素を重複して選ぶ探索分岐を刈り取ることで、各要素が 1 度だけ選ばれるようにします。
  • 全順列問題では、集合内に重複要素があると最終結果にも重複した順列が現れます。各ラウンドで等しい要素は 1 回しか選べないように制約する必要があり、通常はハッシュ集合を用いて実現します。
  • 部分和問題の目標は、与えられた集合の中から和が目標値となるすべての部分集合を見つけることです。集合では要素順序を区別しませんが、探索過程では順序違いの結果も出力されるため、重複部分集合が生じます。そこで、バックトラッキング前にデータをソートし、各ラウンドの走査開始位置を示す変数を設定することで、重複部分集合を生成する探索分岐を枝刈りします。
  • 部分和問題では、配列中の等しい要素が重複集合を生みます。配列がソート済みであるという前提を利用し、隣接要素が等しいかどうかを判定して枝刈りすることで、等しい要素が各ラウンドで 1 回しか選ばれないようにします。
  • \\(n\\) クイーン問題の目的は、\\(n \\times n\\) の盤面に \\(n\\) 個のクイーンを配置する方法を見つけることであり、どの 2 つのクイーンも互いに攻撃できないことが条件です。この問題の制約には行制約、列制約、主対角線制約、副対角線制約があります。行制約を満たすため、行ごとに配置する戦略を採用し、各行に 1 個のクイーンを置くことを保証します。
  • 列制約と対角線制約の扱い方は似ています。列制約については、各列にクイーンが存在するかどうかを配列で記録し、選択したマスが有効かどうかを判定します。対角線制約については、主対角線と副対角線それぞれにクイーンが存在するかを 2 つの配列で記録します。難点は、同じ主対角線または副対角線上にあるマスが満たす行列インデックスの規則を見つけることにあります。
","path":["第 13 章   バックトラッキング","13.5   まとめ"],"tags":[]},{"location":"chapter_backtracking/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:バックトラッキングと再帰の関係はどのように理解すればよいですか?

全体として見ると、バックトラッキングは「アルゴリズム戦略」の一種であり、再帰はむしろ「道具」に近いものです。

  • バックトラッキングアルゴリズムは通常、再帰に基づいて実装されます。ただし、バックトラッキングは再帰の応用場面の 1 つであり、探索問題における再帰の応用です。
  • 再帰の構造は「部分問題への分解」という問題解決パラダイムを表しており、分割統治、バックトラッキング、動的計画法(メモ化再帰)などの問題によく用いられます。
","path":["第 13 章   バックトラッキング","13.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"第 2 章   計算量解析","text":"

Abstract

計算量解析は、広大なアルゴリズム宇宙における時空の案内人のようなものです。

それは、時間と空間という二つの次元で私たちをより深く探求へ導き、より洗練された解決策を見つけ出します。

","path":["第 2 章   計算量解析"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"章の内容","text":"
  • 2.1   アルゴリズム効率の評価
  • 2.2   反復と再帰
  • 2.3   時間計算量
  • 2.4   空間計算量
  • 2.5   まとめ
","path":["第 2 章   計算量解析"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   反復と再帰","text":"

アルゴリズムでは、ある処理を繰り返し実行することがよくあり、これは複雑度解析と密接に関係しています。そのため、時間計算量と空間計算量を紹介する前に、まずプログラム内で反復実行を実現する方法、つまり 2 つの基本的な制御構造である反復と再帰について見ていきます。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#221","level":2,"title":"2.2.1   反復","text":"

反復(iteration)は、ある処理を繰り返し実行するための制御構造です。反復では、プログラムは一定の条件を満たす間、あるコード片を繰り返し実行し、その条件を満たさなくなるまで続けます。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-for","level":3,"title":"1.   for ループ","text":"

for ループは最も一般的な反復形式の 1 つで、反復回数があらかじめ分かっている場合に適しています。

次の関数は for ループを用いて \\(1 + 2 + \\dots + n\\) の総和を計算しており、その結果は変数 res に記録されます。なお、Python の range(a, b) に対応する区間は「左閉右開」であり、走査範囲は \\(a, a + 1, \\dots, b-1\\) です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def for_loop(n: int) -> int:\n    \"\"\"for ループ\"\"\"\n    res = 0\n    # 1, 2, ..., n-1, n を順に加算する\n    for i in range(1, n + 1):\n        res += i\n    return res\n
iteration.cpp
/* for ループ */\nint forLoop(int n) {\n    int res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (int i = 1; i <= n; ++i) {\n        res += i;\n    }\n    return res;\n}\n
iteration.java
/* for ループ */\nint forLoop(int n) {\n    int res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.cs
/* for ループ */\nint ForLoop(int n) {\n    int res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.go
/* for ループ */\nfunc forLoop(n int) int {\n    res := 0\n    // 1, 2, ..., n-1, n を順に加算する\n    for i := 1; i <= n; i++ {\n        res += i\n    }\n    return res\n}\n
iteration.swift
/* for ループ */\nfunc forLoop(n: Int) -> Int {\n    var res = 0\n    // 1, 2, ..., n-1, n を順に加算する\n    for i in 1 ... n {\n        res += i\n    }\n    return res\n}\n
iteration.js
/* for ループ */\nfunction forLoop(n) {\n    let res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.ts
/* for ループ */\nfunction forLoop(n: number): number {\n    let res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.dart
/* for ループ */\nint forLoop(int n) {\n  int res = 0;\n  // 1, 2, ..., n-1, n を順に加算する\n  for (int i = 1; i <= n; i++) {\n    res += i;\n  }\n  return res;\n}\n
iteration.rs
/* for ループ */\nfn for_loop(n: i32) -> i32 {\n    let mut res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for i in 1..=n {\n        res += i;\n    }\n    res\n}\n
iteration.c
/* for ループ */\nint forLoop(int n) {\n    int res = 0;\n    // 1, 2, ..., n-1, n を順に加算する\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.kt
/* for ループ */\nfun forLoop(n: Int): Int {\n    var res = 0\n    // 1, 2, ..., n-1, n を順に加算する\n    for (i in 1..n) {\n        res += i\n    }\n    return res\n}\n
iteration.rb
### for ループ ###\ndef for_loop(n)\n  res = 0\n\n  # 1, 2, ..., n-1, n を順に加算する\n  for i in 1..n\n    res += i\n  end\n\n  res\nend\n
コードの可視化

全画面で見る >

次の図は、この総和関数のフローチャートです。

図 2-1   総和関数のフローチャート

この総和関数の操作回数は入力データサイズ \\(n\\) に比例し、言い換えれば「線形関係」にあります。実際、時間計算量が記述するのはこの「線形関係」そのものです。関連内容は次節で詳しく説明します。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-while","level":3,"title":"2.   while ループ","text":"

for ループと同様に、while ループも反復を実現する方法の 1 つです。while ループでは、各反復のたびにまず条件を確認し、条件が真であれば実行を続け、そうでなければループを終了します。

次に、while ループを使って \\(1 + 2 + \\dots + n\\) の総和を求めてみましょう。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def while_loop(n: int) -> int:\n    \"\"\"while ループ\"\"\"\n    res = 0\n    i = 1  # 条件変数を初期化する\n    # 1, 2, ..., n-1, n を順に加算する\n    while i <= n:\n        res += i\n        i += 1  # 条件変数を更新する\n    return res\n
iteration.cpp
/* while ループ */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i++; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.java
/* while ループ */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i++; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.cs
/* while ループ */\nint WhileLoop(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i += 1; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.go
/* while ループ */\nfunc whileLoop(n int) int {\n    res := 0\n    // 条件変数を初期化する\n    i := 1\n    // 1, 2, ..., n-1, n を順に加算する\n    for i <= n {\n        res += i\n        // 条件変数を更新する\n        i++\n    }\n    return res\n}\n
iteration.swift
/* while ループ */\nfunc whileLoop(n: Int) -> Int {\n    var res = 0\n    var i = 1 // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while i <= n {\n        res += i\n        i += 1 // 条件変数を更新する\n    }\n    return res\n}\n
iteration.js
/* while ループ */\nfunction whileLoop(n) {\n    let res = 0;\n    let i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i++; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.ts
/* while ループ */\nfunction whileLoop(n: number): number {\n    let res = 0;\n    let i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i++; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.dart
/* while ループ */\nint whileLoop(int n) {\n  int res = 0;\n  int i = 1; // 条件変数を初期化する\n  // 1, 2, ..., n-1, n を順に加算する\n  while (i <= n) {\n    res += i;\n    i++; // 条件変数を更新する\n  }\n  return res;\n}\n
iteration.rs
/* while ループ */\nfn while_loop(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // 条件変数を初期化する\n\n    // 1, 2, ..., n-1, n を順に加算する\n    while i <= n {\n        res += i;\n        i += 1; // 条件変数を更新する\n    }\n    res\n}\n
iteration.c
/* while ループ */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i;\n        i++; // 条件変数を更新する\n    }\n    return res;\n}\n
iteration.kt
/* while ループ */\nfun whileLoop(n: Int): Int {\n    var res = 0\n    var i = 1 // 条件変数を初期化する\n    // 1, 2, ..., n-1, n を順に加算する\n    while (i <= n) {\n        res += i\n        i++ // 条件変数を更新する\n    }\n    return res\n}\n
iteration.rb
### while ループ ###\ndef while_loop(n)\n  res = 0\n  i = 1 # 条件変数を初期化する\n\n  # 1, 2, ..., n-1, n を順に加算する\n  while i <= n\n    res += i\n    i += 1 # 条件変数を更新する\n  end\n\n  res\nend\n
コードの可視化

全画面で見る >

**while ループは for ループより自由度が高い**です。while ループでは、条件変数の初期化や更新手順を柔軟に設計できます。

たとえば次のコードでは、条件変数 \\(i\\) が各反復で 2 回更新されており、このようなケースは for ループではあまり扱いやすくありません。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def while_loop_ii(n: int) -> int:\n    \"\"\"while ループ(2回更新)\"\"\"\n    res = 0\n    i = 1  # 条件変数を初期化する\n    # 1, 4, 10, ... を順に加算する\n    while i <= n:\n        res += i\n        # 条件変数を更新する\n        i += 1\n        i *= 2\n    return res\n
iteration.cpp
/* while ループ(2回更新) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.java
/* while ループ(2回更新) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.cs
/* while ループ(2回更新) */\nint WhileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i += 1; \n        i *= 2;\n    }\n    return res;\n}\n
iteration.go
/* while ループ(2回更新) */\nfunc whileLoopII(n int) int {\n    res := 0\n    // 条件変数を初期化する\n    i := 1\n    // 1, 4, 10, ... を順に加算する\n    for i <= n {\n        res += i\n        // 条件変数を更新する\n        i++\n        i *= 2\n    }\n    return res\n}\n
iteration.swift
/* while ループ(2回更新) */\nfunc whileLoopII(n: Int) -> Int {\n    var res = 0\n    var i = 1 // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while i <= n {\n        res += i\n        // 条件変数を更新する\n        i += 1\n        i *= 2\n    }\n    return res\n}\n
iteration.js
/* while ループ(2回更新) */\nfunction whileLoopII(n) {\n    let res = 0;\n    let i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.ts
/* while ループ(2回更新) */\nfunction whileLoopII(n: number): number {\n    let res = 0;\n    let i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.dart
/* while ループ(2回更新) */\nint whileLoopII(int n) {\n  int res = 0;\n  int i = 1; // 条件変数を初期化する\n  // 1, 4, 10, ... を順に加算する\n  while (i <= n) {\n    res += i;\n    // 条件変数を更新する\n    i++;\n    i *= 2;\n  }\n  return res;\n}\n
iteration.rs
/* while ループ(2回更新) */\nfn while_loop_ii(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // 条件変数を初期化する\n\n    // 1, 4, 10, ... を順に加算する\n    while i <= n {\n        res += i;\n        // 条件変数を更新する\n        i += 1;\n        i *= 2;\n    }\n    res\n}\n
iteration.c
/* while ループ(2回更新) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i;\n        // 条件変数を更新する\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.kt
/* while ループ(2回更新) */\nfun whileLoopII(n: Int): Int {\n    var res = 0\n    var i = 1 // 条件変数を初期化する\n    // 1, 4, 10, ... を順に加算する\n    while (i <= n) {\n        res += i\n        // 条件変数を更新する\n        i++\n        i *= 2\n    }\n    return res\n}\n
iteration.rb
### while ループ ###\ndef while_loop(n)\n  res = 0\n  i = 1 # 条件変数を初期化する\n\n  # 1, 2, ..., n-1, n を順に加算する\n  while i <= n\n    res += i\n    i += 1 # 条件変数を更新する\n  end\n\n  res\nend\n\n# ## while ループ(2 回更新)###\ndef while_loop_ii(n)\n  res = 0\n  i = 1 # 条件変数を初期化する\n\n  # 1, 4, 10, ... を順に加算する\n  while i <= n\n    res += i\n    # 条件変数を更新する\n    i += 1\n    i *= 2\n  end\n\n  res\nend\n
コードの可視化

全画面で見る >

総じて、**for ループのコードはより簡潔で、while ループはより柔軟**です。どちらも反復構造を実現できますが、どちらを使うかは問題ごとの要件に応じて決めるべきです。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3","level":3,"title":"3.   ネストしたループ","text":"

1 つのループ構造の中に別のループ構造を入れ子にできます。以下では for ループを例にします。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def nested_for_loop(n: int) -> str:\n    \"\"\"二重 for ループ\"\"\"\n    res = \"\"\n    # i = 1, 2, ..., n-1, n とループする\n    for i in range(1, n + 1):\n        # j = 1, 2, ..., n-1, n とループする\n        for j in range(1, n + 1):\n            res += f\"({i}, {j}), \"\n    return res\n
iteration.cpp
/* 二重 for ループ */\nstring nestedForLoop(int n) {\n    ostringstream res;\n    // i = 1, 2, ..., n-1, n とループする\n    for (int i = 1; i <= n; ++i) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (int j = 1; j <= n; ++j) {\n            res << \"(\" << i << \", \" << j << \"), \";\n        }\n    }\n    return res.str();\n}\n
iteration.java
/* 二重 for ループ */\nString nestedForLoop(int n) {\n    StringBuilder res = new StringBuilder();\n    // i = 1, 2, ..., n-1, n とループする\n    for (int i = 1; i <= n; i++) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (int j = 1; j <= n; j++) {\n            res.append(\"(\" + i + \", \" + j + \"), \");\n        }\n    }\n    return res.toString();\n}\n
iteration.cs
/* 二重 for ループ */\nstring NestedForLoop(int n) {\n    StringBuilder res = new();\n    // i = 1, 2, ..., n-1, n とループする\n    for (int i = 1; i <= n; i++) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (int j = 1; j <= n; j++) {\n            res.Append($\"({i}, {j}), \");\n        }\n    }\n    return res.ToString();\n}\n
iteration.go
/* 二重 for ループ */\nfunc nestedForLoop(n int) string {\n    res := \"\"\n    // i = 1, 2, ..., n-1, n とループする\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= n; j++ {\n            // j = 1, 2, ..., n-1, n とループする\n            res += fmt.Sprintf(\"(%d, %d), \", i, j)\n        }\n    }\n    return res\n}\n
iteration.swift
/* 二重 for ループ */\nfunc nestedForLoop(n: Int) -> String {\n    var res = \"\"\n    // i = 1, 2, ..., n-1, n とループする\n    for i in 1 ... n {\n        // j = 1, 2, ..., n-1, n とループする\n        for j in 1 ... n {\n            res.append(\"(\\(i), \\(j)), \")\n        }\n    }\n    return res\n}\n
iteration.js
/* 二重 for ループ */\nfunction nestedForLoop(n) {\n    let res = '';\n    // i = 1, 2, ..., n-1, n とループする\n    for (let i = 1; i <= n; i++) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
iteration.ts
/* 二重 for ループ */\nfunction nestedForLoop(n: number): string {\n    let res = '';\n    // i = 1, 2, ..., n-1, n とループする\n    for (let i = 1; i <= n; i++) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
iteration.dart
/* 二重 for ループ */\nString nestedForLoop(int n) {\n  String res = \"\";\n  // i = 1, 2, ..., n-1, n とループする\n  for (int i = 1; i <= n; i++) {\n    // j = 1, 2, ..., n-1, n とループする\n    for (int j = 1; j <= n; j++) {\n      res += \"($i, $j), \";\n    }\n  }\n  return res;\n}\n
iteration.rs
/* 二重 for ループ */\nfn nested_for_loop(n: i32) -> String {\n    let mut res = vec![];\n    // i = 1, 2, ..., n-1, n とループする\n    for i in 1..=n {\n        // j = 1, 2, ..., n-1, n とループする\n        for j in 1..=n {\n            res.push(format!(\"({}, {}), \", i, j));\n        }\n    }\n    res.join(\"\")\n}\n
iteration.c
/* 二重 for ループ */\nchar *nestedForLoop(int n) {\n    // n * n は対応する点の個数であり、\"(i, j), \" に対応する文字列長の最大は 6+10*2 で、さらに末尾の空文字 \\0 のための追加領域が必要\n    int size = n * n * 26 + 1;\n    char *res = malloc(size * sizeof(char));\n    // i = 1, 2, ..., n-1, n とループする\n    for (int i = 1; i <= n; i++) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (int j = 1; j <= n; j++) {\n            char tmp[26];\n            snprintf(tmp, sizeof(tmp), \"(%d, %d), \", i, j);\n            strncat(res, tmp, size - strlen(res) - 1);\n        }\n    }\n    return res;\n}\n
iteration.kt
/* 二重 for ループ */\nfun nestedForLoop(n: Int): String {\n    val res = StringBuilder()\n    // i = 1, 2, ..., n-1, n とループする\n    for (i in 1..n) {\n        // j = 1, 2, ..., n-1, n とループする\n        for (j in 1..n) {\n            res.append(\" ($i, $j), \")\n        }\n    }\n    return res.toString()\n}\n
iteration.rb
### 二重 for ループ ###\ndef nested_for_loop(n)\n  res = \"\"\n\n  # i = 1, 2, ..., n-1, n とループする\n  for i in 1..n\n    # j = 1, 2, ..., n-1, n とループする\n    for j in 1..n\n      res += \"(#{i}, #{j}), \"\n    end\n  end\n\n  res\nend\n
コードの可視化

全画面で見る >

次の図は、このネストしたループのフローチャートです。

図 2-2   ネストしたループのフローチャート

この場合、関数の操作回数は \\(n^2\\) に比例し、言い換えればアルゴリズムの実行時間は入力データサイズ \\(n\\) と「二次関係」にあります。

さらにネストしたループを追加することもできます。ネストが 1 段増えるたびに「次元が 1 つ上がる」ことになり、時間計算量は「三次関係」「四次関係」へと高くなっていきます。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222","level":2,"title":"2.2.2   再帰","text":"

再帰(recursion)は、関数が自分自身を呼び出すことで問題を解決するアルゴリズム戦略です。主に 2 つの段階から成ります。

  1. 再帰呼び出し:プログラムは自分自身をより深く呼び出し続け、通常はより小さい、またはより単純化された引数を渡し、「終了条件」に達するまで進みます。
  2. 復帰: 「終了条件」が満たされると、プログラムは最も深い再帰関数から 1 層ずつ戻り、各層の結果をまとめていきます。

実装の観点から見ると、再帰コードは主に 3 つの要素から成ります。

  1. 終了条件:いつ再帰呼び出しから復帰へ切り替わるかを決めます。
  2. 再帰呼び出し:再帰呼び出しに対応し、関数が自分自身を呼び出します。通常はより小さい、またはより単純化された引数を入力します。
  3. 結果の返却:復帰に対応し、現在の再帰レベルの結果を 1 つ上の層へ返します。

次のコードを見ると、関数 recur(n) を呼び出すだけで \\(1 + 2 + \\dots + n\\) を計算できます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def recur(n: int) -> int:\n    \"\"\"再帰\"\"\"\n    # 終了条件\n    if n == 1:\n        return 1\n    # 再帰:再帰呼び出し\n    res = recur(n - 1)\n    # 帰りがけ:結果を返す\n    return n + res\n
recursion.cpp
/* 再帰 */\nint recur(int n) {\n    // 終了条件\n    if (n == 1)\n        return 1;\n    // 再帰:再帰呼び出し\n    int res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.java
/* 再帰 */\nint recur(int n) {\n    // 終了条件\n    if (n == 1)\n        return 1;\n    // 再帰:再帰呼び出し\n    int res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.cs
/* 再帰 */\nint Recur(int n) {\n    // 終了条件\n    if (n == 1)\n        return 1;\n    // 再帰:再帰呼び出し\n    int res = Recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.go
/* 再帰 */\nfunc recur(n int) int {\n    // 終了条件\n    if n == 1 {\n        return 1\n    }\n    // 再帰:再帰呼び出し\n    res := recur(n - 1)\n    // 帰りがけ:結果を返す\n    return n + res\n}\n
recursion.swift
/* 再帰 */\nfunc recur(n: Int) -> Int {\n    // 終了条件\n    if n == 1 {\n        return 1\n    }\n    // 再帰:再帰呼び出し\n    let res = recur(n: n - 1)\n    // 帰りがけ:結果を返す\n    return n + res\n}\n
recursion.js
/* 再帰 */\nfunction recur(n) {\n    // 終了条件\n    if (n === 1) return 1;\n    // 再帰:再帰呼び出し\n    const res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.ts
/* 再帰 */\nfunction recur(n: number): number {\n    // 終了条件\n    if (n === 1) return 1;\n    // 再帰:再帰呼び出し\n    const res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.dart
/* 再帰 */\nint recur(int n) {\n  // 終了条件\n  if (n == 1) return 1;\n  // 再帰:再帰呼び出し\n  int res = recur(n - 1);\n  // 帰りがけ:結果を返す\n  return n + res;\n}\n
recursion.rs
/* 再帰 */\nfn recur(n: i32) -> i32 {\n    // 終了条件\n    if n == 1 {\n        return 1;\n    }\n    // 再帰:再帰呼び出し\n    let res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    n + res\n}\n
recursion.c
/* 再帰 */\nint recur(int n) {\n    // 終了条件\n    if (n == 1)\n        return 1;\n    // 再帰:再帰呼び出し\n    int res = recur(n - 1);\n    // 帰りがけ:結果を返す\n    return n + res;\n}\n
recursion.kt
/* 再帰 */\nfun recur(n: Int): Int {\n    // 終了条件\n    if (n == 1)\n        return 1\n    // 再帰: 再帰呼び出し\n    val res = recur(n - 1)\n    // 戻る: 結果を返す\n    return n + res\n}\n
recursion.rb
### 再帰 ###\ndef recur(n)\n  # 終了条件\n  return 1 if n == 1\n  # 再帰:再帰呼び出し\n  res = recur(n - 1)\n  # 帰りがけ:結果を返す\n  n + res\nend\n
コードの可視化

全画面で見る >

次の図は、この関数の再帰過程を示しています。

図 2-3   総和関数の再帰過程

計算の観点では、反復と再帰は同じ結果を得られますが、それらは問題を考え解決するためのまったく異なる 2 つのパラダイムを表しています。

  • 反復:「ボトムアップ」で問題を解決します。最も基本的な手順から始め、それらを繰り返したり積み上げたりして、処理が完了するまで進めます。
  • 再帰:「トップダウン」で問題を解決します。元の問題をより小さな部分問題に分解し、それらの部分問題は元の問題と同じ形を持ちます。さらに部分問題をより小さな部分問題へと分解し、基本ケースに達したところで停止します(基本ケースの解は既知です)。

前述の総和関数を例に、問題を \\(f(n) = 1 + 2 + \\dots + n\\) とします。

  • 反復:ループ内で総和の過程を模擬し、\\(1\\) から \\(n\\) まで走査して、各反復で加算を行えば \\(f(n)\\) を求められます。
  • 再帰:問題を部分問題 \\(f(n) = n + f(n-1)\\) に分解し、これを再帰的に分解し続け、基本ケース \\(f(1) = 1\\) に達したところで終了します。
","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1","level":3,"title":"1.   呼び出しスタック","text":"

再帰関数が自分自身を呼び出すたびに、システムは新たに開始された関数のためにメモリを割り当て、局所変数、呼び出し先アドレス、その他の情報を保存します。これにより 2 つの結果が生じます。

  • 関数のコンテキストデータは「スタックフレーム領域」と呼ばれるメモリ領域に保存され、関数が戻るまで解放されません。したがって、再帰は通常、反復より多くのメモリ空間を消費します。
  • 再帰による関数呼び出しには追加のオーバーヘッドが発生します。そのため再帰は通常、ループより時間効率が低くなります。

次の図のように、終了条件が発動する前には、まだ戻っていない再帰関数が同時に \\(n\\) 個存在し、再帰の深さは \\(n\\) になります。

図 2-4   再帰呼び出しの深さ

実際には、プログラミング言語が許容する再帰の深さには通常上限があり、深すぎる再帰はスタックオーバーフローを引き起こす可能性があります。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2","level":3,"title":"2.   末尾再帰","text":"

興味深いことに、関数が返る直前の最後の処理で再帰呼び出しを行う場合、その関数はコンパイラやインタプリタによって最適化され、空間効率が反復と同程度になることがあります。これを末尾再帰(tail recursion)と呼びます。

  • 通常の再帰:関数が 1 つ上の階層の関数へ戻った後も、引き続きコードを実行する必要があるため、システムは 1 つ上の呼び出しのコンテキストを保存しておく必要があります。
  • 末尾再帰:再帰呼び出しが関数の返却前の最後の操作であるため、1 つ上の階層へ戻った後に他の処理を続ける必要がなく、システムは 1 つ上の関数のコンテキストを保存する必要がありません。

\\(1 + 2 + \\dots + n\\) の計算を例にすると、結果変数 res を関数の引数にすることで、末尾再帰を実現できます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def tail_recur(n, res):\n    \"\"\"末尾再帰\"\"\"\n    # 終了条件\n    if n == 0:\n        return res\n    # 末尾再帰呼び出し\n    return tail_recur(n - 1, res + n)\n
recursion.cpp
/* 末尾再帰 */\nint tailRecur(int n, int res) {\n    // 終了条件\n    if (n == 0)\n        return res;\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n);\n}\n
recursion.java
/* 末尾再帰 */\nint tailRecur(int n, int res) {\n    // 終了条件\n    if (n == 0)\n        return res;\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n);\n}\n
recursion.cs
/* 末尾再帰 */\nint TailRecur(int n, int res) {\n    // 終了条件\n    if (n == 0)\n        return res;\n    // 末尾再帰呼び出し\n    return TailRecur(n - 1, res + n);\n}\n
recursion.go
/* 末尾再帰 */\nfunc tailRecur(n int, res int) int {\n    // 終了条件\n    if n == 0 {\n        return res\n    }\n    // 末尾再帰呼び出し\n    return tailRecur(n-1, res+n)\n}\n
recursion.swift
/* 末尾再帰 */\nfunc tailRecur(n: Int, res: Int) -> Int {\n    // 終了条件\n    if n == 0 {\n        return res\n    }\n    // 末尾再帰呼び出し\n    return tailRecur(n: n - 1, res: res + n)\n}\n
recursion.js
/* 末尾再帰 */\nfunction tailRecur(n, res) {\n    // 終了条件\n    if (n === 0) return res;\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n);\n}\n
recursion.ts
/* 末尾再帰 */\nfunction tailRecur(n: number, res: number): number {\n    // 終了条件\n    if (n === 0) return res;\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n);\n}\n
recursion.dart
/* 末尾再帰 */\nint tailRecur(int n, int res) {\n  // 終了条件\n  if (n == 0) return res;\n  // 末尾再帰呼び出し\n  return tailRecur(n - 1, res + n);\n}\n
recursion.rs
/* 末尾再帰 */\nfn tail_recur(n: i32, res: i32) -> i32 {\n    // 終了条件\n    if n == 0 {\n        return res;\n    }\n    // 末尾再帰呼び出し\n    tail_recur(n - 1, res + n)\n}\n
recursion.c
/* 末尾再帰 */\nint tailRecur(int n, int res) {\n    // 終了条件\n    if (n == 0)\n        return res;\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n);\n}\n
recursion.kt
/* 末尾再帰 */\ntailrec fun tailRecur(n: Int, res: Int): Int {\n    // `tailrec` キーワードを追加して末尾再帰最適化を有効にする\n    // 終了条件\n    if (n == 0)\n        return res\n    // 末尾再帰呼び出し\n    return tailRecur(n - 1, res + n)\n}\n
recursion.rb
### 末尾再帰 ###\ndef tail_recur(n, res)\n  # 終了条件\n  return res if n == 0\n  # 末尾再帰呼び出し\n  tail_recur(n - 1, res + n)\nend\n
コードの可視化

全画面で見る >

末尾再帰の実行過程を次の図に示します。通常の再帰と末尾再帰を比べると、加算処理が実行されるタイミングが異なります。

  • 通常の再帰:加算処理は復帰の過程で実行され、各層が戻るたびにもう一度加算を行います。
  • 末尾再帰:加算処理は再帰呼び出しの過程で実行され、復帰の過程では各層が戻るだけで済みます。

図 2-5   末尾再帰の過程

Tip

多くのコンパイラやインタプリタは末尾再帰最適化をサポートしていない点に注意してください。たとえば、Python はデフォルトで末尾再帰最適化をサポートしていないため、関数が末尾再帰の形であっても、スタックオーバーフローが発生する可能性があります。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3_1","level":3,"title":"3.   再帰木","text":"

「分割統治」に関連するアルゴリズム問題を扱う際、再帰は反復よりも発想が直感的で、コードも読みやすいことがよくあります。「フィボナッチ数列」を例に見てみましょう。

Question

フィボナッチ数列 \\(0, 1, 1, 2, 3, 5, 8, 13, \\dots\\) が与えられたとき、この数列の第 \\(n\\) 項を求めてください。

フィボナッチ数列の第 \\(n\\) 項を \\(f(n)\\) とすると、次の 2 つが容易に分かります。

  • 数列の最初の 2 項は \\(f(1) = 0\\) と \\(f(2) = 1\\) です。
  • 数列中の各項は直前の 2 項の和であり、すなわち \\(f(n) = f(n - 1) + f(n - 2)\\) です。

漸化式に従って再帰呼び出しを行い、最初の 2 項を終了条件とすれば、再帰コードを書けます。fib(n) を呼び出すことでフィボナッチ数列の第 \\(n\\) 項を得られます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def fib(n: int) -> int:\n    \"\"\"フィボナッチ数列:再帰\"\"\"\n    # 終了条件 f(1) = 0, f(2) = 1\n    if n == 1 or n == 2:\n        return n - 1\n    # f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    res = fib(n - 1) + fib(n - 2)\n    # 結果 f(n) を返す\n    return res\n
recursion.cpp
/* フィボナッチ数列:再帰 */\nint fib(int n) {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    int res = fib(n - 1) + fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.java
/* フィボナッチ数列:再帰 */\nint fib(int n) {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    int res = fib(n - 1) + fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.cs
/* フィボナッチ数列:再帰 */\nint Fib(int n) {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    int res = Fib(n - 1) + Fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.go
/* フィボナッチ数列:再帰 */\nfunc fib(n int) int {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    res := fib(n-1) + fib(n-2)\n    // 結果 f(n) を返す\n    return res\n}\n
recursion.swift
/* フィボナッチ数列:再帰 */\nfunc fib(n: Int) -> Int {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    let res = fib(n: n - 1) + fib(n: n - 2)\n    // 結果 f(n) を返す\n    return res\n}\n
recursion.js
/* フィボナッチ数列:再帰 */\nfunction fib(n) {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    const res = fib(n - 1) + fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.ts
/* フィボナッチ数列:再帰 */\nfunction fib(n: number): number {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    const res = fib(n - 1) + fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.dart
/* フィボナッチ数列:再帰 */\nint fib(int n) {\n  // 終了条件 f(1) = 0, f(2) = 1\n  if (n == 1 || n == 2) return n - 1;\n  // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n  int res = fib(n - 1) + fib(n - 2);\n  // 結果 f(n) を返す\n  return res;\n}\n
recursion.rs
/* フィボナッチ数列:再帰 */\nfn fib(n: i32) -> i32 {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1;\n    }\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    let res = fib(n - 1) + fib(n - 2);\n    // 結果を返す\n    res\n}\n
recursion.c
/* フィボナッチ数列:再帰 */\nint fib(int n) {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    int res = fib(n - 1) + fib(n - 2);\n    // 結果 f(n) を返す\n    return res;\n}\n
recursion.kt
/* フィボナッチ数列:再帰 */\nfun fib(n: Int): Int {\n    // 終了条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1\n    // f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n    val res = fib(n - 1) + fib(n - 2)\n    // 結果 f(n) を返す\n    return res\n}\n
recursion.rb
### フィボナッチ数列:再帰 ###\ndef fib(n)\n  # 終了条件 f(1) = 0, f(2) = 1\n  return n - 1 if n == 1 || n == 2\n  # f(n) = f(n-1) + f(n-2) を再帰的に呼び出す\n  res = fib(n - 1) + fib(n - 2)\n  # 結果 f(n) を返す\n  res\nend\n
コードの可視化

全画面で見る >

上のコードを見ると、関数内で 2 回の再帰呼び出しを行っています。これは 1 回の呼び出しから 2 つの呼び出し分岐が生じることを意味します。次の図のように、この再帰呼び出しを繰り返していくと、最終的に深さ \\(n\\) の再帰木(recursion tree)が生成されます。

図 2-6   フィボナッチ数列の再帰木

本質的に見ると、再帰は「問題をより小さな部分問題へ分解する」という思考パラダイムを体現しており、この分割統治の戦略は非常に重要です。

  • アルゴリズムの観点では、探索、ソート、バックトラッキング、分割統治、動的計画法など、多くの重要な戦略が直接または間接にこの考え方を用いています。
  • データ構造の観点では、再帰は連結リスト、木、グラフに関する問題の処理に本質的に適しており、これらは分割統治の考え方で分析しやすいからです。
","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223","level":2,"title":"2.2.3   両者の比較","text":"

以上をまとめると、次の表のように、反復と再帰は実装、性能、適用性の面で違いがあります。

表 2-1   反復と再帰の特徴の比較

反復 再帰 実装方法 ループ構造 関数が自分自身を呼び出す 時間効率 通常は効率が高く、関数呼び出しの負荷がない 関数呼び出しのたびにオーバーヘッドが発生する メモリ使用 通常は固定サイズのメモリ空間を使う 関数呼び出しの蓄積により大量のスタックフレーム領域を使う可能性がある 適用対象 単純な反復処理に適し、コードが直感的で読みやすい 木、グラフ、分割統治、バックトラッキングなどの部分問題分解に適し、コード構造が簡潔で明快

Tip

以下の内容が難しいと感じる場合は、「スタック」の章を読み終えた後に改めて復習してください。

では、反復と再帰にはどのような内在的な関係があるのでしょうか。前述の再帰関数を例にすると、加算処理は再帰の復帰段階で行われます。これは、最初に呼び出された関数が実際には最後に加算を完了することを意味しており、この動作の仕組みはスタックの「後入れ先出し」の原則とよく似ています。

実際、「呼び出しスタック」や「スタックフレーム領域」といった再帰の用語自体が、再帰とスタックの密接な関係を示唆しています。

  1. 再帰呼び出し:関数が呼び出されると、システムは「呼び出しスタック」上にその関数のための新しいスタックフレームを割り当て、局所変数、引数、返却先アドレスなどのデータを保存します。
  2. 復帰:関数の実行が完了して戻ると、対応するスタックフレームは「呼び出しスタック」から取り除かれ、前の関数の実行環境が復元されます。

したがって、明示的なスタックを使って呼び出しスタックの振る舞いを模擬することができ、その結果として再帰を反復形式へ変換できます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def for_loop_recur(n: int) -> int:\n    \"\"\"反復で再帰を模擬する\"\"\"\n    # 明示的なスタックを使ってシステムコールスタックを模擬する\n    stack = []\n    res = 0\n    # 再帰:再帰呼び出し\n    for i in range(n, 0, -1):\n        # 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.append(i)\n    # 帰りがけ:結果を返す\n    while stack:\n        # 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop()\n    # res = 1+2+3+...+n\n    return res\n
recursion.cpp
/* 反復で再帰を模擬する */\nint forLoopRecur(int n) {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    stack<int> stack;\n    int res = 0;\n    // 再帰:再帰呼び出し\n    for (int i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i);\n    }\n    // 帰りがけ:結果を返す\n    while (!stack.empty()) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.top();\n        stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.java
/* 反復で再帰を模擬する */\nint forLoopRecur(int n) {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    Stack<Integer> stack = new Stack<>();\n    int res = 0;\n    // 再帰:再帰呼び出し\n    for (int i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i);\n    }\n    // 帰りがけ:結果を返す\n    while (!stack.isEmpty()) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.cs
/* 反復で再帰を模擬する */\nint ForLoopRecur(int n) {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    Stack<int> stack = new();\n    int res = 0;\n    // 再帰:再帰呼び出し\n    for (int i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.Push(i);\n    }\n    // 帰りがけ:結果を返す\n    while (stack.Count > 0) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.Pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.go
/* 反復で再帰を模擬する */\nfunc forLoopRecur(n int) int {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    stack := list.New()\n    res := 0\n    // 再帰:再帰呼び出し\n    for i := n; i > 0; i-- {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.PushBack(i)\n    }\n    // 帰りがけ:結果を返す\n    for stack.Len() != 0 {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.Back().Value.(int)\n        stack.Remove(stack.Back())\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.swift
/* 反復で再帰を模擬する */\nfunc forLoopRecur(n: Int) -> Int {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    var stack: [Int] = []\n    var res = 0\n    // 再帰:再帰呼び出し\n    for i in (1 ... n).reversed() {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.append(i)\n    }\n    // 帰りがけ:結果を返す\n    while !stack.isEmpty {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.removeLast()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.js
/* 反復で再帰を模擬する */\nfunction forLoopRecur(n) {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    const stack = [];\n    let res = 0;\n    // 再帰:再帰呼び出し\n    for (let i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i);\n    }\n    // 帰りがけ:結果を返す\n    while (stack.length) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.ts
/* 反復で再帰を模擬する */\nfunction forLoopRecur(n: number): number {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    const stack: number[] = [];\n    let res: number = 0;\n    // 再帰:再帰呼び出し\n    for (let i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i);\n    }\n    // 帰りがけ:結果を返す\n    while (stack.length) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.dart
/* 反復で再帰を模擬する */\nint forLoopRecur(int n) {\n  // 明示的なスタックを使ってシステムコールスタックを模擬する\n  List<int> stack = [];\n  int res = 0;\n  // 再帰:再帰呼び出し\n  for (int i = n; i > 0; i--) {\n    // 「スタックへのプッシュ」で「再帰」を模擬する\n    stack.add(i);\n  }\n  // 帰りがけ:結果を返す\n  while (!stack.isEmpty) {\n    // 「スタックから取り出す操作」で「帰り」をシミュレート\n    res += stack.removeLast();\n  }\n  // res = 1+2+3+...+n\n  return res;\n}\n
recursion.rs
/* 反復で再帰を模擬する */\nfn for_loop_recur(n: i32) -> i32 {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    let mut stack = Vec::new();\n    let mut res = 0;\n    // 再帰:再帰呼び出し\n    for i in (1..=n).rev() {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i);\n    }\n    // 帰りがけ:結果を返す\n    while !stack.is_empty() {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop().unwrap();\n    }\n    // res = 1+2+3+...+n\n    res\n}\n
recursion.c
/* 反復で再帰を模擬する */\nint forLoopRecur(int n) {\n    int stack[1000]; // 大きな配列を使ってスタックを実装する\n    int top = -1;    // スタックトップのインデックス\n    int res = 0;\n    // 再帰:再帰呼び出し\n    for (int i = n; i > 0; i--) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack[1 + top++] = i;\n    }\n    // 帰りがけ:結果を返す\n    while (top >= 0) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack[top--];\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.kt
/* 反復で再帰を模擬する */\nfun forLoopRecur(n: Int): Int {\n    // 明示的なスタックを使ってシステムコールスタックを模擬する\n    val stack = Stack<Int>()\n    var res = 0\n    // 再帰: 再帰呼び出し\n    for (i in n downTo 0) {\n        // 「スタックへのプッシュ」で「再帰」を模擬する\n        stack.push(i)\n    }\n    // 戻る: 結果を返す\n    while (stack.isNotEmpty()) {\n        // 「スタックから取り出す操作」で「帰り」をシミュレート\n        res += stack.pop()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.rb
### 反復で再帰をシミュレート ###\ndef for_loop_recur(n)\n  # 明示的なスタックを使ってシステムコールスタックを模擬する\n  stack = []\n  res = 0\n\n  # 再帰:再帰呼び出し\n  for i in n.downto(0)\n    # 「スタックへのプッシュ」で「再帰」を模擬する\n    stack << i\n  end\n  # 帰りがけ:結果を返す\n  while !stack.empty?\n    res += stack.pop\n  end\n\n  # res = 1+2+3+...+n\n  res\nend\n
コードの可視化

全画面で見る >

上のコードを見ると、再帰を反復へ変換すると、コードはより複雑になります。反復と再帰は多くの場合に相互変換できますが、常にそうする価値があるとは限りません。理由は次の 2 点です。

  • 変換後のコードは理解しにくくなり、可読性が下がる可能性があります。
  • 複雑な問題によっては、システムの呼び出しスタックの振る舞いを模擬すること自体が非常に難しい場合があります。

要するに、反復を選ぶか再帰を選ぶかは、対象となる問題の性質によって決まります。実際のプログラミングでは、両者の長所と短所を見極め、状況に応じて適切な方法を選ぶことが重要です。

","path":["第 2 章   計算量解析","2.2   反復と再帰"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   アルゴリズム効率の評価","text":"

アルゴリズム設計では、次の 2 つのレベルの目標を順に追求します。

  1. 問題の解法を見つける:アルゴリズムは、定められた入力範囲内で問題の正しい解を確実に求められる必要があります。
  2. 最適な解法を追求する:同じ問題に対して複数の解法が存在する場合があり、私たちはできるだけ効率的なアルゴリズムを見つけたいと考えます。

つまり、問題を解けることを前提として、アルゴリズム効率はその良し悪しを測る主要な評価指標となっており、次の 2 つの観点を含みます。

  • 時間効率:アルゴリズムの実行時間の長さ。
  • 空間効率:アルゴリズムが使用するメモリ空間の大きさ。

簡単に言えば、**私たちの目標は「高速で省メモリ」なデータ構造とアルゴリズムを設計すること**です。そして、アルゴリズム効率を効果的に評価することは非常に重要です。そうすることで初めて、さまざまなアルゴリズムを比較し、さらにアルゴリズム設計と最適化の過程を導けるからです。

効率の評価方法は主に 2 種類に分けられます。実測と理論的な見積もりです。

","path":["第 2 章   計算量解析","2.1   アルゴリズム効率の評価"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211","level":2,"title":"2.1.1   実測","text":"

いまアルゴリズム A とアルゴリズム B があり、どちらも同じ問題を解けるとします。この 2 つのアルゴリズムの効率を比較する必要がある場合、最も直接的な方法は 1 台のコンピュータで両者を実行し、その実行時間とメモリ使用量を監視して記録することです。この評価方法は実際の状況を反映できますが、大きな制約もあります。

一方では、**テスト環境による干渉要因を排除しにくい**という問題があります。ハードウェア構成はアルゴリズムの性能に影響します。たとえば、並列度の高いアルゴリズムはマルチコア CPU での実行により適しており、メモリアクセスが集中的なアルゴリズムは高性能メモリ上でより良い性能を示します。つまり、異なるマシンでのテスト結果は一致しない可能性があります。これは、さまざまなマシンでテストして平均効率を統計的に求める必要があることを意味しますが、それは現実的ではありません。

他方では、**完全なテストを実施するには非常に多くの資源が必要**です。入力データ量が変化すると、アルゴリズムは異なる効率を示します。たとえば、入力データ量が小さいときはアルゴリズム A の実行時間がアルゴリズム B より短くても、入力データ量が大きいときには結果がちょうど逆になるかもしれません。そのため、説得力のある結論を得るには、さまざまな規模の入力データでテストする必要があり、それには大量の計算資源を要します。

","path":["第 2 章   計算量解析","2.1   アルゴリズム効率の評価"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#212","level":2,"title":"2.1.2   理論的な見積もり","text":"

実測には大きな制約があるため、いくつかの計算だけによってアルゴリズムの効率を評価することを考えられます。この見積もり方法は漸近計算量解析(asymptotic complexity analysis)と呼ばれ、略して計算量解析といいます。

計算量解析は、アルゴリズムの実行に必要な時間資源と空間資源が入力データ規模とどのような関係にあるかを表します。これは、入力データ規模が増加するにつれて、アルゴリズムの実行に必要な時間と空間がどのように増加するかという傾向を記述するものです。この定義はややわかりにくいので、次の 3 つのポイントに分けて理解できます。

  • 「時間資源と空間資源」は、それぞれ時間計算量(time complexity)と空間計算量(space complexity)に対応します。
  • 「入力データ規模が増加するにつれて」とは、計算量がアルゴリズムの実行効率と入力データ規模との関係を反映していることを意味します。
  • 「時間と空間の増加傾向」とは、計算量解析が注目するのは実行時間や使用空間の具体的な値ではなく、時間や空間の増加の「速さ」であることを示します。

計算量解析は実測という方法の欠点を克服しています。その点は次のように表れます。

  • 実際にコードを動かす必要がなく、より環境にやさしく省エネルギーです。
  • テスト環境から独立しており、解析結果はすべての実行プラットフォームに適用できます。
  • 異なるデータ量におけるアルゴリズム効率を表せ、とくに大規模データ量での性能を反映できます。

Tip

それでも計算量の概念がまだわかりにくくても、心配はいりません。後続の章で詳しく説明します。

計算量解析は、アルゴリズム効率を評価するための「物差し」を私たちに与えてくれます。これにより、あるアルゴリズムの実行に必要な時間資源と空間資源を測り、異なるアルゴリズム同士の効率を比較できます。

計算量は数学的な概念であり、初学者にとってはやや抽象的で、学習の難度も比較的高いかもしれません。この観点から見ると、計算量解析は最初に紹介する内容としてはあまり適していない可能性があります。しかし、あるデータ構造やアルゴリズムの特徴を議論する際には、その実行速度や空間使用状況の分析を避けることはできません。

以上を踏まえると、データ構造とアルゴリズムを深く学ぶ前に、**まず計算量解析について初歩的な理解を持ち、簡単なアルゴリズムの計算量解析ができるようにしておくこと**を勧めます。

","path":["第 2 章   計算量解析","2.1   アルゴリズム効率の評価"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/","level":1,"title":"2.4   空間計算量","text":"

空間計算量(space complexity)は、アルゴリズムが占有するメモリ空間がデータ量の増加に伴ってどのように増えるかを測る指標です。この概念は時間計算量と非常によく似ており、「実行時間」を「占有メモリ空間」に置き換えるだけです。

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#241","level":2,"title":"2.4.1   アルゴリズムに関連する空間","text":"

アルゴリズムが実行中に使用するメモリ空間には、主に次の種類があります。

  • 入力空間:アルゴリズムの入力データを格納するための空間。
  • 一時空間:アルゴリズムの実行中に使用する変数、オブジェクト、関数コンテキストなどのデータを格納するための空間。
  • 出力空間:アルゴリズムの出力データを格納するための空間。

一般に、空間計算量の集計範囲は「一時空間」と「出力空間」を合わせたものです。

一時空間はさらに三つに分けられます。

  • 一時データ:アルゴリズム実行中の各種定数、変数、オブジェクトなどを保存するための空間。
  • スタックフレーム空間:呼び出された関数のコンテキストデータを保存するための空間。システムは関数を呼び出すたびにスタックの先頭にスタックフレームを作成し、関数が戻るとその空間を解放します。
  • 命令空間:コンパイル後のプログラム命令を保存するための空間で、実際の集計では通常無視されます。

プログラムの空間計算量を分析する際には、通常、一時データ、スタックフレーム空間、出力データの三つを数えます。以下の図に示すとおりです。

図 2-15   アルゴリズムで使用される関連空間

関連するコードを以下に示します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class Node:\n    \"\"\"クラス\"\"\"\n    def __init__(self, x: int):\n        self.val: int = x              # ノードの値\n        self.next: Node | None = None  # 次のノードへの参照\n\ndef function() -> int:\n    \"\"\"関数\"\"\"\n    # いくつかの処理を実行...\n    return 0\n\ndef algorithm(n) -> int:  # 入力データ\n    A = 0                 # 一時データ(定数。一般に大文字で表す)\n    b = 0                 # 一時データ(変数)\n    node = Node(0)        # 一時データ(オブジェクト)\n    c = function()        # スタックフレーム空間(関数呼び出し)\n    return A + b + c      # 出力データ\n
/* 構造体 */\nstruct Node {\n    int val;\n    Node *next;\n    Node(int x) : val(x), next(nullptr) {}\n};\n\n/* 関数 */\nint func() {\n    // いくつかの処理を実行...\n    return 0;\n}\n\nint algorithm(int n) {        // 入力データ\n    const int a = 0;          // 一時データ(定数)\n    int b = 0;                // 一時データ(変数)\n    Node* node = new Node(0); // 一時データ(オブジェクト)\n    int c = func();           // スタックフレーム空間(関数呼び出し)\n    return a + b + c;         // 出力データ\n}\n
/* クラス */\nclass Node {\n    int val;\n    Node next;\n    Node(int x) { val = x; }\n}\n\n/* 関数 */\nint function() {\n    // いくつかの処理を実行...\n    return 0;\n}\n\nint algorithm(int n) {        // 入力データ\n    final int a = 0;          // 一時データ(定数)\n    int b = 0;                // 一時データ(変数)\n    Node node = new Node(0);  // 一時データ(オブジェクト)\n    int c = function();       // スタックフレーム空間(関数呼び出し)\n    return a + b + c;         // 出力データ\n}\n
/* クラス */\nclass Node(int x) {\n    int val = x;\n    Node next;\n}\n\n/* 関数 */\nint Function() {\n    // いくつかの処理を実行...\n    return 0;\n}\n\nint Algorithm(int n) {        // 入力データ\n    const int a = 0;          // 一時データ(定数)\n    int b = 0;                // 一時データ(変数)\n    Node node = new(0);       // 一時データ(オブジェクト)\n    int c = Function();       // スタックフレーム空間(関数呼び出し)\n    return a + b + c;         // 出力データ\n}\n
/* 構造体 */\ntype node struct {\n    val  int\n    next *node\n}\n\n/* node 構造体を作成 */\nfunc newNode(val int) *node {\n    return &node{val: val}\n}\n\n/* 関数 */\nfunc function() int {\n    // いくつかの処理を実行...\n    return 0\n}\n\nfunc algorithm(n int) int { // 入力データ\n    const a = 0             // 一時データ(定数)\n    b := 0                  // 一時データ(変数)\n    newNode(0)              // 一時データ(オブジェクト)\n    c := function()         // スタックフレーム空間(関数呼び出し)\n    return a + b + c        // 出力データ\n}\n
/* クラス */\nclass Node {\n    var val: Int\n    var next: Node?\n\n    init(x: Int) {\n        val = x\n    }\n}\n\n/* 関数 */\nfunc function() -> Int {\n    // いくつかの処理を実行...\n    return 0\n}\n\nfunc algorithm(n: Int) -> Int { // 入力データ\n    let a = 0             // 一時データ(定数)\n    var b = 0             // 一時データ(変数)\n    let node = Node(x: 0) // 一時データ(オブジェクト)\n    let c = function()    // スタックフレーム空間(関数呼び出し)\n    return a + b + c      // 出力データ\n}\n
/* クラス */\nclass Node {\n    val;\n    next;\n    constructor(val) {\n        this.val = val === undefined ? 0 : val; // ノードの値\n        this.next = null;                       // 次のノードへの参照\n    }\n}\n\n/* 関数 */\nfunction constFunc() {\n    // いくつかの処理を実行\n    return 0;\n}\n\nfunction algorithm(n) {       // 入力データ\n    const a = 0;              // 一時データ(定数)\n    let b = 0;                // 一時データ(変数)\n    const node = new Node(0); // 一時データ(オブジェクト)\n    const c = constFunc();    // スタックフレーム空間(関数呼び出し)\n    return a + b + c;         // 出力データ\n}\n
/* クラス */\nclass Node {\n    val: number;\n    next: Node | null;\n    constructor(val?: number) {\n        this.val = val === undefined ? 0 : val; // ノードの値\n        this.next = null;                       // 次のノードへの参照\n    }\n}\n\n/* 関数 */\nfunction constFunc(): number {\n    // いくつかの処理を実行\n    return 0;\n}\n\nfunction algorithm(n: number): number { // 入力データ\n    const a = 0;                        // 一時データ(定数)\n    let b = 0;                          // 一時データ(変数)\n    const node = new Node(0);           // 一時データ(オブジェクト)\n    const c = constFunc();              // スタックフレーム空間(関数呼び出し)\n    return a + b + c;                   // 出力データ\n}\n
/* クラス */\nclass Node {\n  int val;\n  Node next;\n  Node(this.val, [this.next]);\n}\n\n/* 関数 */\nint function() {\n  // いくつかの処理を実行...\n  return 0;\n}\n\nint algorithm(int n) {  // 入力データ\n  const int a = 0;      // 一時データ(定数)\n  int b = 0;            // 一時データ(変数)\n  Node node = Node(0);  // 一時データ(オブジェクト)\n  int c = function();   // スタックフレーム空間(関数呼び出し)\n  return a + b + c;     // 出力データ\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 構造体 */\nstruct Node {\n    val: i32,\n    next: Option<Rc<RefCell<Node>>>,\n}\n\n/* Node 構造体を作成 */\nimpl Node {\n    fn new(val: i32) -> Self {\n        Self { val: val, next: None }\n    }\n}\n\n/* 関数 */\nfn function() -> i32 {      \n    // いくつかの処理を実行...\n    return 0;\n}\n\nfn algorithm(n: i32) -> i32 {       // 入力データ\n    const a: i32 = 0;               // 一時データ(定数)\n    let mut b = 0;                  // 一時データ(変数)\n    let node = Node::new(0);        // 一時データ(オブジェクト)\n    let c = function();             // スタックフレーム空間(関数呼び出し)\n    return a + b + c;               // 出力データ\n}\n
/* 関数 */\nint func() {\n    // いくつかの処理を実行...\n    return 0;\n}\n\nint algorithm(int n) { // 入力データ\n    const int a = 0;   // 一時データ(定数)\n    int b = 0;         // 一時データ(変数)\n    int c = func();    // スタックフレーム空間(関数呼び出し)\n    return a + b + c;  // 出力データ\n}\n
/* クラス */\nclass Node(var _val: Int) {\n    var next: Node? = null\n}\n\n/* 関数 */\nfun function(): Int {\n    // いくつかの処理を実行...\n    return 0\n}\n\nfun algorithm(n: Int): Int { // 入力データ\n    val a = 0                // 一時データ(定数)\n    var b = 0                // 一時データ(変数)\n    val node = Node(0)       // 一時データ(オブジェクト)\n    val c = function()       // スタックフレーム空間(関数呼び出し)\n    return a + b + c         // 出力データ\n}\n
### クラス ###\nclass Node\n    attr_accessor :val      # ノードの値\n    attr_accessor :next     # 次のノードへの参照\n\n    def initialize(x)\n        @val = x\n    end\nend\n\n### 関数 ###\ndef function\n    # いくつかの処理を実行...\n    0\nend\n\n### アルゴリズム ###\ndef algorithm(n)        # 入力データ\n    a = 0               # 一時データ(定数)\n    b = 0               # 一時データ(変数)\n    node = Node.new(0)  # 一時データ(オブジェクト)\n    c = function        # スタックフレーム空間(関数呼び出し)\n    a + b + c           # 出力データ\nend\n
","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#242","level":2,"title":"2.4.2   推定方法","text":"

空間計算量の推定方法は時間計算量とおおむね同じで、数える対象を「操作回数」から「使用空間の大きさ」に変えるだけです。

ただし時間計算量と異なり、通常は最悪空間計算量だけに注目します。メモリ空間は厳格な要件であり、どの入力データに対しても十分なメモリを確保できることを保証しなければならないからです。

以下のコードを見ると、最悪空間計算量における「最悪」には二つの意味があります。

  1. 最悪の入力データを基準にする:\\(n < 10\\) のとき空間計算量は \\(O(1)\\) ですが、\\(n > 10\\) のとき初期化される配列 nums が \\(O(n)\\) の空間を占有するため、最悪空間計算量は \\(O(n)\\) です。
  2. アルゴリズム実行中のメモリ使用量のピークを基準にする:例えば、プログラムは最後の行を実行する前までは \\(O(1)\\) の空間しか使いませんが、配列 nums を初期化するときには \\(O(n)\\) の空間を占有するため、最悪空間計算量は \\(O(n)\\) です。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 0               # O(1)\n    b = [0] * 10000     # O(1)\n    if n > 10:\n        nums = [0] * n  # O(n)\n
void algorithm(int n) {\n    int a = 0;               // O(1)\n    vector<int> b(10000);    // O(1)\n    if (n > 10)\n        vector<int> nums(n); // O(n)\n}\n
void algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10)\n        int[] nums = new int[n]; // O(n)\n}\n
void Algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10) {\n        int[] nums = new int[n]; // O(n)\n    }\n}\n
func algorithm(n int) {\n    a := 0                      // O(1)\n    b := make([]int, 10000)     // O(1)\n    var nums []int\n    if n > 10 {\n        nums := make([]int, n)  // O(n)\n    }\n    fmt.Println(a, b, nums)\n}\n
func algorithm(n: Int) {\n    let a = 0 // O(1)\n    let b = Array(repeating: 0, count: 10000) // O(1)\n    if n > 10 {\n        let nums = Array(repeating: 0, count: n) // O(n)\n    }\n}\n
function algorithm(n) {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
function algorithm(n: number): void {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
void algorithm(int n) {\n  int a = 0;                            // O(1)\n  List<int> b = List.filled(10000, 0);  // O(1)\n  if (n > 10) {\n    List<int> nums = List.filled(n, 0); // O(n)\n  }\n}\n
fn algorithm(n: i32) {\n    let a = 0;                              // O(1)\n    let b = [0; 10000];                     // O(1)\n    if n > 10 {\n        let nums = vec![0; n as usize];     // O(n)\n    }\n}\n
void algorithm(int n) {\n    int a = 0;               // O(1)\n    int b[10000];            // O(1)\n    if (n > 10)\n        int nums[n] = {0};   // O(n)\n}\n
fun algorithm(n: Int) {\n    val a = 0                    // O(1)\n    val b = IntArray(10000)      // O(1)\n    if (n > 10) {\n        val nums = IntArray(n)   // O(n)\n    }\n}\n
def algorithm(n)\n    a = 0                           # O(1)\n    b = Array.new(10000)            # O(1)\n    nums = Array.new(n) if n > 10   # O(n)\nend\n

再帰関数では、スタックフレーム空間の集計に注意が必要です。以下のコードを見てみましょう。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def function() -> int:\n    # いくつかの処理を実行\n    return 0\n\ndef loop(n: int):\n    \"\"\"ループの空間計算量は O(1)\"\"\"\n    for _ in range(n):\n        function()\n\ndef recur(n: int):\n    \"\"\"再帰の空間計算量は O(n)\"\"\"\n    if n == 1:\n        return\n    return recur(n - 1)\n
int func() {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
int function() {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
int Function() {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nvoid Loop(int n) {\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nint Recur(int n) {\n    if (n == 1) return 1;\n    return Recur(n - 1);\n}\n
func function() int {\n    // いくつかの処理を実行\n    return 0\n}\n\n/* ループの空間計算量は O(1) */\nfunc loop(n int) {\n    for i := 0; i < n; i++ {\n        function()\n    }\n}\n\n/* 再帰の空間計算量は O(n) */\nfunc recur(n int) {\n    if n == 1 {\n        return\n    }\n    recur(n - 1)\n}\n
@discardableResult\nfunc function() -> Int {\n    // いくつかの処理を実行\n    return 0\n}\n\n/* ループの空間計算量は O(1) */\nfunc loop(n: Int) {\n    for _ in 0 ..< n {\n        function()\n    }\n}\n\n/* 再帰の空間計算量は O(n) */\nfunc recur(n: Int) {\n    if n == 1 {\n        return\n    }\n    recur(n: n - 1)\n}\n
function constFunc() {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nfunction loop(n) {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nfunction recur(n) {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
function constFunc(): number {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nfunction loop(n: number): void {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nfunction recur(n: number): void {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
int function() {\n  // いくつかの処理を実行\n  return 0;\n}\n/* ループの空間計算量は O(1) */\nvoid loop(int n) {\n  for (int i = 0; i < n; i++) {\n    function();\n  }\n}\n/* 再帰の空間計算量は O(n) */\nvoid recur(int n) {\n  if (n == 1) return;\n  recur(n - 1);\n}\n
fn function() -> i32 {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nfn loop(n: i32) {\n    for i in 0..n {\n        function();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nfn recur(n: i32) {\n    if n == 1 {\n        return;\n    }\n    recur(n - 1);\n}\n
int func() {\n    // いくつかの処理を実行\n    return 0;\n}\n/* ループの空間計算量は O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* 再帰の空間計算量は O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
fun function(): Int {\n    // いくつかの処理を実行\n    return 0\n}\n/* ループの空間計算量は O(1) */\nfun loop(n: Int) {\n    for (i in 0..<n) {\n        function()\n    }\n}\n/* 再帰の空間計算量は O(n) */\nfun recur(n: Int) {\n    if (n == 1) return\n    return recur(n - 1)\n}\n
def function\n    # いくつかの処理を実行\n    0\nend\n\n### ループの空間計算量は O(1) ###\ndef loop(n)\n    (0...n).each { function }\nend\n\n### 再帰の空間計算量は O(n) ###\ndef recur(n)\n    return if n == 1\n    recur(n - 1)\nend\n

関数 loop()recur() はどちらも時間計算量は \\(O(n)\\) ですが、空間計算量は異なります。

  • 関数 loop() はループの中で function() を \\(n\\) 回呼び出しますが、各反復での function() は戻るたびにスタックフレーム空間が解放されるため、空間計算量は依然として \\(O(1)\\) です。
  • 再帰関数 recur() は実行中に未返却の recur() が同時に \\(n\\) 個存在するため、\\(O(n)\\) のスタックフレーム空間を占有します。
","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243","level":2,"title":"2.4.3   よくある型","text":"

入力データサイズを \\(n\\) とすると、以下の図はよくある空間計算量の型を低い順から高い順に示しています。

\\[ \\begin{aligned} O(1) < O(\\log n) < O(n) < O(n^2) < O(2^n) \\newline \\text{定数階} < \\text{対数階} < \\text{線形階} < \\text{平方階} < \\text{指数階} \\end{aligned} \\]

図 2-16   よくある空間計算量の型

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#1-o1","level":3,"title":"1.   定数階 \\(O(1)\\)","text":"

定数階は、個数が入力データサイズ \\(n\\) に依存しない定数、変数、オブジェクトなどによく現れます。

注意すべき点として、ループ内で変数を初期化したり関数を呼び出したりして使用されたメモリは、次の反復に入ると解放されるため、空間の占有は累積せず、空間計算量は依然として \\(O(1)\\) です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def function() -> int:\n    \"\"\"関数\"\"\"\n    # 何らかの処理を行う\n    return 0\n\ndef constant(n: int):\n    \"\"\"定数階\"\"\"\n    # 定数、変数、オブジェクトは O(1) の空間を占める\n    a = 0\n    nums = [0] * 10000\n    node = ListNode(0)\n    # ループ内の変数は O(1) の空間を占める\n    for _ in range(n):\n        c = 0\n    # ループ内の関数は O(1) の空間を占める\n    for _ in range(n):\n        function()\n
space_complexity.cpp
/* 関数 */\nint func() {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nvoid constant(int n) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const int a = 0;\n    int b = 0;\n    vector<int> nums(10000);\n    ListNode node(0);\n    // ループ内の変数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
space_complexity.java
/* 関数 */\nint function() {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nvoid constant(int n) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    final int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new ListNode(0);\n    // ループ内の変数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n
space_complexity.cs
/* 関数 */\nint Function() {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nvoid Constant(int n) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new(0);\n    // ループ内の変数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n
space_complexity.go
/* 関数 */\nfunc function() int {\n    // いくつかの操作を実行...\n    return 0\n}\n\n/* 定数階 */\nfunc spaceConstant(n int) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const a = 0\n    b := 0\n    nums := make([]int, 10000)\n    node := newNode(0)\n    // ループ内の変数は O(1) の空間を占める\n    var c int\n    for i := 0; i < n; i++ {\n        c = 0\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for i := 0; i < n; i++ {\n        function()\n    }\n    b += 0\n    c += 0\n    nums[0] = 0\n    node.val = 0\n}\n
space_complexity.swift
/* 関数 */\n@discardableResult\nfunc function() -> Int {\n    // 何らかの処理を行う\n    return 0\n}\n\n/* 定数階 */\nfunc constant(n: Int) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    let a = 0\n    var b = 0\n    let nums = Array(repeating: 0, count: 10000)\n    let node = ListNode(x: 0)\n    // ループ内の変数は O(1) の空間を占める\n    for _ in 0 ..< n {\n        let c = 0\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for _ in 0 ..< n {\n        function()\n    }\n}\n
space_complexity.js
/* 関数 */\nfunction constFunc() {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nfunction constant(n) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // ループ内の変数は O(1) の空間を占める\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
space_complexity.ts
/* 関数 */\nfunction constFunc(): number {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nfunction constant(n: number): void {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // ループ内の変数は O(1) の空間を占める\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
space_complexity.dart
/* 関数 */\nint function() {\n  // 何らかの処理を行う\n  return 0;\n}\n\n/* 定数階 */\nvoid constant(int n) {\n  // 定数、変数、オブジェクトは O(1) の空間を占める\n  final int a = 0;\n  int b = 0;\n  List<int> nums = List.filled(10000, 0);\n  ListNode node = ListNode(0);\n  // ループ内の変数は O(1) の空間を占める\n  for (var i = 0; i < n; i++) {\n    int c = 0;\n  }\n  // ループ内の関数は O(1) の空間を占める\n  for (var i = 0; i < n; i++) {\n    function();\n  }\n}\n
space_complexity.rs
/* 関数 */\nfn function() -> i32 {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\n#[allow(unused)]\nfn constant(n: i32) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const A: i32 = 0;\n    let b = 0;\n    let nums = vec![0; 10000];\n    let node = ListNode::new(0);\n    // ループ内の変数は O(1) の空間を占める\n    for i in 0..n {\n        let c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for i in 0..n {\n        function();\n    }\n}\n
space_complexity.c
/* 関数 */\nint func() {\n    // 何らかの処理を行う\n    return 0;\n}\n\n/* 定数階 */\nvoid constant(int n) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    const int a = 0;\n    int b = 0;\n    int nums[1000];\n    ListNode *node = newListNode(0);\n    free(node);\n    // ループ内の変数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
space_complexity.kt
/* 関数 */\nfun function(): Int {\n    // 何らかの処理を行う\n    return 0\n}\n\n/* 定数階 */\nfun constant(n: Int) {\n    // 定数、変数、オブジェクトは O(1) の空間を占める\n    val a = 0\n    var b = 0\n    val nums = Array(10000) { 0 }\n    val node = ListNode(0)\n    // ループ内の変数は O(1) の空間を占める\n    for (i in 0..<n) {\n        val c = 0\n    }\n    // ループ内の関数は O(1) の空間を占める\n    for (i in 0..<n) {\n        function()\n    }\n}\n
space_complexity.rb
### 関数 ###\ndef function\n  # 何らかの処理を行う\n  0\nend\n\n### 定数階 ###\ndef constant(n)\n  # 定数、変数、オブジェクトは O(1) の空間を占める\n  a = 0\n  nums = [0] * 10000\n  node = ListNode.new\n\n  # ループ内の変数は O(1) の空間を占める\n  (0...n).each { c = 0 }\n  # ループ内の関数は O(1) の空間を占める\n  (0...n).each { function }\nend\n
コードの可視化

全画面で見る >

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#2-on","level":3,"title":"2.   線形階 \\(O(n)\\)","text":"

線形階は、要素数が \\(n\\) に比例する配列、連結リスト、スタック、キューなどによく現れます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def linear(n: int):\n    \"\"\"線形階\"\"\"\n    # 長さ n のリストは O(n) の空間を使用\n    nums = [0] * n\n    # 長さ n のハッシュテーブルは O(n) の空間を使用\n    hmap = dict[int, str]()\n    for i in range(n):\n        hmap[i] = str(i)\n
space_complexity.cpp
/* 線形階 */\nvoid linear(int n) {\n    // 長さ n の配列は O(n) の空間を使用\n    vector<int> nums(n);\n    // 長さ n のリストは O(n) の空間を使用\n    vector<ListNode> nodes;\n    for (int i = 0; i < n; i++) {\n        nodes.push_back(ListNode(i));\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    unordered_map<int, string> map;\n    for (int i = 0; i < n; i++) {\n        map[i] = to_string(i);\n    }\n}\n
space_complexity.java
/* 線形階 */\nvoid linear(int n) {\n    // 長さ n の配列は O(n) の空間を使用\n    int[] nums = new int[n];\n    // 長さ n のリストは O(n) の空間を使用\n    List<ListNode> nodes = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        nodes.add(new ListNode(i));\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    Map<Integer, String> map = new HashMap<>();\n    for (int i = 0; i < n; i++) {\n        map.put(i, String.valueOf(i));\n    }\n}\n
space_complexity.cs
/* 線形階 */\nvoid Linear(int n) {\n    // 長さ n の配列は O(n) の空間を使用\n    int[] nums = new int[n];\n    // 長さ n のリストは O(n) の空間を使用\n    List<ListNode> nodes = [];\n    for (int i = 0; i < n; i++) {\n        nodes.Add(new ListNode(i));\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    Dictionary<int, string> map = [];\n    for (int i = 0; i < n; i++) {\n        map.Add(i, i.ToString());\n    }\n}\n
space_complexity.go
/* 線形階 */\nfunc spaceLinear(n int) {\n    // 長さ n の配列は O(n) の空間を使用\n    _ = make([]int, n)\n    // 長さ n のリストは O(n) の空間を使用\n    var nodes []*node\n    for i := 0; i < n; i++ {\n        nodes = append(nodes, newNode(i))\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    m := make(map[int]string, n)\n    for i := 0; i < n; i++ {\n        m[i] = strconv.Itoa(i)\n    }\n}\n
space_complexity.swift
/* 線形階 */\nfunc linear(n: Int) {\n    // 長さ n の配列は O(n) の空間を使用\n    let nums = Array(repeating: 0, count: n)\n    // 長さ n のリストは O(n) の空間を使用\n    let nodes = (0 ..< n).map { ListNode(x: $0) }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, \"\\($0)\") })\n}\n
space_complexity.js
/* 線形階 */\nfunction linear(n) {\n    // 長さ n の配列は O(n) の空間を使用\n    const nums = new Array(n);\n    // 長さ n のリストは O(n) の空間を使用\n    const nodes = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
space_complexity.ts
/* 線形階 */\nfunction linear(n: number): void {\n    // 長さ n の配列は O(n) の空間を使用\n    const nums = new Array(n);\n    // 長さ n のリストは O(n) の空間を使用\n    const nodes: ListNode[] = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
space_complexity.dart
/* 線形階 */\nvoid linear(int n) {\n  // 長さ n の配列は O(n) の空間を使用\n  List<int> nums = List.filled(n, 0);\n  // 長さ n のリストは O(n) の空間を使用\n  List<ListNode> nodes = [];\n  for (var i = 0; i < n; i++) {\n    nodes.add(ListNode(i));\n  }\n  // 長さ n のハッシュテーブルは O(n) の空間を使用\n  Map<int, String> map = HashMap();\n  for (var i = 0; i < n; i++) {\n    map.putIfAbsent(i, () => i.toString());\n  }\n}\n
space_complexity.rs
/* 線形階 */\n#[allow(unused)]\nfn linear(n: i32) {\n    // 長さ n の配列は O(n) の空間を使用\n    let mut nums = vec![0; n as usize];\n    // 長さ n のリストは O(n) の空間を使用\n    let mut nodes = Vec::new();\n    for i in 0..n {\n        nodes.push(ListNode::new(i))\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    let mut map = HashMap::new();\n    for i in 0..n {\n        map.insert(i, i.to_string());\n    }\n}\n
space_complexity.c
/* ハッシュテーブル */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // uthash.h を用いて実装\n} HashTable;\n\n/* 線形階 */\nvoid linear(int n) {\n    // 長さ n の配列は O(n) の空間を使用\n    int *nums = malloc(sizeof(int) * n);\n    free(nums);\n\n    // 長さ n のリストは O(n) の空間を使用\n    ListNode **nodes = malloc(sizeof(ListNode *) * n);\n    for (int i = 0; i < n; i++) {\n        nodes[i] = newListNode(i);\n    }\n    // メモリを解放する\n    for (int i = 0; i < n; i++) {\n        free(nodes[i]);\n    }\n    free(nodes);\n\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    HashTable *h = NULL;\n    for (int i = 0; i < n; i++) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = i;\n        tmp->val = i;\n        HASH_ADD_INT(h, key, tmp);\n    }\n\n    // メモリを解放する\n    HashTable *curr, *tmp;\n    HASH_ITER(hh, h, curr, tmp) {\n        HASH_DEL(h, curr);\n        free(curr);\n    }\n}\n
space_complexity.kt
/* 線形階 */\nfun linear(n: Int) {\n    // 長さ n の配列は O(n) の空間を使用\n    val nums = Array(n) { 0 }\n    // 長さ n のリストは O(n) の空間を使用\n    val nodes = mutableListOf<ListNode>()\n    for (i in 0..<n) {\n        nodes.add(ListNode(i))\n    }\n    // 長さ n のハッシュテーブルは O(n) の空間を使用\n    val map = mutableMapOf<Int, String>()\n    for (i in 0..<n) {\n        map[i] = i.toString()\n    }\n}\n
space_complexity.rb
### 線形階 ###\ndef linear(n)\n  # 長さ n のリストは O(n) の空間を使用\n  nums = Array.new(n, 0)\n\n  # 長さ n のハッシュテーブルは O(n) の空間を使用\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n
コードの可視化

全画面で見る >

以下の図に示すように、この関数の再帰の深さは \\(n\\) であり、同時に \\(n\\) 個の未返却 linear_recur() 関数が存在するため、\\(O(n)\\) のスタックフレーム空間を使用します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def linear_recur(n: int):\n    \"\"\"線形時間(再帰実装)\"\"\"\n    print(\"再帰 n =\", n)\n    if n == 1:\n        return\n    linear_recur(n - 1)\n
space_complexity.cpp
/* 線形時間(再帰実装) */\nvoid linearRecur(int n) {\n    cout << \"再帰 n = \" << n << endl;\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.java
/* 線形時間(再帰実装) */\nvoid linearRecur(int n) {\n    System.out.println(\"再帰 n = \" + n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.cs
/* 線形時間(再帰実装) */\nvoid LinearRecur(int n) {\n    Console.WriteLine(\"再帰 n = \" + n);\n    if (n == 1) return;\n    LinearRecur(n - 1);\n}\n
space_complexity.go
/* 線形時間(再帰実装) */\nfunc spaceLinearRecur(n int) {\n    fmt.Println(\"再帰 n =\", n)\n    if n == 1 {\n        return\n    }\n    spaceLinearRecur(n - 1)\n}\n
space_complexity.swift
/* 線形時間(再帰実装) */\nfunc linearRecur(n: Int) {\n    print(\"再帰 n = \\(n)\")\n    if n == 1 {\n        return\n    }\n    linearRecur(n: n - 1)\n}\n
space_complexity.js
/* 線形時間(再帰実装) */\nfunction linearRecur(n) {\n    console.log(`再帰 n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
space_complexity.ts
/* 線形時間(再帰実装) */\nfunction linearRecur(n: number): void {\n    console.log(`再帰 n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
space_complexity.dart
/* 線形時間(再帰実装) */\nvoid linearRecur(int n) {\n  print('再帰 n = $n');\n  if (n == 1) return;\n  linearRecur(n - 1);\n}\n
space_complexity.rs
/* 線形時間(再帰実装) */\nfn linear_recur(n: i32) {\n    println!(\"再帰 n = {}\", n);\n    if n == 1 {\n        return;\n    };\n    linear_recur(n - 1);\n}\n
space_complexity.c
/* 線形時間(再帰実装) */\nvoid linearRecur(int n) {\n    printf(\"再帰 n = %d\\r\\n\", n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.kt
/* 線形時間(再帰実装) */\nfun linearRecur(n: Int) {\n    println(\"再帰 n = $n\")\n    if (n == 1)\n        return\n    linearRecur(n - 1)\n}\n
space_complexity.rb
### 線形階 ###\ndef linear(n)\n  # 長さ n のリストは O(n) の空間を使用\n  nums = Array.new(n, 0)\n\n  # 長さ n のハッシュテーブルは O(n) の空間を使用\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n\n# ## 線形階(再帰実装)###\ndef linear_recur(n)\n  puts \"再帰 n = #{n}\"\n  return if n == 1\n  linear_recur(n - 1)\nend\n
コードの可視化

全画面で見る >

図 2-17   再帰関数が生み出す線形階の空間計算量

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#3-on2","level":3,"title":"3.   平方階 \\(O(n^2)\\)","text":"

平方階は、要素数が \\(n\\) の二乗に比例する行列やグラフによく現れます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def quadratic(n: int):\n    \"\"\"二乗階\"\"\"\n    # 二次元リストは O(n^2) の空間を使用\n    num_matrix = [[0] * n for _ in range(n)]\n
space_complexity.cpp
/* 二乗階 */\nvoid quadratic(int n) {\n    // 二次元リストは O(n^2) の空間を使用\n    vector<vector<int>> numMatrix;\n    for (int i = 0; i < n; i++) {\n        vector<int> tmp;\n        for (int j = 0; j < n; j++) {\n            tmp.push_back(0);\n        }\n        numMatrix.push_back(tmp);\n    }\n}\n
space_complexity.java
/* 二乗階 */\nvoid quadratic(int n) {\n    // 行列は O(n^2) の空間を使用する\n    int[][] numMatrix = new int[n][n];\n    // 二次元リストは O(n^2) の空間を使用\n    List<List<Integer>> numList = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<Integer> tmp = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            tmp.add(0);\n        }\n        numList.add(tmp);\n    }\n}\n
space_complexity.cs
/* 二乗階 */\nvoid Quadratic(int n) {\n    // 行列は O(n^2) の空間を使用する\n    int[,] numMatrix = new int[n, n];\n    // 二次元リストは O(n^2) の空間を使用\n    List<List<int>> numList = [];\n    for (int i = 0; i < n; i++) {\n        List<int> tmp = [];\n        for (int j = 0; j < n; j++) {\n            tmp.Add(0);\n        }\n        numList.Add(tmp);\n    }\n}\n
space_complexity.go
/* 二乗階 */\nfunc spaceQuadratic(n int) {\n    // 行列は O(n^2) の空間を使用する\n    numMatrix := make([][]int, n)\n    for i := 0; i < n; i++ {\n        numMatrix[i] = make([]int, n)\n    }\n}\n
space_complexity.swift
/* 二乗階 */\nfunc quadratic(n: Int) {\n    // 二次元リストは O(n^2) の空間を使用\n    let numList = Array(repeating: Array(repeating: 0, count: n), count: n)\n}\n
space_complexity.js
/* 二乗階 */\nfunction quadratic(n) {\n    // 行列は O(n^2) の空間を使用する\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 二次元リストは O(n^2) の空間を使用\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
space_complexity.ts
/* 二乗階 */\nfunction quadratic(n: number): void {\n    // 行列は O(n^2) の空間を使用する\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 二次元リストは O(n^2) の空間を使用\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
space_complexity.dart
/* 二乗階 */\nvoid quadratic(int n) {\n  // 行列は O(n^2) の空間を使用する\n  List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));\n  // 二次元リストは O(n^2) の空間を使用\n  List<List<int>> numList = [];\n  for (var i = 0; i < n; i++) {\n    List<int> tmp = [];\n    for (int j = 0; j < n; j++) {\n      tmp.add(0);\n    }\n    numList.add(tmp);\n  }\n}\n
space_complexity.rs
/* 二乗階 */\n#[allow(unused)]\nfn quadratic(n: i32) {\n    // 行列は O(n^2) の空間を使用する\n    let num_matrix = vec![vec![0; n as usize]; n as usize];\n    // 二次元リストは O(n^2) の空間を使用\n    let mut num_list = Vec::new();\n    for i in 0..n {\n        let mut tmp = Vec::new();\n        for j in 0..n {\n            tmp.push(0);\n        }\n        num_list.push(tmp);\n    }\n}\n
space_complexity.c
/* 二乗階 */\nvoid quadratic(int n) {\n    // 二次元リストは O(n^2) の空間を使用\n    int **numMatrix = malloc(sizeof(int *) * n);\n    for (int i = 0; i < n; i++) {\n        int *tmp = malloc(sizeof(int) * n);\n        for (int j = 0; j < n; j++) {\n            tmp[j] = 0;\n        }\n        numMatrix[i] = tmp;\n    }\n\n    // メモリを解放する\n    for (int i = 0; i < n; i++) {\n        free(numMatrix[i]);\n    }\n    free(numMatrix);\n}\n
space_complexity.kt
/* 二乗階 */\nfun quadratic(n: Int) {\n    // 行列は O(n^2) の空間を使用する\n    val numMatrix = arrayOfNulls<Array<Int>?>(n)\n    // 二次元リストは O(n^2) の空間を使用\n    val numList = mutableListOf<MutableList<Int>>()\n    for (i in 0..<n) {\n        val tmp = mutableListOf<Int>()\n        for (j in 0..<n) {\n            tmp.add(0)\n        }\n        numList.add(tmp)\n    }\n}\n
space_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  # 二次元リストは O(n^2) の空間を使用\n  Array.new(n) { Array.new(n, 0) }\nend\n
コードの可視化

全画面で見る >

以下の図に示すように、この関数の再帰の深さは \\(n\\) であり、各再帰関数の中で長さがそれぞれ \\(n\\)、\\(n-1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) の配列を初期化しています。平均長は \\(n / 2\\) なので、全体では \\(O(n^2)\\) の空間を占有します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def quadratic_recur(n: int) -> int:\n    \"\"\"二次時間(再帰実装)\"\"\"\n    if n <= 0:\n        return 0\n    # 配列 nums の長さは n, n-1, ..., 2, 1\n    nums = [0] * n\n    return quadratic_recur(n - 1)\n
space_complexity.cpp
/* 二次時間(再帰実装) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    vector<int> nums(n);\n    cout << \"再帰 n = \" << n << \" における nums の長さ = \" << nums.size() << endl;\n    return quadraticRecur(n - 1);\n}\n
space_complexity.java
/* 二次時間(再帰実装) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    // 配列 nums の長さは n, n-1, ..., 2, 1\n    int[] nums = new int[n];\n    System.out.println(\"再帰 n = \" + n + \" における nums の長さ = \" + nums.length);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.cs
/* 二次時間(再帰実装) */\nint QuadraticRecur(int n) {\n    if (n <= 0) return 0;\n    int[] nums = new int[n];\n    Console.WriteLine(\"再帰 n = \" + n + \" における nums の長さ = \" + nums.Length);\n    return QuadraticRecur(n - 1);\n}\n
space_complexity.go
/* 二次時間(再帰実装) */\nfunc spaceQuadraticRecur(n int) int {\n    if n <= 0 {\n        return 0\n    }\n    nums := make([]int, n)\n    fmt.Printf(\"再帰 n = %d における nums の長さ = %d \\n\", n, len(nums))\n    return spaceQuadraticRecur(n - 1)\n}\n
space_complexity.swift
/* 二次時間(再帰実装) */\n@discardableResult\nfunc quadraticRecur(n: Int) -> Int {\n    if n <= 0 {\n        return 0\n    }\n    // 配列 nums の長さは n, n-1, ..., 2, 1\n    let nums = Array(repeating: 0, count: n)\n    print(\"再帰 n = \\(n) における nums の長さ = \\(nums.count)\")\n    return quadraticRecur(n: n - 1)\n}\n
space_complexity.js
/* 二次時間(再帰実装) */\nfunction quadraticRecur(n) {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`再帰 n = ${n} における nums の長さ = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.ts
/* 二次時間(再帰実装) */\nfunction quadraticRecur(n: number): number {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`再帰 n = ${n} における nums の長さ = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.dart
/* 二次時間(再帰実装) */\nint quadraticRecur(int n) {\n  if (n <= 0) return 0;\n  List<int> nums = List.filled(n, 0);\n  print('再帰 n = $n における nums の長さ = ${nums.length}');\n  return quadraticRecur(n - 1);\n}\n
space_complexity.rs
/* 二次時間(再帰実装) */\nfn quadratic_recur(n: i32) -> i32 {\n    if n <= 0 {\n        return 0;\n    };\n    // 配列 nums の長さは n, n-1, ..., 2, 1\n    let nums = vec![0; n as usize];\n    println!(\"再帰 n = {} における nums の長さ = {}\", n, nums.len());\n    return quadratic_recur(n - 1);\n}\n
space_complexity.c
/* 二次時間(再帰実装) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    int *nums = malloc(sizeof(int) * n);\n    printf(\"再帰 n = %d における nums の長さ = %d\\r\\n\", n, n);\n    int res = quadraticRecur(n - 1);\n    free(nums);\n    return res;\n}\n
space_complexity.kt
/* 二次時間(再帰実装) */\ntailrec fun quadraticRecur(n: Int): Int {\n    if (n <= 0)\n        return 0\n    // 配列 nums の長さは n, n-1, ..., 2, 1\n    val nums = Array(n) { 0 }\n    println(\"再帰 n = $n における nums の長さ = ${nums.size}\")\n    return quadraticRecur(n - 1)\n}\n
space_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  # 二次元リストは O(n^2) の空間を使用\n  Array.new(n) { Array.new(n, 0) }\nend\n\n# ## 平方階(再帰実装)###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # 配列 nums の長さは n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n
コードの可視化

全画面で見る >

図 2-18   再帰関数が生み出す平方階の空間計算量

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#4-o2n","level":3,"title":"4.   指数階 \\(O(2^n)\\)","text":"

指数階は二分木によく現れます。以下の図を見ると、高さが \\(n\\) の「満二分木」のノード数は \\(2^n - 1\\) であり、\\(O(2^n)\\) の空間を占有します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def build_tree(n: int) -> TreeNode | None:\n    \"\"\"指数時間(完全二分木の構築)\"\"\"\n    if n == 0:\n        return None\n    root = TreeNode(0)\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n    return root\n
space_complexity.cpp
/* 指数時間(完全二分木の構築) */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return nullptr;\n    TreeNode *root = new TreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.java
/* 指数時間(完全二分木の構築) */\nTreeNode buildTree(int n) {\n    if (n == 0)\n        return null;\n    TreeNode root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.cs
/* 指数時間(完全二分木の構築) */\nTreeNode? BuildTree(int n) {\n    if (n == 0) return null;\n    TreeNode root = new(0) {\n        left = BuildTree(n - 1),\n        right = BuildTree(n - 1)\n    };\n    return root;\n}\n
space_complexity.go
/* 指数時間(完全二分木の構築) */\nfunc buildTree(n int) *TreeNode {\n    if n == 0 {\n        return nil\n    }\n    root := NewTreeNode(0)\n    root.Left = buildTree(n - 1)\n    root.Right = buildTree(n - 1)\n    return root\n}\n
space_complexity.swift
/* 指数時間(完全二分木の構築) */\nfunc buildTree(n: Int) -> TreeNode? {\n    if n == 0 {\n        return nil\n    }\n    let root = TreeNode(x: 0)\n    root.left = buildTree(n: n - 1)\n    root.right = buildTree(n: n - 1)\n    return root\n}\n
space_complexity.js
/* 指数時間(完全二分木の構築) */\nfunction buildTree(n) {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.ts
/* 指数時間(完全二分木の構築) */\nfunction buildTree(n: number): TreeNode | null {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.dart
/* 指数時間(完全二分木の構築) */\nTreeNode? buildTree(int n) {\n  if (n == 0) return null;\n  TreeNode root = TreeNode(0);\n  root.left = buildTree(n - 1);\n  root.right = buildTree(n - 1);\n  return root;\n}\n
space_complexity.rs
/* 指数時間(完全二分木の構築) */\nfn build_tree(n: i32) -> Option<Rc<RefCell<TreeNode>>> {\n    if n == 0 {\n        return None;\n    };\n    let root = TreeNode::new(0);\n    root.borrow_mut().left = build_tree(n - 1);\n    root.borrow_mut().right = build_tree(n - 1);\n    return Some(root);\n}\n
space_complexity.c
/* 指数時間(完全二分木の構築) */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return NULL;\n    TreeNode *root = newTreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.kt
/* 指数時間(完全二分木の構築) */\nfun buildTree(n: Int): TreeNode? {\n    if (n == 0)\n        return null\n    val root = TreeNode(0)\n    root.left = buildTree(n - 1)\n    root.right = buildTree(n - 1)\n    return root\n}\n
space_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  # 二次元リストは O(n^2) の空間を使用\n  Array.new(n) { Array.new(n, 0) }\nend\n\n# ## 平方階(再帰実装)###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # 配列 nums の長さは n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n\n# ## 指数階(満二分木を構築)###\ndef build_tree(n)\n  return if n == 0\n\n  TreeNode.new.tap do |root|\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n  end\nend\n
コードの可視化

全画面で見る >

図 2-19   満二分木が生み出す指数階の空間計算量

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#5-olog-n","level":3,"title":"5.   対数階 \\(O(\\log n)\\)","text":"

対数階は分割統治アルゴリズムによく現れます。例えばマージソートでは、長さ \\(n\\) の配列を入力として、各再帰で配列を中央から二つに分割するため、高さ \\(\\log n\\) の再帰木が形成され、\\(O(\\log n)\\) のスタックフレーム空間を使用します。

また、数値を文字列に変換する場合を考えると、正の整数 \\(n\\) の桁数は \\(\\lfloor \\log_{10} n \\rfloor + 1\\) であり、対応する文字列長も \\(\\lfloor \\log_{10} n \\rfloor + 1\\) です。したがって空間計算量は \\(O(\\log_{10} n + 1) = O(\\log n)\\) となります。

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#244","level":2,"title":"2.4.4   時間と空間のトレードオフ","text":"

理想的には、アルゴリズムの時間計算量と空間計算量の両方を最適にしたいところです。しかし実際には、この二つを同時に最適化するのは通常きわめて困難です。

時間計算量を下げるには、通常、空間計算量を増やす代償が必要であり、その逆も同様です。メモリ空間を犠牲にして実行速度を上げる考え方を「空間を時間と引き換えにする」と呼び、その逆を「時間を空間と引き換えにする」と呼びます。

どちらの考え方を選ぶかは、何をより重視するかによって決まります。多くの場合、空間より時間のほうが貴重なので、「空間を時間と引き換えにする」戦略のほうが一般的です。もちろん、データ量が非常に大きい場合には、空間計算量を抑えることも同じくらい重要です。

","path":["第 2 章   計算量解析","2.4   空間計算量"],"tags":[]},{"location":"chapter_computational_complexity/summary/","level":1,"title":"2.5   まとめ","text":"","path":["第 2 章   計算量解析","2.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/summary/#1","level":3,"title":"1.   要点の振り返り","text":"

アルゴリズム効率の評価

  • 時間効率と空間効率は、アルゴリズムの良し悪しを測る二つの主要な評価指標です。
  • 実測によってアルゴリズム効率を評価できますが、テスト環境の影響を排除しにくく、多くの計算資源も消費します。
  • 複雑度分析は実測の欠点を補い、分析結果はすべての実行プラットフォームに適用でき、データ規模ごとの効率も明らかにできます。

時間計算量

  • 時間計算量は、アルゴリズムの実行時間がデータ量の増加に伴ってどう変化するかを測るためのものであり、効率評価に有効です。ただし、入力データ量が小さい場合や時間計算量が同じ場合などには、効率の優劣を正確に比較できないことがあります。
  • 最悪時間計算量はビッグオー記法 \\(O\\) で表され、関数の漸近上界に対応し、\\(n\\) が正の無限大に近づくときの操作回数 \\(T(n)\\) の増加の度合いを表します。
  • 時間計算量の推定は二段階に分かれ、まず操作回数を数え、次に漸近上界を判断します。
  • 一般的な時間計算量を低い順から並べると、\\(O(1)\\)、\\(O(\\log n)\\)、\\(O(n)\\)、\\(O(n \\log n)\\)、\\(O(n^2)\\)、\\(O(2^n)\\)、\\(O(n!)\\) などがあります。
  • 一部のアルゴリズムの時間計算量は固定ではなく、入力データの分布に関係します。時間計算量には最悪、最良、平均時間計算量がありますが、最良時間計算量は入力データが厳しい条件を満たす必要があるため、ほとんど使われません。
  • 平均時間計算量は、ランダムな入力データに対するアルゴリズムの実行効率を表し、実運用時の性能に最も近い指標です。平均時間計算量を求めるには、入力データの分布と、それを踏まえた数学的期待値を統計する必要があります。

空間計算量

  • 空間計算量の役割は時間計算量に似ており、アルゴリズムが使用するメモリ空間がデータ量の増加に伴ってどう変化するかを測ります。
  • アルゴリズム実行中に関係するメモリ空間は、入力空間、一時空間、出力空間に分けられます。通常、入力空間は空間計算量の計算に含めません。一時空間は一時データ、スタックフレーム空間、命令空間に分けられ、このうちスタックフレーム空間は通常、再帰関数でのみ空間計算量に影響します。
  • 私たちは通常、最悪空間計算量のみに注目し、最悪の入力データと最悪の実行時点における空間計算量を数えます。
  • 一般的な空間計算量を低い順から並べると、\\(O(1)\\)、\\(O(\\log n)\\)、\\(O(n)\\)、\\(O(n^2)\\)、\\(O(2^n)\\) などがあります。
","path":["第 2 章   計算量解析","2.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:尾再帰の空間計算量は \\(O(1)\\) ですか?

理論上、尾再帰関数の空間計算量は \\(O(1)\\) まで最適化できます。ただし、ほとんどのプログラミング言語(Java、Python、C++、Go、C# など)は尾再帰の自動最適化をサポートしていないため、通常は空間計算量を \\(O(n)\\) と見なします。

Q:関数とメソッドという二つの用語の違いは何ですか?

関数(function)は独立して実行でき、すべての引数は明示的に渡されます。メソッド(method)はオブジェクトに関連付けられ、それを呼び出すオブジェクトが暗黙的に渡され、クラスのインスタンスに含まれるデータを操作できます。

以下では、いくつかの一般的なプログラミング言語を例に説明します。

  • C 言語は手続き型プログラミング言語であり、オブジェクト指向の概念がないため、関数しかありません。ただし、構造体(struct)を作成してオブジェクト指向プログラミングを模倣でき、構造体に関連付けられた関数は、他のプログラミング言語におけるメソッドに相当します。
  • Java と C# はオブジェクト指向のプログラミング言語であり、コードブロック(メソッド)は通常あるクラスの一部です。静的メソッドの振る舞いは関数に似ており、クラスに束縛され、特定のインスタンス変数にはアクセスできません。
  • C++ と Python は、手続き型プログラミング(関数)にもオブジェクト指向プログラミング(メソッド)にも対応しています。

Q:「一般的な空間計算量の種類」の図が表しているのは、使用空間の絶対量ですか?

いいえ。この図が示しているのは空間計算量であり、表しているのは増加傾向であって、使用空間の絶対量ではありません。

\\(n = 8\\) と仮定すると、各曲線の値が対応する関数と一致していないように見えるかもしれません。これは、各曲線に定数項が含まれており、値の範囲を視覚的に見やすい範囲へ圧縮しているためです。

実際には、各手法の「定数項」の複雑度がどれほどか通常は分からないため、一般に複雑度だけを根拠に \\(n = 8\\) 以下で最適解を選ぶことはできません。ただし、\\(n = 8^5\\) であれば選びやすく、このときは増加傾向がすでに支配的になっています。

Q 実際の利用場面に応じて、時間(または空間)を犠牲にしてアルゴリズムを設計することはありますか?

実際の応用では、多くの場合、空間を犠牲にして時間を得る選択をします。たとえばデータベースのインデックスでは、通常 B+ 木やハッシュインデックスを構築し、大量のメモリ空間を使う代わりに、\\(O(\\log n)\\) あるいは \\(O(1)\\) の高速な検索を実現します。

空間資源が貴重な場面では、時間を犠牲にして空間を得ることもあります。たとえば組み込み開発では、デバイスのメモリが非常に貴重なため、エンジニアはハッシュテーブルの使用をやめ、配列による順次探索を選んでメモリ使用量を節約することがあります。その代償として探索は遅くなります。

","path":["第 2 章   計算量解析","2.5   まとめ"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   時間計算量","text":"

実行時間はアルゴリズムの効率を直感的かつ正確に反映します。あるコードの実行時間を正確に見積もりたい場合、どのようにすればよいでしょうか?

  1. 実行プラットフォームを特定する。ハードウェア構成、プログラミング言語、システム環境などが含まれ、これらの要因はいずれもコードの実行効率に影響します。
  2. 各種計算操作に必要な実行時間を評価する。例えば加算 + には 1 ns 、乗算 * には 10 ns 、出力 print() には 5 ns などが必要です。
  3. コード中のすべての計算操作を数える。そして各操作の実行時間を合計することで、実行時間を得ます。

例えば次のコードでは、入力データサイズを \\(n\\) とします:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# ある実行プラットフォーム上で\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # n 回ループ\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
// ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // n 回ループ\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // n 回ループ\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // n 回ループ\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // n 回ループ\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // n 回ループ\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
// ある実行プラットフォーム上で\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // n 回ループ\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // n 回ループ\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
// ある実行プラットフォーム上で\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // n 回ループ\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
# ある実行プラットフォーム上で\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # n 回ループ\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

上記の方法に基づくと、アルゴリズムの実行時間は \\((6n + 12)\\) ns になります:

\\[ 1 + 1 + 10 + (1 + 5) \\times n = 6n + 12 \\]

しかし実際には、アルゴリズムの実行時間を統計的に求めることは合理的でも現実的でもありません。まず、見積もり時間を実行プラットフォームに縛りたくありません。アルゴリズムはさまざまな異なるプラットフォームで動作する必要があるからです。次に、各種操作の実行時間を知ること自体が難しく、見積もりの難易度を大きく引き上げます。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#231","level":2,"title":"2.3.1   実行時間の増加傾向を捉える","text":"

時間計算量の分析で扱うのはアルゴリズムの実行時間そのものではなく、**データ量が増えたときに実行時間がどう増加するかという傾向**です。

「実行時間の増加傾向」という概念はやや抽象的なので、例を通して理解してみましょう。入力データサイズを \\(n\\) とし、3 つのアルゴリズム ABC を考えます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# アルゴリズム A の時間計算量:定数階\ndef algorithm_A(n: int):\n    print(0)\n# アルゴリズム B の時間計算量:線形階\ndef algorithm_B(n: int):\n    for _ in range(n):\n        print(0)\n# アルゴリズム C の時間計算量:定数階\ndef algorithm_C(n: int):\n    for _ in range(1000000):\n        print(0)\n
// アルゴリズム A の時間計算量:定数階\nvoid algorithm_A(int n) {\n    cout << 0 << endl;\n}\n// アルゴリズム B の時間計算量:線形階\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        cout << 0 << endl;\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        cout << 0 << endl;\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nvoid algorithm_A(int n) {\n    System.out.println(0);\n}\n// アルゴリズム B の時間計算量:線形階\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        System.out.println(0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        System.out.println(0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nvoid AlgorithmA(int n) {\n    Console.WriteLine(0);\n}\n// アルゴリズム B の時間計算量:線形階\nvoid AlgorithmB(int n) {\n    for (int i = 0; i < n; i++) {\n        Console.WriteLine(0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nvoid AlgorithmC(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        Console.WriteLine(0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nfunc algorithm_A(n int) {\n    fmt.Println(0)\n}\n// アルゴリズム B の時間計算量:線形階\nfunc algorithm_B(n int) {\n    for i := 0; i < n; i++ {\n        fmt.Println(0)\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nfunc algorithm_C(n int) {\n    for i := 0; i < 1000000; i++ {\n        fmt.Println(0)\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nfunc algorithmA(n: Int) {\n    print(0)\n}\n\n// アルゴリズム B の時間計算量:線形階\nfunc algorithmB(n: Int) {\n    for _ in 0 ..< n {\n        print(0)\n    }\n}\n\n// アルゴリズム C の時間計算量:定数階\nfunc algorithmC(n: Int) {\n    for _ in 0 ..< 1_000_000 {\n        print(0)\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nfunction algorithm_A(n) {\n    console.log(0);\n}\n// アルゴリズム B の時間計算量:線形階\nfunction algorithm_B(n) {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nfunction algorithm_C(n) {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nfunction algorithm_A(n: number): void {\n    console.log(0);\n}\n// アルゴリズム B の時間計算量:線形階\nfunction algorithm_B(n: number): void {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nfunction algorithm_C(n: number): void {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nvoid algorithmA(int n) {\n  print(0);\n}\n// アルゴリズム B の時間計算量:線形階\nvoid algorithmB(int n) {\n  for (int i = 0; i < n; i++) {\n    print(0);\n  }\n}\n// アルゴリズム C の時間計算量:定数階\nvoid algorithmC(int n) {\n  for (int i = 0; i < 1000000; i++) {\n    print(0);\n  }\n}\n
// アルゴリズム A の時間計算量:定数階\nfn algorithm_A(n: i32) {\n    println!(\"{}\", 0);\n}\n// アルゴリズム B の時間計算量:線形階\nfn algorithm_B(n: i32) {\n    for _ in 0..n {\n        println!(\"{}\", 0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nfn algorithm_C(n: i32) {\n    for _ in 0..1000000 {\n        println!(\"{}\", 0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nvoid algorithm_A(int n) {\n    printf(\"%d\", 0);\n}\n// アルゴリズム B の時間計算量:線形階\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        printf(\"%d\", 0);\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        printf(\"%d\", 0);\n    }\n}\n
// アルゴリズム A の時間計算量:定数階\nfun algoritm_A(n: Int) {\n    println(0)\n}\n// アルゴリズム B の時間計算量:線形階\nfun algorithm_B(n: Int) {\n    for (i in 0..<n){\n        println(0)\n    }\n}\n// アルゴリズム C の時間計算量:定数階\nfun algorithm_C(n: Int) {\n    for (i in 0..<1000000) {\n        println(0)\n    }\n}\n
# アルゴリズム A の時間計算量:定数階\ndef algorithm_A(n)\n    puts 0\nend\n\n# アルゴリズム B の時間計算量:線形階\ndef algorithm_B(n)\n    (0...n).each { puts 0 }\nend\n\n# アルゴリズム C の時間計算量:定数階\ndef algorithm_C(n)\n    (0...1_000_000).each { puts 0 }\nend\n

以下の図は、上記 3 つのアルゴリズム関数の時間計算量を示しています。

  • アルゴリズム A には出力操作が \\(1\\) 回しかなく、実行時間は \\(n\\) が大きくなっても増加しません。このアルゴリズムの時間計算量を「定数階」と呼びます。
  • アルゴリズム B の出力操作は \\(n\\) 回ループする必要があり、実行時間は \\(n\\) の増加に対して線形に増加します。このアルゴリズムの時間計算量は「線形階」と呼ばれます。
  • アルゴリズム C の出力操作は \\(1000000\\) 回ループする必要があり、実行時間は長いものの、入力データサイズ \\(n\\) とは無関係です。したがって C の時間計算量は A と同じく、依然として「定数階」です。

図 2-7   アルゴリズム A、B、C の時間増加傾向

アルゴリズムの実行時間を直接数える方法と比べて、時間計算量分析にはどのような特徴があるでしょうか?

  • 時間計算量はアルゴリズム効率を有効に評価できます。例えばアルゴリズム B の実行時間は線形に増加するため、\\(n > 1\\) ではアルゴリズム A より遅く、\\(n > 1000000\\) ではアルゴリズム C より遅くなります。実際、入力データサイズ \\(n\\) が十分に大きければ、「定数階」のアルゴリズムは必ず「線形階」のアルゴリズムより優れます。これが実行時間の増加傾向の意味です。
  • 時間計算量の見積もり方法はより簡潔です。実行プラットフォームや計算操作の種類は、アルゴリズム実行時間の増加傾向とは無関係です。そのため時間計算量分析では、すべての計算操作の実行時間を同じ「単位時間」とみなしてよく、「計算操作の実行時間を数える」作業を「計算操作の個数を数える」作業へ簡略化できます。これにより見積もりの難易度は大きく下がります。
  • 時間計算量には一定の限界もあります。例えばアルゴリズム AC の時間計算量は同じでも、実際の実行時間には大きな差があります。同様に、アルゴリズム B の時間計算量は C より高いものの、入力データサイズ \\(n\\) が小さい場合にはアルゴリズム B のほうが明らかに優れます。このような場合、時間計算量だけでアルゴリズム効率の高低を判断するのは難しいことがあります。もっとも、こうした問題があっても、複雑度分析は依然としてアルゴリズム効率を評価する最も有効で一般的な方法です。
","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#232","level":2,"title":"2.3.2   関数の漸近上界","text":"

入力サイズが \\(n\\) の次の関数を考えます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 1      # +1\n    a = a + 1  # +1\n    a = a * 2  # +1\n    # n 回ループ\n    for i in range(n):  # +1\n        print(0)        # +1\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // n 回ループ\n    for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行)\n        cout << 0 << endl;    // +1\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // n 回ループ\n    for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行)\n        System.out.println(0);    // +1\n    }\n}\n
void Algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // n 回ループ\n    for (int i = 0; i < n; i++) {   // +1(各反復で i ++ を実行)\n        Console.WriteLine(0);   // +1\n    }\n}\n
func algorithm(n int) {\n    a := 1      // +1\n    a = a + 1   // +1\n    a = a * 2   // +1\n    // n 回ループ\n    for i := 0; i < n; i++ {   // +1\n        fmt.Println(a)         // +1\n    }\n}\n
func algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // n 回ループ\n    for _ in 0 ..< n { // +1\n        print(0) // +1\n    }\n}\n
function algorithm(n) {\n    var a = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // n 回ループ\n    for(let i = 0; i < n; i++){ // +1(各反復で i ++ を実行)\n        console.log(0); // +1\n    }\n}\n
function algorithm(n: number): void{\n    var a: number = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // n 回ループ\n    for(let i = 0; i < n; i++){ // +1(各反復で i ++ を実行)\n        console.log(0); // +1\n    }\n}\n
void algorithm(int n) {\n  int a = 1; // +1\n  a = a + 1; // +1\n  a = a * 2; // +1\n  // n 回ループ\n  for (int i = 0; i < n; i++) { // +1(各反復で i ++ を実行)\n    print(0); // +1\n  }\n}\n
fn algorithm(n: i32) {\n    let mut a = 1;   // +1\n    a = a + 1;      // +1\n    a = a * 2;      // +1\n\n    // n 回ループ\n    for _ in 0..n { // +1(各反復で i ++ を実行)\n        println!(\"{}\", 0); // +1\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // n 回ループ\n    for (int i = 0; i < n; i++) {   // +1(各反復で i ++ を実行)\n        printf(\"%d\", 0);            // +1\n    }\n}\n
fun algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // n 回ループ\n    for (i in 0..<n) { // +1(各反復で i ++ を実行)\n        println(0) // +1\n    }\n}\n
def algorithm(n)\n    a = 1       # +1\n    a = a + 1   # +1\n    a = a * 2   # +1\n    # n 回ループ\n    (0...n).each do # +1\n        puts 0      # +1\n    end\nend\n

アルゴリズムの操作回数を入力データサイズ \\(n\\) の関数とし、\\(T(n)\\) と表すと、上の関数の操作回数は次のようになります:

\\[ T(n) = 3 + 2n \\]

\\(T(n)\\) は一次関数であり、実行時間の増加傾向が線形であることを示しています。したがってその時間計算量は線形階です。

線形階の時間計算量を \\(O(n)\\) と表します。この数学記号はビッグ \\(O\\) 記法(big-\\(O\\) notation)と呼ばれ、関数 \\(T(n)\\) の漸近上界(asymptotic upper bound)を表します。

時間計算量の分析は本質的に「操作回数 \\(T(n)\\)」の漸近上界を求めることであり、明確な数学的定義があります。

関数の漸近上界

正の実数 \\(c\\) と実数 \\(n_0\\) が存在し、すべての \\(n > n_0\\) について \\(T(n) \\leq c \\cdot f(n)\\) が成り立つならば、\\(f(n)\\) は \\(T(n)\\) の漸近上界の 1 つであるとみなせます。これを \\(T(n) = O(f(n))\\) と記します。

下図のように、漸近上界を求めるとは関数 \\(f(n)\\) を探すことであり、\\(n\\) が無限大へ近づくときに \\(T(n)\\) と \\(f(n)\\) が同じ増加オーダーにあり、定数係数 \\(c\\) だけが異なる状態を表します。

図 2-8   関数の漸近上界

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#233","level":2,"title":"2.3.3   求め方","text":"

漸近上界はやや数学色が強い概念ですが、完全に理解できていなくても心配はいりません。まずは求め方を押さえ、実践を重ねる中で徐々にその数学的意味をつかめば十分です。

定義より、\\(f(n)\\) が定まれば時間計算量 \\(O(f(n))\\) が得られます。では、漸近上界 \\(f(n)\\) をどのように決めればよいのでしょうか。大きく 2 段階あります。まず操作回数を数え、その後で漸近上界を判断します。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-1","level":3,"title":"1.   第 1 ステップ:操作回数を数える","text":"

コードについては、上から下へ 1 行ずつ数えれば十分です。しかし、前述の \\(c \\cdot f(n)\\) における定数係数 \\(c\\) は任意に大きく取れるため、操作回数 \\(T(n)\\) に含まれるさまざまな係数や定数項は無視できます。この原則から、次のような簡略化のコツが得られます。

  1. \\(T(n)\\) 中の定数を無視する。それらはすべて \\(n\\) と無関係なので、時間計算量には影響しません。
  2. すべての係数を省略する。例えば \\(2n\\) 回や \\(5n + 1\\) 回のループは、いずれも \\(n\\) 回と簡略化できます。\\(n\\) の前の係数は時間計算量に影響しないからです。
  3. ループが入れ子のときは乗算を使う。総操作回数は外側のループと内側のループの操作回数の積に等しく、各ループ層には引き続き 1.2. のコツをそれぞれ適用できます。

次の関数では、上記のコツを使って操作回数を数えられます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 1      # +0(コツ 1)\n    a = a + n  # +0(コツ 1)\n    # +n(コツ 2)\n    for i in range(5 * n + 1):\n        print(0)\n    # +n*n(コツ 3)\n    for i in range(2 * n):\n        for j in range(n + 1):\n            print(0)\n
void algorithm(int n) {\n    int a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        cout << 0 << endl;\n    }\n    // +n*n(コツ 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            cout << 0 << endl;\n        }\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        System.out.println(0);\n    }\n    // +n*n(コツ 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            System.out.println(0);\n        }\n    }\n}\n
void Algorithm(int n) {\n    int a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        Console.WriteLine(0);\n    }\n    // +n*n(コツ 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            Console.WriteLine(0);\n        }\n    }\n}\n
func algorithm(n int) {\n    a := 1     // +0(コツ 1)\n    a = a + n  // +0(コツ 1)\n    // +n(コツ 2)\n    for i := 0; i < 5 * n + 1; i++ {\n        fmt.Println(0)\n    }\n    // +n*n(コツ 3)\n    for i := 0; i < 2 * n; i++ {\n        for j := 0; j < n + 1; j++ {\n            fmt.Println(0)\n        }\n    }\n}\n
func algorithm(n: Int) {\n    var a = 1 // +0(コツ 1)\n    a = a + n // +0(コツ 1)\n    // +n(コツ 2)\n    for _ in 0 ..< (5 * n + 1) {\n        print(0)\n    }\n    // +n*n(コツ 3)\n    for _ in 0 ..< (2 * n) {\n        for _ in 0 ..< (n + 1) {\n            print(0)\n        }\n    }\n}\n
function algorithm(n) {\n    let a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n(コツ 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
function algorithm(n: number): void {\n    let a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n(コツ 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
void algorithm(int n) {\n  int a = 1; // +0(コツ 1)\n  a = a + n; // +0(コツ 1)\n  // +n(コツ 2)\n  for (int i = 0; i < 5 * n + 1; i++) {\n    print(0);\n  }\n  // +n*n(コツ 3)\n  for (int i = 0; i < 2 * n; i++) {\n    for (int j = 0; j < n + 1; j++) {\n      print(0);\n    }\n  }\n}\n
fn algorithm(n: i32) {\n    let mut a = 1;     // +0(コツ 1)\n    a = a + n;        // +0(コツ 1)\n\n    // +n(コツ 2)\n    for i in 0..(5 * n + 1) {\n        println!(\"{}\", 0);\n    }\n\n    // +n*n(コツ 3)\n    for i in 0..(2 * n) {\n        for j in 0..(n + 1) {\n            println!(\"{}\", 0);\n        }\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +0(コツ 1)\n    a = a + n;  // +0(コツ 1)\n    // +n(コツ 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        printf(\"%d\", 0);\n    }\n    // +n*n(コツ 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            printf(\"%d\", 0);\n        }\n    }\n}\n
fun algorithm(n: Int) {\n    var a = 1   // +0(コツ 1)\n    a = a + n   // +0(コツ 1)\n    // +n(コツ 2)\n    for (i in 0..<5 * n + 1) {\n        println(0)\n    }\n    // +n*n(コツ 3)\n    for (i in 0..<2 * n) {\n        for (j in 0..<n + 1) {\n            println(0)\n        }\n    }\n}\n
def algorithm(n)\n    a = 1       # +0(コツ 1)\n    a = a + n   # +0(コツ 1)\n    # +n(コツ 2)\n    (0...(5 * n + 1)).each do { puts 0 }\n    # +n*n(コツ 3)\n    (0...(2 * n)).each do\n        (0...(n + 1)).each do { puts 0 }\n    end\nend\n

次の式は、上記のコツを使う前後の集計結果を示したもので、どちらから求めても時間計算量は \\(O(n^2)\\) です。

\\[ \\begin{aligned} T(n) & = 2n(n + 1) + (5n + 1) + 2 & \\text{厳密集計 (-.-|||)} \\newline & = 2n^2 + 7n + 3 \\newline T(n) & = n^2 + n & \\text{手抜き集計 (o.O)} \\end{aligned} \\]","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-2","level":3,"title":"2.   第 2 ステップ:漸近上界を判断する","text":"

時間計算量は \\(T(n)\\) の最高次の項によって決まります。これは、\\(n\\) が無限大に近づくとき、最高次の項が支配的となり、他の項の影響は無視できるからです。

以下の表はその例です。いくつか極端な値を入れているのは、「係数では次数は変わらない」という結論を強調するためです。\\(n\\) が無限大に近づくと、これらの定数は重要でなくなります。

表 2-2   異なる操作回数に対応する時間計算量

操作回数 \\(T(n)\\) 時間計算量 \\(O(f(n))\\) \\(100000\\) \\(O(1)\\) \\(3n + 2\\) \\(O(n)\\) \\(2n^2 + 3n + 2\\) \\(O(n^2)\\) \\(n^3 + 10000n^2\\) \\(O(n^3)\\) \\(2^n + 10000n^{10000}\\) \\(O(2^n)\\)","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#234","level":2,"title":"2.3.4   よくある種類","text":"

入力データサイズを \\(n\\) とすると、よくある時間計算量の種類は次図のとおりです(小さい順に並べています)。

\\[ \\begin{aligned} O(1) < O(\\log n) < O(n) < O(n \\log n) < O(n^2) < O(2^n) < O(n!) \\newline \\text{定数階} < \\text{対数階} < \\text{線形階} < \\text{線形対数階} < \\text{平方階} < \\text{指数階} < \\text{階乗階} \\end{aligned} \\]

図 2-9   よくある時間計算量の種類

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-o1","level":3,"title":"1.   定数階 \\(O(1)\\)","text":"

定数階の操作回数は入力データサイズ \\(n\\) と無関係であり、\\(n\\) が変化しても増減しません。

次の関数では、操作回数 size が大きい可能性はありますが、入力データサイズ \\(n\\) とは無関係なので、時間計算量は依然として \\(O(1)\\) です:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def constant(n: int) -> int:\n    \"\"\"定数階\"\"\"\n    count = 0\n    size = 100000\n    for _ in range(size):\n        count += 1\n    return count\n
time_complexity.cpp
/* 定数階 */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.java
/* 定数階 */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.cs
/* 定数階 */\nint Constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.go
/* 定数階 */\nfunc constant(n int) int {\n    count := 0\n    size := 100000\n    for i := 0; i < size; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 定数階 */\nfunc constant(n: Int) -> Int {\n    var count = 0\n    let size = 100_000\n    for _ in 0 ..< size {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 定数階 */\nfunction constant(n) {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
time_complexity.ts
/* 定数階 */\nfunction constant(n: number): number {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
time_complexity.dart
/* 定数階 */\nint constant(int n) {\n  int count = 0;\n  int size = 100000;\n  for (var i = 0; i < size; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 定数階 */\nfn constant(n: i32) -> i32 {\n    _ = n;\n    let mut count = 0;\n    let size = 100_000;\n    for _ in 0..size {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 定数階 */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    int i = 0;\n    for (int i = 0; i < size; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 定数階 */\nfun constant(n: Int): Int {\n    var count = 0\n    val size = 100000\n    for (i in 0..<size)\n        count++\n    return count\n}\n
time_complexity.rb
### 定数階 ###\ndef constant(n)\n  count = 0\n  size = 100000\n\n  (0...size).each { count += 1 }\n\n  count\nend\n
コードの可視化

全画面で見る >

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-on","level":3,"title":"2.   線形階 \\(O(n)\\)","text":"

線形階の操作回数は入力データサイズ \\(n\\) に対して線形に増加します。線形階は通常、単一ループに現れます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def linear(n: int) -> int:\n    \"\"\"線形階\"\"\"\n    count = 0\n    for _ in range(n):\n        count += 1\n    return count\n
time_complexity.cpp
/* 線形階 */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.java
/* 線形階 */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.cs
/* 線形階 */\nint Linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.go
/* 線形階 */\nfunc linear(n int) int {\n    count := 0\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 線形階 */\nfunc linear(n: Int) -> Int {\n    var count = 0\n    for _ in 0 ..< n {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 線形階 */\nfunction linear(n) {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
time_complexity.ts
/* 線形階 */\nfunction linear(n: number): number {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
time_complexity.dart
/* 線形階 */\nint linear(int n) {\n  int count = 0;\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 線形階 */\nfn linear(n: i32) -> i32 {\n    let mut count = 0;\n    for _ in 0..n {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 線形階 */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 線形階 */\nfun linear(n: Int): Int {\n    var count = 0\n    for (i in 0..<n)\n        count++\n    return count\n}\n
time_complexity.rb
### 線形階 ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n
コードの可視化

全画面で見る >

配列走査や連結リスト走査などの操作の時間計算量はいずれも \\(O(n)\\) であり、ここでの \\(n\\) は配列または連結リストの長さです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def array_traversal(nums: list[int]) -> int:\n    \"\"\"線形時間(配列を走査)\"\"\"\n    count = 0\n    # ループ回数は配列長に比例する\n    for num in nums:\n        count += 1\n    return count\n
time_complexity.cpp
/* 線形時間(配列を走査) */\nint arrayTraversal(vector<int> &nums) {\n    int count = 0;\n    // ループ回数は配列長に比例する\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* 線形時間(配列を走査) */\nint arrayTraversal(int[] nums) {\n    int count = 0;\n    // ループ回数は配列長に比例する\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* 線形時間(配列を走査) */\nint ArrayTraversal(int[] nums) {\n    int count = 0;\n    // ループ回数は配列長に比例する\n    foreach (int num in nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* 線形時間(配列を走査) */\nfunc arrayTraversal(nums []int) int {\n    count := 0\n    // ループ回数は配列長に比例する\n    for range nums {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 線形時間(配列を走査) */\nfunc arrayTraversal(nums: [Int]) -> Int {\n    var count = 0\n    // ループ回数は配列長に比例する\n    for _ in nums {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 線形時間(配列を走査) */\nfunction arrayTraversal(nums) {\n    let count = 0;\n    // ループ回数は配列長に比例する\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* 線形時間(配列を走査) */\nfunction arrayTraversal(nums: number[]): number {\n    let count = 0;\n    // ループ回数は配列長に比例する\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* 線形時間(配列を走査) */\nint arrayTraversal(List<int> nums) {\n  int count = 0;\n  // ループ回数は配列長に比例する\n  for (var _num in nums) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 線形時間(配列を走査) */\nfn array_traversal(nums: &[i32]) -> i32 {\n    let mut count = 0;\n    // ループ回数は配列長に比例する\n    for _ in nums {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 線形時間(配列を走査) */\nint arrayTraversal(int *nums, int n) {\n    int count = 0;\n    // ループ回数は配列長に比例する\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 線形時間(配列を走査) */\nfun arrayTraversal(nums: IntArray): Int {\n    var count = 0\n    // ループ回数は配列長に比例する\n    for (num in nums) {\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### 線形階 ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n\n# ## 線形階(配列を走査)###\ndef array_traversal(nums)\n  count = 0\n\n  # ループ回数は配列長に比例する\n  for num in nums\n    count += 1\n  end\n\n  count\nend\n
コードの可視化

全画面で見る >

注意すべきなのは、**入力データサイズ \\(n\\) は入力データの型に応じて具体的に定める必要がある**ということです。例えば 1 つ目の例では変数 \\(n\\) が入力データサイズであり、2 つ目の例では配列長 \\(n\\) がデータサイズです。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#3-on2","level":3,"title":"3.   平方階 \\(O(n^2)\\)","text":"

平方階の操作回数は入力データサイズ \\(n\\) に対して二乗のオーダーで増加します。平方階は通常、入れ子ループに現れ、外側のループと内側のループの時間計算量がともに \\(O(n)\\) であるため、全体の時間計算量は \\(O(n^2)\\) になります:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def quadratic(n: int) -> int:\n    \"\"\"二乗階\"\"\"\n    count = 0\n    # ループ回数はデータサイズ n の二乗に比例する\n    for i in range(n):\n        for j in range(n):\n            count += 1\n    return count\n
time_complexity.cpp
/* 二乗階 */\nint quadratic(int n) {\n    int count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.java
/* 二乗階 */\nint quadratic(int n) {\n    int count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.cs
/* 二乗階 */\nint Quadratic(int n) {\n    int count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.go
/* 二乗階 */\nfunc quadratic(n int) int {\n    count := 0\n    // ループ回数はデータサイズ n の二乗に比例する\n    for i := 0; i < n; i++ {\n        for j := 0; j < n; j++ {\n            count++\n        }\n    }\n    return count\n}\n
time_complexity.swift
/* 二乗階 */\nfunc quadratic(n: Int) -> Int {\n    var count = 0\n    // ループ回数はデータサイズ n の二乗に比例する\n    for _ in 0 ..< n {\n        for _ in 0 ..< n {\n            count += 1\n        }\n    }\n    return count\n}\n
time_complexity.js
/* 二乗階 */\nfunction quadratic(n) {\n    let count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.ts
/* 二乗階 */\nfunction quadratic(n: number): number {\n    let count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.dart
/* 二乗階 */\nint quadratic(int n) {\n  int count = 0;\n  // ループ回数はデータサイズ n の二乗に比例する\n  for (int i = 0; i < n; i++) {\n    for (int j = 0; j < n; j++) {\n      count++;\n    }\n  }\n  return count;\n}\n
time_complexity.rs
/* 二乗階 */\nfn quadratic(n: i32) -> i32 {\n    let mut count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for _ in 0..n {\n        for _ in 0..n {\n            count += 1;\n        }\n    }\n    count\n}\n
time_complexity.c
/* 二乗階 */\nint quadratic(int n) {\n    int count = 0;\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.kt
/* 二乗階 */\nfun quadratic(n: Int): Int {\n    var count = 0\n    // ループ回数はデータサイズ n の二乗に比例する\n    for (i in 0..<n) {\n        for (j in 0..<n) {\n            count++\n        }\n    }\n    return count\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n
コードの可視化

全画面で見る >

以下の図は、定数階・線形階・平方階の 3 種類の時間計算量を比較したものです。

図 2-10   定数階、線形階、平方階の時間計算量

バブルソートを例にとると、外側のループは \\(n - 1\\) 回実行され、内側のループは \\(n-1\\)、\\(n-2\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) 回実行され、平均すると \\(n / 2\\) 回です。したがって時間計算量は \\(O((n - 1) n / 2) = O(n^2)\\) となります:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def bubble_sort(nums: list[int]) -> int:\n    \"\"\"二次時間(バブルソート)\"\"\"\n    count = 0  # カウンタ\n    # 外側のループ:未ソート区間は [0, i]\n    for i in range(len(nums) - 1, 0, -1):\n        # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # nums[j] と nums[j + 1] を交換\n                tmp: int = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3  # 要素交換には 3 回の単位操作が含まれる\n    return count\n
time_complexity.cpp
/* 二次時間(バブルソート) */\nint bubbleSort(vector<int> &nums) {\n    int count = 0; // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.java
/* 二次時間(バブルソート) */\nint bubbleSort(int[] nums) {\n    int count = 0; // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.cs
/* 二次時間(バブルソート) */\nint BubbleSort(int[] nums) {\n    int count = 0;  // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                count += 3;  // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.go
/* 二次時間(バブルソート) */\nfunc bubbleSort(nums []int) int {\n    count := 0 // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // nums[j] と nums[j + 1] を交換\n                tmp := nums[j]\n                nums[j] = nums[j+1]\n                nums[j+1] = tmp\n                count += 3 // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count\n}\n
time_complexity.swift
/* 二次時間(バブルソート) */\nfunc bubbleSort(nums: inout [Int]) -> Int {\n    var count = 0 // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3 // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count\n}\n
time_complexity.js
/* 二次時間(バブルソート) */\nfunction bubbleSort(nums) {\n    let count = 0; // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.ts
/* 二次時間(バブルソート) */\nfunction bubbleSort(nums: number[]): number {\n    let count = 0; // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.dart
/* 二次時間(バブルソート) */\nint bubbleSort(List<int> nums) {\n  int count = 0; // カウンタ\n  // 外側のループ:未ソート区間は [0, i]\n  for (var i = nums.length - 1; i > 0; i--) {\n    // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for (var j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // nums[j] と nums[j + 1] を交換\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        count += 3; // 要素交換には 3 回の単位操作が含まれる\n      }\n    }\n  }\n  return count;\n}\n
time_complexity.rs
/* 二次時間(バブルソート) */\nfn bubble_sort(nums: &mut [i32]) -> i32 {\n    let mut count = 0; // カウンタ\n\n    // 外側のループ:未ソート区間は [0, i]\n    for i in (1..nums.len()).rev() {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    count\n}\n
time_complexity.c
/* 二次時間(バブルソート) */\nint bubbleSort(int *nums, int n) {\n    int count = 0; // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = n - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.kt
/* 二次時間(バブルソート) */\nfun bubbleSort(nums: IntArray): Int {\n    var count = 0 // カウンタ\n    // 外側のループ:未ソート区間は [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                count += 3 // 要素交換には 3 回の単位操作が含まれる\n            }\n        }\n    }\n    return count\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## 平方階(バブルソート)###\ndef bubble_sort(nums)\n  count = 0  # カウンタ\n\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (nums.length - 1).downto(0)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # 要素交換には 3 回の単位操作が含まれる\n      end\n    end\n  end\n\n  count\nend\n
コードの可視化

全画面で見る >

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#4-o2n","level":3,"title":"4.   指数階 \\(O(2^n)\\)","text":"

生物学における「細胞分裂」は指数階増加の典型例です。初期状態では細胞が \\(1\\) 個あり、1 回分裂すると \\(2\\) 個、2 回分裂すると \\(4\\) 個となり、以下同様に、\\(n\\) 回分裂すると \\(2^n\\) 個の細胞になります。

以下の図とコードは細胞分裂の過程を模擬したもので、時間計算量は \\(O(2^n)\\) です。なお、入力の \\(n\\) は分裂回数を表し、戻り値 count は総分裂回数を表します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def exponential(n: int) -> int:\n    \"\"\"指数時間(ループ実装)\"\"\"\n    count = 0\n    base = 1\n    # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for _ in range(n):\n        for _ in range(base):\n            count += 1\n        base *= 2\n    # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n
time_complexity.cpp
/* 指数時間(ループ実装) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.java
/* 指数時間(ループ実装) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.cs
/* 指数時間(ループ実装) */\nint Exponential(int n) {\n    int count = 0, bas = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.go
/* 指数時間(ループ実装) */\nfunc exponential(n int) int {\n    count, base := 0, 1\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for i := 0; i < n; i++ {\n        for j := 0; j < base; j++ {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.swift
/* 指数時間(ループ実装) */\nfunc exponential(n: Int) -> Int {\n    var count = 0\n    var base = 1\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for _ in 0 ..< n {\n        for _ in 0 ..< base {\n            count += 1\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.js
/* 指数時間(ループ実装) */\nfunction exponential(n) {\n    let count = 0,\n        base = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.ts
/* 指数時間(ループ実装) */\nfunction exponential(n: number): number {\n    let count = 0,\n        base = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.dart
/* 指数時間(ループ実装) */\nint exponential(int n) {\n  int count = 0, base = 1;\n  // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n  for (var i = 0; i < n; i++) {\n    for (var j = 0; j < base; j++) {\n      count++;\n    }\n    base *= 2;\n  }\n  // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  return count;\n}\n
time_complexity.rs
/* 指数時間(ループ実装) */\nfn exponential(n: i32) -> i32 {\n    let mut count = 0;\n    let mut base = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for _ in 0..n {\n        for _ in 0..base {\n            count += 1\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    count\n}\n
time_complexity.c
/* 指数時間(ループ実装) */\nint exponential(int n) {\n    int count = 0;\n    int bas = 1;\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.kt
/* 指数時間(ループ実装) */\nfun exponential(n: Int): Int {\n    var count = 0\n    var base = 1\n    // 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n    for (i in 0..<n) {\n        for (j in 0..<base) {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## 平方階(バブルソート)###\ndef bubble_sort(nums)\n  count = 0  # カウンタ\n\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (nums.length - 1).downto(0)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # 要素交換には 3 回の単位操作が含まれる\n      end\n    end\n  end\n\n  count\nend\n\n# ## 指数階(ループ実装)###\ndef exponential(n)\n  count, base = 0, 1\n\n  # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n
コードの可視化

全画面で見る >

図 2-11   指数階の時間計算量

実際のアルゴリズムでも、指数階は再帰関数によく現れます。例えば次のコードでは、再帰的に 2 つへ分岐し、\\(n\\) 回分裂した後に停止します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def exp_recur(n: int) -> int:\n    \"\"\"指数時間(再帰実装)\"\"\"\n    if n == 1:\n        return 1\n    return exp_recur(n - 1) + exp_recur(n - 1) + 1\n
time_complexity.cpp
/* 指数時間(再帰実装) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.java
/* 指数時間(再帰実装) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.cs
/* 指数時間(再帰実装) */\nint ExpRecur(int n) {\n    if (n == 1) return 1;\n    return ExpRecur(n - 1) + ExpRecur(n - 1) + 1;\n}\n
time_complexity.go
/* 指数時間(再帰実装) */\nfunc expRecur(n int) int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n-1) + expRecur(n-1) + 1\n}\n
time_complexity.swift
/* 指数時間(再帰実装) */\nfunc expRecur(n: Int) -> Int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n: n - 1) + expRecur(n: n - 1) + 1\n}\n
time_complexity.js
/* 指数時間(再帰実装) */\nfunction expRecur(n) {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.ts
/* 指数時間(再帰実装) */\nfunction expRecur(n: number): number {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.dart
/* 指数時間(再帰実装) */\nint expRecur(int n) {\n  if (n == 1) return 1;\n  return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.rs
/* 指数時間(再帰実装) */\nfn exp_recur(n: i32) -> i32 {\n    if n == 1 {\n        return 1;\n    }\n    exp_recur(n - 1) + exp_recur(n - 1) + 1\n}\n
time_complexity.c
/* 指数時間(再帰実装) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.kt
/* 指数時間(再帰実装) */\nfun expRecur(n: Int): Int {\n    if (n == 1) {\n        return 1\n    }\n    return expRecur(n - 1) + expRecur(n - 1) + 1\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## 平方階(バブルソート)###\ndef bubble_sort(nums)\n  count = 0  # カウンタ\n\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (nums.length - 1).downto(0)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # 要素交換には 3 回の単位操作が含まれる\n      end\n    end\n  end\n\n  count\nend\n\n# ## 指数階(ループ実装)###\ndef exponential(n)\n  count, base = 0, 1\n\n  # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## 指数階(再帰実装)###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n
コードの可視化

全画面で見る >

指数階の増加は非常に速く、全探索法(ブルートフォース、バックトラッキングなど)によく見られます。データ規模が大きい問題では、指数階は受け入れられず、通常は動的計画法や貪欲法などを使って解く必要があります。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#5-olog-n","level":3,"title":"5.   対数階 \\(O(\\log n)\\)","text":"

指数階とは逆に、対数階は「各ラウンドで半分になる」状況を表します。入力データサイズを \\(n\\) とすると、各ラウンドで半減するため、ループ回数は \\(\\log_2 n\\)、すなわち \\(2^n\\) の逆関数になります。

以下の図とコードは、「各ラウンドで半分になる」過程を模擬したもので、時間計算量は \\(O(\\log_2 n)\\)、簡潔には \\(O(\\log n)\\) と書きます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def logarithmic(n: int) -> int:\n    \"\"\"対数時間(ループ実装)\"\"\"\n    count = 0\n    while n > 1:\n        n = n / 2\n        count += 1\n    return count\n
time_complexity.cpp
/* 対数時間(ループ実装) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* 対数時間(ループ実装) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* 対数時間(ループ実装) */\nint Logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n /= 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* 対数時間(ループ実装) */\nfunc logarithmic(n int) int {\n    count := 0\n    for n > 1 {\n        n = n / 2\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 対数時間(ループ実装) */\nfunc logarithmic(n: Int) -> Int {\n    var count = 0\n    var n = n\n    while n > 1 {\n        n = n / 2\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 対数時間(ループ実装) */\nfunction logarithmic(n) {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* 対数時間(ループ実装) */\nfunction logarithmic(n: number): number {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* 対数時間(ループ実装) */\nint logarithmic(int n) {\n  int count = 0;\n  while (n > 1) {\n    n = n ~/ 2;\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 対数時間(ループ実装) */\nfn logarithmic(mut n: i32) -> i32 {\n    let mut count = 0;\n    while n > 1 {\n        n = n / 2;\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 対数時間(ループ実装) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 対数時間(ループ実装) */\nfun logarithmic(n: Int): Int {\n    var n1 = n\n    var count = 0\n    while (n1 > 1) {\n        n1 /= 2\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## 平方階(バブルソート)###\ndef bubble_sort(nums)\n  count = 0  # カウンタ\n\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (nums.length - 1).downto(0)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # 要素交換には 3 回の単位操作が含まれる\n      end\n    end\n  end\n\n  count\nend\n\n# ## 指数階(ループ実装)###\ndef exponential(n)\n  count, base = 0, 1\n\n  # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## 指数階(再帰実装)###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n\n# ## 対数階(ループ実装)###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n
コードの可視化

全画面で見る >

図 2-12   対数階の時間計算量

指数階と同様に、対数階も再帰関数によく現れます。次のコードは高さ \\(\\log_2 n\\) の再帰木を形成します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def log_recur(n: int) -> int:\n    \"\"\"対数時間(再帰実装)\"\"\"\n    if n <= 1:\n        return 0\n    return log_recur(n / 2) + 1\n
time_complexity.cpp
/* 対数時間(再帰実装) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.java
/* 対数時間(再帰実装) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.cs
/* 対数時間(再帰実装) */\nint LogRecur(int n) {\n    if (n <= 1) return 0;\n    return LogRecur(n / 2) + 1;\n}\n
time_complexity.go
/* 対数時間(再帰実装) */\nfunc logRecur(n int) int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n/2) + 1\n}\n
time_complexity.swift
/* 対数時間(再帰実装) */\nfunc logRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n: n / 2) + 1\n}\n
time_complexity.js
/* 対数時間(再帰実装) */\nfunction logRecur(n) {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.ts
/* 対数時間(再帰実装) */\nfunction logRecur(n: number): number {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.dart
/* 対数時間(再帰実装) */\nint logRecur(int n) {\n  if (n <= 1) return 0;\n  return logRecur(n ~/ 2) + 1;\n}\n
time_complexity.rs
/* 対数時間(再帰実装) */\nfn log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 0;\n    }\n    log_recur(n / 2) + 1\n}\n
time_complexity.c
/* 対数時間(再帰実装) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.kt
/* 対数時間(再帰実装) */\nfun logRecur(n: Int): Int {\n    if (n <= 1)\n        return 0\n    return logRecur(n / 2) + 1\n}\n
time_complexity.rb
### 平方階 ###\ndef quadratic(n)\n  count = 0\n\n  # ループ回数はデータサイズ n の二乗に比例する\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n\n# ## 平方階(バブルソート)###\ndef bubble_sort(nums)\n  count = 0  # カウンタ\n\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (nums.length - 1).downto(0)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # 要素交換には 3 回の単位操作が含まれる\n      end\n    end\n  end\n\n  count\nend\n\n# ## 指数階(ループ実装)###\ndef exponential(n)\n  count, base = 0, 1\n\n  # 細胞は各ラウンドで 2 つに分裂し、数列 1, 2, 4, 8, ..., 2^(n-1) を形成する\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n\n# ## 指数階(再帰実装)###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n\n# ## 対数階(ループ実装)###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n\n# ## 対数階(再帰実装)###\ndef log_recur(n)\n  return 0 unless n > 1\n  log_recur(n / 2) + 1\nend\n
コードの可視化

全画面で見る >

対数階は分割統治に基づくアルゴリズムによく現れ、「1 つを複数に分ける」「複雑なものを単純化する」という考え方を体現しています。増加は緩やかで、定数階に次いで理想的な時間計算量です。

\\(O(\\log n)\\) の底は何か?

正確には、「\\(m\\) 個に分ける」場合に対応する時間計算量は \\(O(\\log_m n)\\) です。そして対数の底の変換公式により、底が異なっても同値な時間計算量が得られます:

\\[ O(\\log_m n) = O(\\log_k n / \\log_k m) = O(\\log_k n) \\]

つまり、底 \\(m\\) は複雑度に影響を与えずに変換できます。そのため通常は底 \\(m\\) を省略し、対数階を単に \\(O(\\log n)\\) と記します。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#6-on-log-n","level":3,"title":"6.   線形対数階 \\(O(n \\log n)\\)","text":"

線形対数階は入れ子ループによく現れ、2 層のループの時間計算量はそれぞれ \\(O(\\log n)\\) と \\(O(n)\\) です。関連するコードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def linear_log_recur(n: int) -> int:\n    \"\"\"線形対数時間\"\"\"\n    if n <= 1:\n        return 1\n    # 二つに分割すると、部分問題の規模は半分になる\n    count = linear_log_recur(n // 2) + linear_log_recur(n // 2)\n    # 現在の部分問題には n 個の操作が含まれる\n    for _ in range(n):\n        count += 1\n    return count\n
time_complexity.cpp
/* 線形対数時間 */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* 線形対数時間 */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* 線形対数時間 */\nint LinearLogRecur(int n) {\n    if (n <= 1) return 1;\n    int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* 線形対数時間 */\nfunc linearLogRecur(n int) int {\n    if n <= 1 {\n        return 1\n    }\n    count := linearLogRecur(n/2) + linearLogRecur(n/2)\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 線形対数時間 */\nfunc linearLogRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 1\n    }\n    var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2)\n    for _ in stride(from: 0, to: n, by: 1) {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 線形対数時間 */\nfunction linearLogRecur(n) {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* 線形対数時間 */\nfunction linearLogRecur(n: number): number {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* 線形対数時間 */\nint linearLogRecur(int n) {\n  if (n <= 1) return 1;\n  int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2);\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 線形対数時間 */\nfn linear_log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 1;\n    }\n    let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2);\n    for _ in 0..n {\n        count += 1;\n    }\n    return count;\n}\n
time_complexity.c
/* 線形対数時間 */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 線形対数時間 */\nfun linearLogRecur(n: Int): Int {\n    if (n <= 1)\n        return 1\n    var count = linearLogRecur(n / 2) + linearLogRecur(n / 2)\n    for (i in 0..<n) {\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### 線形対数時間 ###\ndef linear_log_recur(n)\n  return 1 unless n > 1\n\n  count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n  (0...n).each { count += 1 }\n\n  count\nend\n
コードの可視化

全画面で見る >

下図は線形対数階がどのように生じるかを示しています。二分木の各層の操作総数はすべて \\(n\\) であり、木全体は \\(\\log_2 n + 1\\) 層あるため、時間計算量は \\(O(n \\log n)\\) です。

図 2-13   線形対数階の時間計算量

主なソートアルゴリズムの時間計算量は通常 \\(O(n \\log n)\\) であり、例えばクイックソート、マージソート、ヒープソートなどがあります。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#7-on","level":3,"title":"7.   階乗階 \\(O(n!)\\)","text":"

階乗階は、数学における「全順列」の問題に対応します。互いに重複しない \\(n\\) 個の要素が与えられたとき、そのすべての並べ方を求めると、通り数は次のようになります:

\\[ n! = n \\times (n - 1) \\times (n - 2) \\times \\dots \\times 2 \\times 1 \\]

階乗は通常、再帰で実装されます。以下の図とコードのように、第 1 層では \\(n\\) 個に分岐し、第 2 層では \\(n - 1\\) 個に分岐し、以下同様に、第 \\(n\\) 層で分岐が停止します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def factorial_recur(n: int) -> int:\n    \"\"\"階乗時間(再帰実装)\"\"\"\n    if n == 0:\n        return 1\n    count = 0\n    # 1個から n 個に分裂\n    for _ in range(n):\n        count += factorial_recur(n - 1)\n    return count\n
time_complexity.cpp
/* 階乗時間(再帰実装) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // 1個から n 個に分裂\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.java
/* 階乗時間(再帰実装) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // 1個から n 個に分裂\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.cs
/* 階乗時間(再帰実装) */\nint FactorialRecur(int n) {\n    if (n == 0) return 1;\n    int count = 0;\n    // 1個から n 個に分裂\n    for (int i = 0; i < n; i++) {\n        count += FactorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.go
/* 階乗時間(再帰実装) */\nfunc factorialRecur(n int) int {\n    if n == 0 {\n        return 1\n    }\n    count := 0\n    // 1個から n 個に分裂\n    for i := 0; i < n; i++ {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
time_complexity.swift
/* 階乗時間(再帰実装) */\nfunc factorialRecur(n: Int) -> Int {\n    if n == 0 {\n        return 1\n    }\n    var count = 0\n    // 1個から n 個に分裂\n    for _ in 0 ..< n {\n        count += factorialRecur(n: n - 1)\n    }\n    return count\n}\n
time_complexity.js
/* 階乗時間(再帰実装) */\nfunction factorialRecur(n) {\n    if (n === 0) return 1;\n    let count = 0;\n    // 1個から n 個に分裂\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.ts
/* 階乗時間(再帰実装) */\nfunction factorialRecur(n: number): number {\n    if (n === 0) return 1;\n    let count = 0;\n    // 1個から n 個に分裂\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.dart
/* 階乗時間(再帰実装) */\nint factorialRecur(int n) {\n  if (n == 0) return 1;\n  int count = 0;\n  // 1個から n 個に分裂\n  for (var i = 0; i < n; i++) {\n    count += factorialRecur(n - 1);\n  }\n  return count;\n}\n
time_complexity.rs
/* 階乗時間(再帰実装) */\nfn factorial_recur(n: i32) -> i32 {\n    if n == 0 {\n        return 1;\n    }\n    let mut count = 0;\n    // 1個から n 個に分裂\n    for _ in 0..n {\n        count += factorial_recur(n - 1);\n    }\n    count\n}\n
time_complexity.c
/* 階乗時間(再帰実装) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.kt
/* 階乗時間(再帰実装) */\nfun factorialRecur(n: Int): Int {\n    if (n == 0)\n        return 1\n    var count = 0\n    // 1個から n 個に分裂\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
time_complexity.rb
### 線形対数時間 ###\ndef linear_log_recur(n)\n  return 1 unless n > 1\n\n  count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n  (0...n).each { count += 1 }\n\n  count\nend\n\n# ## 階乗階(再帰実装)###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # 1個から n 個に分裂\n  (0...n).each { count += factorial_recur(n - 1) }\n\n  count\nend\n
コードの可視化

全画面で見る >

図 2-14   階乗階の時間計算量

注意すべき点として、\\(n \\geq 4\\) なら常に \\(n! > 2^n\\) なので、階乗階は指数階よりもさらに速く増加し、\\(n\\) が大きい場合にはやはり受け入れられません。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#235","level":2,"title":"2.3.5   最悪・最良・平均時間計算量","text":"

アルゴリズムの時間効率は固定ではなく、入力データの分布に左右されることが多いです。長さ \\(n\\) の配列 nums を考えます。nums は \\(1\\) から \\(n\\) までの数字で構成され、各数字は 1 回だけ現れます。ただし要素の順序はランダムにシャッフルされており、目標は要素 \\(1\\) のインデックスを返すことです。ここから次の結論が得られます。

  • nums = [?, ?, ..., 1]、つまり末尾の要素が \\(1\\) の場合は、配列全体を最後まで走査する必要があり、最悪時間計算量 \\(O(n)\\) になります。
  • nums = [1, ?, ?, ...]、つまり先頭要素が \\(1\\) の場合は、配列がどれだけ長くてもそれ以上走査する必要がなく、最良時間計算量 \\(\\Omega(1)\\) になります。

「最悪時間計算量」は関数の漸近上界に対応し、ビッグ \\(O\\) 記法で表します。同様に、「最良時間計算量」は関数の漸近下界に対応し、\\(\\Omega\\) 記法で表します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby worst_best_time_complexity.py
def random_numbers(n: int) -> list[int]:\n    \"\"\"要素が 1, 2, ..., n で順序がシャッフルされた配列を生成する\"\"\"\n    # 配列 nums =: 1, 2, 3, ..., n を生成する\n    nums = [i for i in range(1, n + 1)]\n    # 配列要素をランダムにシャッフル\n    random.shuffle(nums)\n    return nums\n\ndef find_one(nums: list[int]) -> int:\n    \"\"\"配列 nums 内で数値 1 のインデックスを探す\"\"\"\n    for i in range(len(nums)):\n        # 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        # 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if nums[i] == 1:\n            return i\n    return -1\n
worst_best_time_complexity.cpp
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nvector<int> randomNumbers(int n) {\n    vector<int> nums(n);\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // システム時刻を使って乱数シードを生成する\n    unsigned seed = chrono::system_clock::now().time_since_epoch().count();\n    // 配列要素をランダムにシャッフル\n    shuffle(nums.begin(), nums.end(), default_random_engine(seed));\n    return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nint findOne(vector<int> &nums) {\n    for (int i = 0; i < nums.size(); i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.java
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nint[] randomNumbers(int n) {\n    Integer[] nums = new Integer[n];\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 配列要素をランダムにシャッフル\n    Collections.shuffle(Arrays.asList(nums));\n    // Integer[] -> int[]\n    int[] res = new int[n];\n    for (int i = 0; i < n; i++) {\n        res[i] = nums[i];\n    }\n    return res;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nint findOne(int[] nums) {\n    for (int i = 0; i < nums.length; i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.cs
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nint[] RandomNumbers(int n) {\n    int[] nums = new int[n];\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n\n    // 配列要素をランダムにシャッフル\n    for (int i = 0; i < nums.Length; i++) {\n        int index = new Random().Next(i, nums.Length);\n        (nums[i], nums[index]) = (nums[index], nums[i]);\n    }\n    return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nint FindOne(int[] nums) {\n    for (int i = 0; i < nums.Length; i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.go
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfunc randomNumbers(n int) []int {\n    nums := make([]int, n)\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for i := 0; i < n; i++ {\n        nums[i] = i + 1\n    }\n    // 配列要素をランダムにシャッフル\n    rand.Shuffle(len(nums), func(i, j int) {\n        nums[i], nums[j] = nums[j], nums[i]\n    })\n    return nums\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfunc findOne(nums []int) int {\n    for i := 0; i < len(nums); i++ {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
worst_best_time_complexity.swift
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfunc randomNumbers(n: Int) -> [Int] {\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    var nums = Array(1 ... n)\n    // 配列要素をランダムにシャッフル\n    nums.shuffle()\n    return nums\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfunc findOne(nums: [Int]) -> Int {\n    for i in nums.indices {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
worst_best_time_complexity.js
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfunction randomNumbers(n) {\n    const nums = Array(n);\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 配列要素をランダムにシャッフル\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfunction findOne(nums) {\n    for (let i = 0; i < nums.length; i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
worst_best_time_complexity.ts
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfunction randomNumbers(n: number): number[] {\n    const nums = Array(n);\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 配列要素をランダムにシャッフル\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfunction findOne(nums: number[]): number {\n    for (let i = 0; i < nums.length; i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
worst_best_time_complexity.dart
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nList<int> randomNumbers(int n) {\n  final nums = List.filled(n, 0);\n  // 配列 nums = { 1, 2, 3, ..., n } を生成\n  for (var i = 0; i < n; i++) {\n    nums[i] = i + 1;\n  }\n  // 配列要素をランダムにシャッフル\n  nums.shuffle();\n\n  return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nint findOne(List<int> nums) {\n  for (var i = 0; i < nums.length; i++) {\n    // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n    // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n    if (nums[i] == 1) return i;\n  }\n\n  return -1;\n}\n
worst_best_time_complexity.rs
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfn random_numbers(n: i32) -> Vec<i32> {\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    let mut nums = (1..=n).collect::<Vec<i32>>();\n    // 配列要素をランダムにシャッフル\n    nums.shuffle(&mut thread_rng());\n    nums\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfn find_one(nums: &[i32]) -> Option<usize> {\n    for i in 0..nums.len() {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if nums[i] == 1 {\n            return Some(i);\n        }\n    }\n    None\n}\n
worst_best_time_complexity.c
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nint *randomNumbers(int n) {\n    // ヒープ領域にメモリを確保する(要素数 n、要素型 int の一次元可変長配列を作成)\n    int *nums = (int *)malloc(n * sizeof(int));\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 配列要素をランダムにシャッフル\n    for (int i = n - 1; i > 0; i--) {\n        int j = rand() % (i + 1);\n        int temp = nums[i];\n        nums[i] = nums[j];\n        nums[j] = temp;\n    }\n    return nums;\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nint findOne(int *nums, int n) {\n    for (int i = 0; i < n; i++) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.kt
/* 要素が { 1, 2, ..., n } で、順序がシャッフルされた配列を生成 */\nfun randomNumbers(n: Int): Array<Int?> {\n    val nums = IntArray(n)\n    // 配列 nums = { 1, 2, 3, ..., n } を生成\n    for (i in 0..<n) {\n        nums[i] = i + 1\n    }\n    // 配列要素をランダムにシャッフル\n    nums.shuffle()\n    val res = arrayOfNulls<Int>(n)\n    for (i in 0..<n) {\n        res[i] = nums[i]\n    }\n    return res\n}\n\n/* 配列 nums 内で数値 1 のインデックスを探す */\nfun findOne(nums: Array<Int?>): Int {\n    for (i in nums.indices) {\n        // 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n        // 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n        if (nums[i] == 1)\n            return i\n    }\n    return -1\n}\n
worst_best_time_complexity.rb
### 1, 2, ..., n を要素とする配列を生成し、順序をシャッフルする ###\ndef random_numbers(n)\n  # 配列 nums =: 1, 2, 3, ..., n を生成する\n  nums = Array.new(n) { |i| i + 1 }\n  # 配列要素をランダムにシャッフル\n  nums.shuffle!\nend\n\n### 配列 nums 内の数値 1 のインデックスを探す ###\ndef find_one(nums)\n  for i in 0...nums.length\n    # 要素 1 が配列の先頭にあるとき、最良時間計算量 O(1) となる\n    # 要素 1 が配列の末尾にあるとき、最悪時間計算量 O(n) となる\n    return i if nums[i] == 1\n  end\n\n  -1\nend\n
コードの可視化

全画面で見る >

実際には、最良時間計算量を使うことはあまりありません。通常それが実現する確率はごく低く、誤解を招く可能性があるからです。**一方で最悪時間計算量はより実用的で、効率の安全側の目安を与えてくれる**ため、安心してアルゴリズムを使えます。

上の例から分かるように、最悪時間計算量と最良時間計算量は「特殊なデータ分布」でのみ現れ、その発生確率は低いことが多く、アルゴリズムの実行効率をそのまま正確に反映するわけではありません。それに対して、**平均時間計算量はランダム入力におけるアルゴリズムの実行効率を表せる**ため、\\(\\Theta\\) 記法で表します。

一部のアルゴリズムでは、ランダムなデータ分布における平均的な状況を比較的簡単に求められます。例えば上の例では、入力配列はシャッフルされているため、要素 \\(1\\) が任意のインデックスに現れる確率は等しいです。したがってアルゴリズムの平均ループ回数は配列長の半分 \\(n / 2\\) となり、平均時間計算量は \\(\\Theta(n / 2) = \\Theta(n)\\) です。

しかし、より複雑なアルゴリズムでは、平均時間計算量を計算するのはしばしば困難です。データ分布に対する全体の数学的期待値を分析するのが難しいからです。そのような場合、通常は最悪時間計算量をアルゴリズム効率の評価基準として用います。

なぜ \\(\\Theta\\) 記号をあまり見かけないのか?

おそらく \\(O\\) 記号のほうが口にしやすいため、平均時間計算量を表すのにもよく使われます。ただし厳密には、この用法は正確ではありません。本書や他の資料で「平均時間計算量 \\(O(n)\\)」のような表現を見かけた場合は、そのまま \\(\\Theta(n)\\) と理解してください。

","path":["第 2 章   計算量解析","2.3   時間計算量"],"tags":[]},{"location":"chapter_data_structure/","level":1,"title":"第 3 章   データ構造","text":"

Abstract

データ構造は、堅固で多様な枠組みのようなものである。

それはデータを秩序立てて組織するための青写真を示し、アルゴリズムはその上で生き生きと動き出す。

","path":["第 3 章   データ構造"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"章の内容","text":"
  • 3.1   データ構造の分類
  • 3.2   基本データ型
  • 3.3   数値エンコーディング *
  • 3.4   文字エンコーディング *
  • 3.5   まとめ
","path":["第 3 章   データ構造"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   基本データ型","text":"

コンピュータ内のデータについて考えるとき、テキスト、画像、動画、音声、3D モデルなど、さまざまな形態を思い浮かべます。これらのデータの構成形式はそれぞれ異なりますが、いずれも各種の基本データ型によって成り立っています。

**基本データ型は CPU が直接演算できる型**であり、アルゴリズムの中で直接使われます。主なものは次のとおりです。

  • 整数型 byteshortintlong
  • 浮動小数点数型 floatdouble ,小数を表すために使います。
  • 文字型 char ,各言語の文字、句読点、さらには絵文字などを表すために使います。
  • 真偽値型 bool ,真か偽かの判定を表すために使います。

基本データ型はコンピュータ内で 2 進数の形で格納されます。1 つの二進桁は \\(1\\) ビットです。現代のほとんどのオペレーティングシステムでは、\\(1\\) バイト(byte)は \\(8\\) ビット(bit)で構成されます。

基本データ型の値域は、その型が占める領域の大きさによって決まります。以下では Java を例に取ります。

  • 整数型 byte は \\(1\\) バイト = \\(8\\) ビットを占め、\\(2^{8}\\) 個の数を表せます。
  • 整数型 int は \\(4\\) バイト = \\(32\\) ビットを占め、\\(2^{32}\\) 個の数を表せます。

下表は、Java における各種基本データ型の使用領域、値域、デフォルト値を示したものです。この表を丸暗記する必要はなく、大まかに理解しておけば十分であり、必要になったときに参照すればかまいません。

表 3-1   基本データ型の使用領域と値域

型 記号 使用領域 最小値 最大値 デフォルト値 整数 byte 1 バイト \\(-2^7\\) (\\(-128\\)) \\(2^7 - 1\\) (\\(127\\)) \\(0\\) short 2 バイト \\(-2^{15}\\) \\(2^{15} - 1\\) \\(0\\) int 4 バイト \\(-2^{31}\\) \\(2^{31} - 1\\) \\(0\\) long 8 バイト \\(-2^{63}\\) \\(2^{63} - 1\\) \\(0\\) 浮動小数点数 float 4 バイト \\(1.175 \\times 10^{-38}\\) \\(3.403 \\times 10^{38}\\) \\(0.0\\text{f}\\) double 8 バイト \\(2.225 \\times 10^{-308}\\) \\(1.798 \\times 10^{308}\\) \\(0.0\\) 文字 char 2 バイト \\(0\\) \\(2^{16} - 1\\) \\(0\\) 真偽値 bool 1 バイト \\(\\text{false}\\) \\(\\text{true}\\) \\(\\text{false}\\)

注意してください。上表は Java の基本データ型に対するものです。各プログラミング言語にはそれぞれ独自のデータ型定義があり、使用領域、値域、デフォルト値は異なる場合があります。

  • Python では、整数型 int は利用可能なメモリに制限されるだけで任意の大きさを取れます。浮動小数点数 float は倍精度 64 ビットです。char 型はなく、1 文字は実際には長さ 1 の文字列 str です。
  • C と C++ では基本データ型の大きさは明確に規定されておらず、実装やプラットフォームによって異なります。上表は LP64 データモデル に従っており、Linux や macOS を含む Unix 系 64 ビット OS で用いられています。
  • char の大きさは C と C++ では 1 バイトですが、多くのプログラミング言語では採用する文字エンコーディング方式によって決まります。詳しくは「文字エンコーディング」の章を参照してください。
  • 真偽値を表すのに必要なのは 1 ビット(\\(0\\) または \\(1\\))だけですが、メモリ上では通常 1 バイトとして格納されます。これは、現代のコンピュータ CPU が通常 1 バイトを最小のアドレス指定可能なメモリ単位としているためです。

では、基本データ型とデータ構造の間にはどのような関係があるのでしょうか。データ構造とは、コンピュータ内でデータを組織し格納する方法のことです。この言葉で主役なのは「データ」ではなく「構造」です。

「数字の並び」を表したいなら、自然に配列の使用を思い浮かべるでしょう。これは、配列の線形構造が数字どうしの隣接関係や順序関係を表せるからです。しかし、格納する内容が整数 int なのか、小数 float なのか、文字 char なのかは、「データ構造」とは関係ありません。

言い換えると、基本データ型はデータの「内容の型」を提供し、データ構造はデータの「組織方法」を提供します。たとえば次のコードでは、同じデータ構造(配列)を使って intfloatcharbool など異なる基本データ型を格納・表現しています。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# さまざまな基本データ型で配列を初期化する\nnumbers: list[int] = [0] * 5\ndecimals: list[float] = [0.0] * 5\n# Python の文字は実際には長さ 1 の文字列\ncharacters: list[str] = ['0'] * 5\nbools: list[bool] = [False] * 5\n# Python のリストはさまざまな基本データ型とオブジェクト参照を自由に格納できる\ndata = [0, 0.0, 'a', False, ListNode(0)]\n
// さまざまな基本データ型で配列を初期化する\nint numbers[5];\nfloat decimals[5];\nchar characters[5];\nbool bools[5];\n
// さまざまな基本データ型で配列を初期化する\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nboolean[] bools = new boolean[5];\n
// さまざまな基本データ型で配列を初期化する\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nbool[] bools = new bool[5];\n
// さまざまな基本データ型で配列を初期化する\nvar numbers = [5]int{}\nvar decimals = [5]float64{}\nvar characters = [5]byte{}\nvar bools = [5]bool{}\n
// さまざまな基本データ型で配列を初期化する\nlet numbers = Array(repeating: 0, count: 5)\nlet decimals = Array(repeating: 0.0, count: 5)\nlet characters: [Character] = Array(repeating: \"a\", count: 5)\nlet bools = Array(repeating: false, count: 5)\n
// JavaScript の配列はさまざまな基本データ型とオブジェクトを自由に格納できる\nconst array = [0, 0.0, 'a', false];\n
// さまざまな基本データ型で配列を初期化する\nconst numbers: number[] = [];\nconst characters: string[] = [];\nconst bools: boolean[] = [];\n
// さまざまな基本データ型で配列を初期化する\nList<int> numbers = List.filled(5, 0);\nList<double> decimals = List.filled(5, 0.0);\nList<String> characters = List.filled(5, 'a');\nList<bool> bools = List.filled(5, false);\n
// さまざまな基本データ型で配列を初期化する\nlet numbers: Vec<i32> = vec![0; 5];\nlet decimals: Vec<f32> = vec![0.0; 5];\nlet characters: Vec<char> = vec!['0'; 5];\nlet bools: Vec<bool> = vec![false; 5];\n
// さまざまな基本データ型で配列を初期化する\nint numbers[10];\nfloat decimals[10];\nchar characters[10];\nbool bools[10];\n
// さまざまな基本データ型で配列を初期化する\nval numbers = IntArray(5)\nval decinals = FloatArray(5)\nval characters = CharArray(5)\nval bools = BooleanArray(5)\n
# Ruby のリストはさまざまな基本データ型とオブジェクト参照を自由に格納できる\ndata = [0, 0.0, 'a', false, ListNode(0)]\n
実行の可視化

https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 3 章   データ構造","3.2   基本データ型"],"tags":[]},{"location":"chapter_data_structure/character_encoding/","level":1,"title":"3.4   文字エンコーディング *","text":"

コンピュータでは、すべてのデータは二進数の形で保存されており、文字 char も例外ではありません。文字を表すためには、「文字セット」を定義し、各文字と二進数の間の一対一の対応関係を定める必要があります。文字セットがあれば、コンピュータは対応表を参照して二進数から文字への変換を行えます。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#341-ascii","level":2,"title":"3.4.1   ASCII 文字セット","text":"

ASCII コードは最も早く登場した文字セットで、その正式名称は American Standard Code for Information Interchange(米国標準情報交換コード)です。これは 7 ビットの二進数(1 バイトの下位 7 ビット)で 1 文字を表し、最大で 128 種類の異なる文字を表現できます。下図のように、ASCII コードには英字の大文字と小文字、数字 0 ~ 9、いくつかの句読点、そしていくつかの制御文字(改行やタブなど)が含まれます。

図 3-6   ASCII コード

しかし、ASCII コードで表現できるのは英語だけです。コンピュータのグローバル化に伴い、より多くの言語を表せる EASCII 文字セットが生まれました。これは ASCII の 7 ビットを 8 ビットへ拡張したもので、256 種類の異なる文字を表現できます。

世界では、さまざまな地域に適した EASCII 文字セットが次々に登場しました。これらの文字セットでは、前半の 128 文字は ASCII コードで統一され、後半の 128 文字は各言語の要件に合わせて個別に定義されています。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#342-gbk","level":2,"title":"3.4.2   GBK 文字セット","text":"

その後、人々は**EASCII コードでも多くの言語に必要な文字数を満たせない**ことに気づきました。たとえば漢字は 10 万字近くあり、日常的に使うものだけでも数千字あります。中国国家標準総局は 1980 年に GB2312 文字セットを公開し、6763 字の漢字を収録して、漢字のコンピュータ処理の基本的な需要を満たしました。

しかし、GB2312 では一部の珍しい字や繁体字を扱えません。GBK 文字セットは GB2312 を基に拡張されたもので、合計 21886 字の漢字を収録しています。GBK のエンコーディング方式では、ASCII 文字は 1 バイト、漢字は 2 バイトで表されます。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode","level":2,"title":"3.4.3   Unicode 文字セット","text":"

コンピュータ技術が急速に発展するにつれて、文字セットと符号化規格は百花繚乱の状態となり、それに伴って多くの問題も生じました。一方では、これらの文字セットは通常、特定の言語の文字しか定義しておらず、多言語環境では正常に動作できませんでした。もう一方では、同じ言語にも複数の文字セット規格が存在し、2 台のコンピュータが異なる符号化規格を使っていると、情報伝達の際に文字化けが発生しました。

当時の研究者たちはこう考えました。十分に完全な文字セットを打ち出して、世界中のあらゆる言語と記号をそこに収録すれば、多言語環境や文字化けの問題を解決できるのではないか。この発想に後押しされて、大規模で包括的な文字セット Unicode が誕生しました。

Unicode の中国語名は「統一コード」であり、理論上は 100 万を超える文字を収容できます。Unicode は世界中の文字を 1 つの文字セットに統合することを目指し、さまざまな言語の文字を処理・表示できる汎用文字セットを提供することで、符号化規格の違いによる文字化けを減らそうとしています。

1991 年の公開以来、Unicode は新しい言語と文字を継続的に拡充してきました。2022 年 9 月時点で、Unicode にはすでに 149186 文字が含まれており、各種言語の文字、記号、さらには絵文字まで収録されています。巨大な Unicode 文字セットでは、よく使われる文字は 2 バイトを占め、一部の珍しい文字は 3 バイト、さらには 4 バイトを占めます。

Unicode は汎用文字セットであり、本質的には各文字に番号(「コードポイント」)を割り当てるものですが、それらのコードポイントをコンピュータ内でどのように保存するかまでは規定していません。ここで疑問が生じます。長さの異なる Unicode コードポイントが同じテキストに現れたとき、システムはどのように文字を解析するのでしょうか。たとえば長さ 2 バイトの符号が与えられたとき、それが 2 バイトの 1 文字なのか、1 バイトの 2 文字なのかをどう判定するのでしょうか。

この問題に対して、**すべての文字を固定長の符号として保存する**という直接的な解決策があります。下図のように、「Hello」の各文字は 1 バイト、「アルゴリズム」の各文字は 2 バイトを占めます。上位ビットを 0 で埋めることで、「Hello アルゴリズム」のすべての文字を 2 バイト長にエンコードできます。こうすれば、システムは 2 バイトごとに 1 文字を解析して、この語句の内容を復元できます。

図 3-7   Unicode エンコーディングの例

しかし ASCII コードはすでに、英語の符号化には 1 バイトで十分であることを示しています。上記の方式を採用すると、英語のテキストが占める空間は ASCII エンコーディング時の 2 倍になり、メモリ空間の浪費が大きくなります。そのため、より効率的な Unicode エンコーディング方式が必要です。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#344-utf-8","level":2,"title":"3.4.4   UTF-8 エンコーディング","text":"

現在、UTF-8 は国際的に最も広く使われている Unicode エンコーディング方式になっています。**これは可変長のエンコーディング**であり、1 文字を 1 〜 4 バイトで表し、文字の複雑さに応じて長さが変わります。ASCII 文字は 1 バイト、ラテン文字とギリシャ文字は 2 バイト、一般的な漢字は 3 バイト、そのほかの一部の珍しい文字は 4 バイト必要です。

UTF-8 の符号化規則はそれほど複雑ではなく、次の 2 つのケースに分けられます。

  • 長さ 1 バイトの文字では、最上位ビットを \\(0\\) にし、残りの 7 ビットを Unicode コードポイントに設定します。ここで注意すべきなのは、ASCII 文字が Unicode 文字セットの先頭 128 個のコードポイントを占めていることです。つまり、UTF-8 エンコーディングは ASCII コードと下位互換性があります。このため、UTF-8 を使って古い ASCII コードのテキストを解析できます。
  • 長さ \\(n\\) バイトの文字(ただし \\(n > 1\\))では、先頭バイトの上位 \\(n\\) ビットをすべて \\(1\\) にし、第 \\(n + 1\\) ビットを \\(0\\) に設定します。2 バイト目以降では、各バイトの上位 2 ビットをいずれも \\(10\\) にし、残りのすべてのビットで文字の Unicode コードポイントを埋めます。

下図は「Helloアルゴリズム」に対応する UTF-8 エンコーディングを示しています。観察すると、上位 \\(n\\) ビットがすべて \\(1\\) に設定されているため、システムは先頭から連続する \\(1\\) の個数を読むことで、その文字の長さが \\(n\\) であると解析できます。

では、なぜ残りのすべてのバイトの上位 2 ビットを \\(10\\) にするのでしょうか。実は、この \\(10\\) は検査用の印として機能します。システムが誤ったバイト位置からテキストを解析し始めたとしても、バイト先頭の \\(10\\) によって異常を素早く判定できます。

この \\(10\\) を検査用の印とする理由は、UTF-8 の符号化規則では上位 2 ビットが \\(10\\) になる文字は存在しないからです。この結論は背理法で証明できます。ある文字の上位 2 ビットが \\(10\\) だと仮定すると、その文字の長さは \\(1\\) であり、ASCII コードに対応することになります。しかし ASCII コードの最上位ビットは \\(0\\) であるはずなので、仮定と矛盾します。

図 3-8   UTF-8 エンコーディングの例

UTF-8 以外にも、一般的なエンコーディング方式として次の 2 つがあります。

  • UTF-16 エンコーディング:1 文字を 2 バイトまたは 4 バイトで表します。すべての ASCII 文字と一般的な非英語文字は 2 バイトで表し、一部の文字だけが 4 バイトを必要とします。2 バイトの文字については、UTF-16 エンコーディングは Unicode コードポイントと等しくなります。
  • UTF-32 エンコーディング:各文字を必ず 4 バイトで表します。つまり UTF-32 は UTF-8 や UTF-16 よりも多くの領域を消費し、とくに ASCII 文字の比率が高いテキストでその傾向が顕著です。

記憶領域の使用量という観点では、UTF-8 は英語文字の表現に非常に効率的で、必要なのは 1 バイトだけです。一方、UTF-16 は一部の非英語文字(たとえば中国語の文字)の符号化でより効率的になることがあり、必要なのは 2 バイトだけで、UTF-8 では 3 バイト必要になる場合があります。

互換性という観点では、UTF-8 の汎用性が最も高く、多くのツールやライブラリが UTF-8 を優先的にサポートしています。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#345","level":2,"title":"3.4.5   プログラミング言語の文字エンコーディング","text":"

従来の多くのプログラミング言語では、実行中の文字列に UTF-16 や UTF-32 のような固定長エンコーディングが使われています。固定長エンコーディングでは、文字列を配列のように扱えるため、次のような利点があります。

  • ランダムアクセス:UTF-16 で符号化された文字列はランダムアクセスが容易です。UTF-8 は可変長エンコーディングなので、第 \\(i\\) 文字を見つけるには文字列の先頭から第 \\(i\\) 文字まで走査する必要があり、\\(O(n)\\) の時間がかかります。
  • 文字数の計算:ランダムアクセスと同様に、UTF-16 で符号化された文字列の長さを計算するのも \\(O(1)\\) の操作です。しかし、UTF-8 で符号化された文字列の長さを計算するには、文字列全体を走査する必要があります。
  • 文字列操作:UTF-16 で符号化された文字列では、多くの文字列操作(分割、連結、挿入、削除など)をより簡単に行えます。UTF-8 で符号化された文字列では、これらの操作を行う際に、無効な UTF-8 エンコーディングを生じさせないための追加計算が通常必要になります。

実際、プログラミング言語における文字エンコーディング方式の設計は、とても興味深い話題であり、多くの要因が関わっています。

  • Java の String 型は UTF-16 エンコーディングを使用し、各文字は 2 バイトを占めます。これは Java 言語の設計当初、人々が 16 ビットあればあらゆる文字を表現するのに十分だと考えていたためです。しかし、これは誤った判断でした。その後 Unicode 規格は 16 ビットを超える範囲へ拡張されたため、現在の Java では 1 文字が 16 ビット値の組(「サロゲートペア」)で表されることがあります。
  • JavaScript と TypeScript の文字列が UTF-16 エンコーディングを使う理由も Java と似ています。1995 年に Netscape 社が初めて JavaScript 言語を公開した当時、Unicode はまだ発展初期にあり、16 ビットの符号化で十分すべての Unicode 文字を表せると考えられていました。
  • C# が UTF-16 エンコーディングを使う主な理由は、.NET プラットフォームが Microsoft によって設計され、Microsoft の多くの技術(Windows オペレーティングシステムを含む)で UTF-16 エンコーディングが広く使われているためです。

以上のプログラミング言語は文字数を過小評価していたため、16 ビットを超える長さの Unicode 文字を表すために「サロゲートペア」を採用せざるを得ませんでした。これはやむを得ない妥協策です。一方では、サロゲートペアを含む文字列では、1 文字が 2 バイトまたは 4 バイトを占める可能性があり、固定長エンコーディングの利点が失われます。もう一方では、サロゲートペアの処理には追加のコードが必要となり、プログラミングの複雑さとデバッグの難しさが増します。

こうした理由から、一部のプログラミング言語では別のエンコーディング方式が採用されました。

  • Python の str は Unicode エンコーディングを使用し、柔軟な文字列表現を採用しています。保存される文字の長さは、その文字列中で最大の Unicode コードポイントに依存します。文字列がすべて ASCII 文字であれば各文字は 1 バイト、ASCII の範囲を超える文字があってもすべてが基本多言語面(BMP)内であれば各文字は 2 バイト、BMP を超える文字があれば各文字は 4 バイトを占めます。
  • Go 言語の string 型は内部で UTF-8 エンコーディングを使用します。Go 言語には単一の Unicode コードポイントを表す rune 型も用意されています。
  • Rust 言語の strString 型は内部で UTF-8 エンコーディングを使用します。Rust にも単一の Unicode コードポイントを表す char 型があります。

注意すべきなのは、ここまでの議論はすべて、プログラミング言語内での文字列の保存方法についてであり、**文字列をファイルに保存したりネットワークで転送したりする方法とは別の問題である**ということです。ファイル保存やネットワーク転送では、通常、互換性と空間効率を最適化するために文字列を UTF-8 形式にエンコードします。

","path":["第 3 章   データ構造","3.4   文字エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   データ構造の分類","text":"

代表的なデータ構造には、配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフがあり、これらは「論理構造」と「物理構造」の 2 つの観点から分類できます。

","path":["第 3 章   データ構造","3.1   データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311","level":2,"title":"3.1.1   論理構造:線形と非線形","text":"

論理構造はデータ要素間の論理的な関係を示します。配列と連結リストでは、データは一定の順序で並び、データ間の線形関係を表します。一方、木ではデータは上から下へ階層的に並び、「祖先」と「子孫」の派生関係を示します。グラフはノードと辺で構成され、複雑なネットワーク関係を反映します。

以下の図に示すように、論理構造は「線形」と「非線形」の 2 つに大別できます。線形構造は比較的直感的で、データが論理関係において線形に並ぶことを指します。非線形構造はその逆で、非線形に配置されます。

  • 線形データ構造:配列、連結リスト、スタック、キュー、ハッシュテーブルであり、要素間は 1 対 1 の順序関係です。
  • 非線形データ構造:木、ヒープ、グラフ、ハッシュテーブル。

非線形データ構造は、さらに木構造と網状構造に分けられます。

  • 木構造:木、ヒープ、ハッシュテーブルであり、要素間は 1 対多の関係です。
  • 網状構造:グラフであり、要素間は多対多の関係です。

図 3-1   線形データ構造と非線形データ構造

","path":["第 3 章   データ構造","3.1   データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312","level":2,"title":"3.1.2   物理構造:連続と分散","text":"

アルゴリズムのプログラムが実行されるとき、処理中のデータは主にメモリに格納されます。下図はコンピュータのメモリモジュールを示しており、各黒い四角はそれぞれ 1 つのメモリ空間を表しています。メモリは巨大な Excel の表のようなものだと考えることができ、各セルには一定量のデータを格納できます。

システムはメモリアドレスを通じて目的の位置にあるデータへアクセスします。下図に示すように、コンピュータは特定の規則に従って表内の各セルに番号を割り当て、各メモリ空間が一意のメモリアドレスを持つようにします。これらのアドレスがあれば、プログラムはメモリ内のデータにアクセスできます。

図 3-2   メモリモジュール、メモリ空間、メモリアドレス

Tip

補足すると、メモリを Excel の表にたとえるのは単純化した比喩であり、実際のメモリの動作機構はより複雑で、アドレス空間、メモリ管理、キャッシュ機構、仮想メモリ、物理メモリなどの概念が関わります。

メモリはすべてのプログラムで共有される資源であり、あるメモリ領域が 1 つのプログラムに占有されると、通常は他のプログラムが同時に利用できません。したがって、データ構造とアルゴリズムの設計では、メモリ資源は重要な考慮要素です。たとえば、アルゴリズムが使用するメモリ使用量のピークは、システムに残っている空きメモリを超えてはなりません。大きな連続メモリ領域が不足している場合、選択するデータ構造は分散したメモリ空間に格納できる必要があります。

下図に示すように、物理構造はデータがコンピュータメモリ内にどのように格納されるかを表します。これは連続空間への格納(配列)と分散空間への格納(連結リスト)に分けられます。物理構造は低レベルでデータのアクセス、更新、追加、削除などの操作方法を決定し、2 種類の物理構造は時間効率と空間効率の面で相補的な特徴を持ちます。

図 3-3   連続空間格納と分散空間格納

補足すると、すべてのデータ構造は配列、連結リスト、またはその両者の組み合わせに基づいて実装されます。たとえば、スタックとキューは配列でも連結リストでも実装できます。一方、ハッシュテーブルの実装には配列と連結リストの両方が含まれる場合があります。

  • 配列に基づいて実装可能:スタック、キュー、ハッシュテーブル、木、ヒープ、グラフ、行列、テンソル(次元 \\(\\geq 3\\) の配列)など。
  • 連結リストに基づいて実装可能:スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなど。

連結リストは初期化後も、プログラムの実行中に長さを調整できるため、「動的データ構造」とも呼ばれます。配列は初期化後に長さを変更できないため、「静的データ構造」とも呼ばれます。なお、配列もメモリを再割り当てすることで長さを変更でき、ある程度の「動的性」を持たせることができます。

Tip

物理構造の理解が難しいと感じる場合は、先に次の章を読んでから本節を振り返ることを勧めます。

","path":["第 3 章   データ構造","3.1   データ構造の分類"],"tags":[]},{"location":"chapter_data_structure/number_encoding/","level":1,"title":"3.3   数値エンコーディング *","text":"

Tip

本書では、タイトルに * 記号が付いている章は選読です。時間が限られている場合や理解が難しいと感じる場合は、いったん読み飛ばし、必読章を終えてから個別に取り組んでください。

","path":["第 3 章   データ構造","3.3   数値エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#331-1-2","level":2,"title":"3.3.1   符号付き絶対値表現、1 の補数、2 の補数","text":"

前節の表を見ると、すべての整数型で表せる負数の個数は正数より 1 つ多く、たとえば byte の値域は \\([-128, 127]\\) です。この現象は直感に反するように見えますが、その背景には符号付き絶対値表現、1 の補数、2 の補数に関する知識があります。

まず押さえておくべきなのは、**数値はコンピュータ内で「2 の補数」の形で保存される**ということです。その理由を説明する前に、まずはこの 3 つの定義を示します。

  • 符号付き絶対値表現:数値の二進表現の最上位ビットを符号ビットとみなし、\\(0\\) は正数、\\(1\\) は負数を表し、残りのビットが数値の値を表します。
  • 1 の補数:正数の 1 の補数は符号付き絶対値表現と同じで、負数の 1 の補数は符号ビットを除くすべてのビットを反転したものです。
  • 2 の補数:正数の 2 の補数は符号付き絶対値表現と同じで、負数の 2 の補数は 1 の補数に \\(1\\) を加えたものです。

下図は、符号付き絶対値表現、1 の補数、2 の補数の変換方法を示しています。

図 3-4   符号付き絶対値表現、1 の補数、2 の補数の相互変換

符号付き絶対値表現(sign-magnitude)は最も直感的ですが、いくつかの制約があります。まず、負数の符号付き絶対値表現はそのまま演算に使えません。たとえば符号付き絶対値表現で \\(1 + (-2)\\) を計算すると、結果は \\(-3\\) になってしまい、これは明らかに誤りです。

\\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline & = 1000 \\; 0011 \\newline & \\rightarrow -3 \\end{aligned} \\]

この問題を解決するために、コンピュータには1 の補数(1's complement)が導入されました。まず符号付き絶対値表現を 1 の補数に変換し、1 の補数で \\(1 + (-2)\\) を計算してから、結果を 1 の補数から符号付き絶対値表現へ戻すと、正しい結果 \\(-1\\) が得られます。

\\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 \\; \\text{(符号付き絶対値表現)} + 1000 \\; 0010 \\; \\text{(符号付き絶対値表現)} \\newline & = 0000 \\; 0001 \\; \\text{(1 の補数)} + 1111 \\; 1101 \\; \\text{(1 の補数)} \\newline & = 1111 \\; 1110 \\; \\text{(1 の補数)} \\newline & = 1000 \\; 0001 \\; \\text{(符号付き絶対値表現)} \\newline & \\rightarrow -1 \\end{aligned} \\]

一方、数値 0 の符号付き絶対値表現には \\(+0\\) と \\(-0\\) の 2 つの表し方があります。つまり、数値 0 に対して異なる 2 つの二進コードが対応しており、これは曖昧さの原因になります。たとえば条件判定で正のゼロと負のゼロを区別しないと、誤った判定結果になる可能性があります。また、この曖昧さを解消しようとすると追加の判定処理が必要になり、計算効率が下がるおそれがあります。

\\[ \\begin{aligned} +0 & \\rightarrow 0000 \\; 0000 \\newline -0 & \\rightarrow 1000 \\; 0000 \\end{aligned} \\]

符号付き絶対値表現と同様に、1 の補数にも正負のゼロの曖昧さがあります。そこでコンピュータはさらに2 の補数(2's complement)を導入しました。まずは負のゼロについて、符号付き絶対値表現、1 の補数、2 の補数の変換を見てみましょう。

\\[ \\begin{aligned} -0 \\rightarrow \\; & 1000 \\; 0000 \\; \\text{(符号付き絶対値表現)} \\newline = \\; & 1111 \\; 1111 \\; \\text{(1 の補数)} \\newline = 1 \\; & 0000 \\; 0000 \\; \\text{(2 の補数)} \\newline \\end{aligned} \\]

負のゼロの 1 の補数に \\(1\\) を加えると桁上がりが発生しますが、byte 型の長さは 8 ビットしかないため、第 9 ビットへあふれた \\(1\\) は捨てられます。つまり、負のゼロの 2 の補数は \\(0000 \\; 0000\\) であり、正のゼロの 2 の補数と同じです。そのため、2 の補数表現ではゼロは 1 つしか存在せず、正負のゼロの曖昧さは解消されます。

最後にもう 1 つ疑問が残ります。byte 型の値域は \\([-128, 127]\\) ですが、余分にある負数 \\(-128\\) はどのように得られるのでしょうか。区間 \\([-127, +127]\\) にあるすべての整数には、それぞれ対応する符号付き絶対値表現、1 の補数、2 の補数があり、符号付き絶対値表現と 2 の補数の間は相互に変換できます。

しかし、2 の補数 \\(1000 \\; 0000\\) だけは例外で、対応する符号付き絶対値表現を持ちません。変換規則に従うと、この 2 の補数に対応する符号付き絶対値表現は \\(0000 \\; 0000\\) になります。これは明らかに矛盾しています。なぜなら、この符号付き絶対値表現は数値 \\(0\\) を表し、その 2 の補数は自分自身であるはずだからです。コンピュータでは、この特別な 2 の補数 \\(1000 \\; 0000\\) を \\(-128\\) と定めています。実際、2 の補数での \\((-1) + (-127)\\) の計算結果はちょうど \\(-128\\) になります。

\\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\; 1111 \\; \\text{(符号付き絶対値表現)} + 1000 \\; 0001 \\; \\text{(符号付き絶対値表現)} \\newline & = 1000 \\; 0000 \\; \\text{(1 の補数)} + 1111 \\; 1110 \\; \\text{(1 の補数)} \\newline & = 1000 \\; 0001 \\; \\text{(2 の補数)} + 1111 \\; 1111 \\; \\text{(2 の補数)} \\newline & = 1000 \\; 0000 \\; \\text{(2 の補数)} \\newline & \\rightarrow -128 \\end{aligned} \\]

すでにお気づきかもしれませんが、上の計算はすべて加算です。これは重要な事実を示しています。**コンピュータ内部のハードウェア回路は、主として加算を基準に設計されている**のです。なぜなら、加算はほかの演算(乗算、除算、減算など)に比べてハードウェアで実装しやすく、並列化もしやすく、演算速度も速いからです。

ただし、これはコンピュータが加算しかできないという意味ではありません。加算といくつかの基本的な論理演算を組み合わせることで、コンピュータはさまざまな数学演算を実現できます。たとえば減算 \\(a - b\\) は加算 \\(a + (-b)\\) に変換できますし、乗算や除算も繰り返しの加算または減算に変換できます。

これで、コンピュータが 2 の補数を使う理由をまとめられます。2 の補数表現に基づけば、コンピュータは同じ回路と操作で正数と負数の加算を扱うことができ、減算専用の特別なハードウェア回路を設計する必要がなく、正負のゼロの曖昧さも特別に処理しなくて済みます。これにより、ハードウェア設計は大幅に簡略化され、演算効率も向上します。

2 の補数の設計は非常に巧妙ですが、紙幅の都合上ここまでにします。興味のある読者は、さらに深く調べてみてください。

","path":["第 3 章   データ構造","3.3   数値エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332","level":2,"title":"3.3.2   浮動小数点数のエンコーディング","text":"

注意深い人なら気づくかもしれません。intfloat はどちらも長さが 4 バイトで同じなのに、なぜ float の値域は int よりはるかに広いのでしょうか。これはかなり直感に反します。というのも、float は小数を表す必要があるので、本来なら値域は狭くなるはずだからです。

実際には、これは浮動小数点数 float が異なる表現方法を採用しているためです。32 ビット長の二進数を次のように表します。

\\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

IEEE 754 標準によれば、32-bit 長の float は次の 3 つの部分から構成されます。

  • 符号部 \\(\\mathrm{S}\\) :1 ビットを占め、\\(b_{31}\\) に対応します。
  • 指数部 \\(\\mathrm{E}\\) :8 ビットを占め、\\(b_{30} b_{29} \\ldots b_{23}\\) に対応します。
  • 仮数部 \\(\\mathrm{N}\\) :23 ビットを占め、\\(b_{22} b_{21} \\ldots b_0\\) に対応します。

二進数 float に対応する値は次式で計算されます。

\\[ \\text {val} = (-1)^{b_{31}} \\times 2^{\\left(b_{30} b_{29} \\ldots b_{23}\\right)_2-127} \\times\\left(1 . b_{22} b_{21} \\ldots b_0\\right)_2 \\]

十進数に直すと、計算式は次のようになります。

\\[ \\text {val}=(-1)^{\\mathrm{S}} \\times 2^{\\mathrm{E} -127} \\times (1 + \\mathrm{N}) \\]

各項の取り得る範囲は次のとおりです。

\\[ \\begin{aligned} \\mathrm{S} \\in & \\{ 0, 1\\}, \\quad \\mathrm{E} \\in \\{ 1, 2, \\dots, 254 \\} \\newline (1 + \\mathrm{N}) = & (1 + \\sum_{i=1}^{23} b_{23-i} 2^{-i}) \\subset [1, 2 - 2^{-23}] \\end{aligned} \\]

図 3-5   IEEE 754 標準における float の計算例

上図を見ると、例として \\(\\mathrm{S} = 0\\) 、 \\(\\mathrm{E} = 124\\) 、\\(\\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\\) が与えられた場合、次のようになります。

\\[ \\text { val } = (-1)^0 \\times 2^{124 - 127} \\times (1 + 0.375) = 0.171875 \\]

これで最初の疑問に答えられます。**float の表現方法には指数部が含まれているため、その値域は int よりはるかに広い**のです。上の計算より、float が表せる最大の正数は \\(2^{254 - 127} \\times (2 - 2^{-23}) \\approx 3.4 \\times 10^{38}\\) であり、符号ビットを切り替えれば最小の負数が得られます。

浮動小数点数 float は値域を広げる一方で、その代償として精度を犠牲にしています。整数型 int は 32 ビットすべてを数値の表現に使うため、数値は一様に分布します。しかし指数部があるため、浮動小数点数 float は値が大きくなるほど、隣り合う 2 つの数の差も大きくなる傾向があります。

次の表のとおり、指数部 \\(\\mathrm{E} = 0\\) と \\(\\mathrm{E} = 255\\) には特別な意味があり、ゼロ、無限大、\\(\\mathrm{NaN}\\) などを表すために使われます。

表 3-2   指数部の意味

指数部 E 仮数部 \\(\\mathrm{N} = 0\\) 仮数部 \\(\\mathrm{N} \\ne 0\\) 計算式 \\(0\\) \\(\\pm 0\\) 非正規化数 \\((-1)^{\\mathrm{S}} \\times 2^{-126} \\times (0.\\mathrm{N})\\) \\(1, 2, \\dots, 254\\) 正規化数 正規化数 \\((-1)^{\\mathrm{S}} \\times 2^{(\\mathrm{E} -127)} \\times (1.\\mathrm{N})\\) \\(255\\) \\(\\pm \\infty\\) \\(\\mathrm{NaN}\\)

なお、非正規化数によって浮動小数点数の精度は大きく向上します。最小の正の正規化数は \\(2^{-126}\\) であり、最小の正の非正規化数は \\(2^{-126} \\times 2^{-23}\\) です。

倍精度 doublefloat と同様の表現方法を採用しているため、ここでは詳述しません。

","path":["第 3 章   データ構造","3.3   数値エンコーディング *"],"tags":[]},{"location":"chapter_data_structure/summary/","level":1,"title":"3.5   まとめ","text":"","path":["第 3 章   データ構造","3.5   まとめ"],"tags":[]},{"location":"chapter_data_structure/summary/#1","level":3,"title":"1.   重要ポイントの振り返り","text":"
  • データ構造は、論理構造と物理構造という 2 つの観点から分類できます。論理構造はデータ要素間の論理的関係を記述し、物理構造はデータのコンピュータメモリ上での格納方法を記述します。
  • 代表的な論理構造には、線形、木構造、網状構造などがあります。通常、論理構造に基づいてデータ構造を線形(配列、連結リスト、スタック、キュー)と非線形(木、グラフ、ヒープ)の 2 種類に分類します。ハッシュテーブルの実装には、線形データ構造と非線形データ構造が同時に含まれる場合があります。
  • プログラムの実行時、データはコンピュータメモリに格納されます。各メモリ空間には対応するメモリアドレスがあり、プログラムはそれらのメモリアドレスを通じてデータにアクセスします。
  • 物理構造は主に連続領域への格納(配列)と分散領域への格納(連結リスト)に分けられます。すべてのデータ構造は、配列、連結リスト、またはその両方の組み合わせによって実装されます。
  • コンピュータにおける基本データ型には、整数 byteshortintlong、浮動小数点数 floatdouble、文字 char、真偽値 bool があります。これらの値域は、使用する記憶領域の大きさと表現方式によって決まります。
  • 符号付き絶対値表現、1 の補数、2 の補数は、コンピュータで数値を符号化する 3 つの方法であり、相互に変換できます。整数の符号付き絶対値表現では最上位ビットが符号ビットで、残りのビットが数値の値です。
  • 整数はコンピュータ内では 2 の補数の形式で格納されます。2 の補数表現では、コンピュータは正数と負数の加算を同じように扱うことができ、減算のために特別なハードウェア回路を別途設計する必要がなく、さらに正負のゼロが重複する問題もありません。
  • 浮動小数点数の符号化は、1 ビットの符号部、8 ビットの指数部、23 ビットの仮数部で構成されます。指数部があるため、浮動小数点数の値域は整数よりはるかに広くなりますが、その代償として精度が犠牲になります。
  • ASCII コードは最も早く登場した英字文字集合で、長さは 1 バイト、収録文字数は 127 です。GBK 文字集合はよく使われる中国語文字集合で、2 万字以上の漢字を収録しています。Unicode は完全な文字集合標準を提供することを目指しており、世界中のさまざまな言語の文字を収録することで、文字コード方式の不一致によって生じる文字化けの問題を解決します。
  • UTF-8 は最も広く使われている Unicode の符号化方式で、汎用性が非常に高いです。可変長の符号化方式であり、拡張性に優れ、記憶領域の利用効率を効果的に高めます。UTF-16 と UTF-32 は固定長の符号化方式です。中国語を符号化する場合、UTF-16 は UTF-8 よりも使用領域が小さくなります。Java や C# などのプログラミング言語は、デフォルトで UTF-16 を使用します。
","path":["第 3 章   データ構造","3.5   まとめ"],"tags":[]},{"location":"chapter_data_structure/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:なぜハッシュテーブルには線形データ構造と非線形データ構造が同時に含まれるのですか?

ハッシュテーブルの基盤は配列であり、ハッシュ衝突を解決するために「チェイン法」(後続の「ハッシュ衝突」の章で説明します)を使うことがあります。配列内の各バケットは 1 つの連結リストを指し、その連結リストの長さがある閾値を超えると、木(通常は赤黒木)に変換されることもあります。

格納の観点から見ると、ハッシュテーブルの基盤は配列であり、各バケットスロットには値が入ることもあれば、連結リストや木が入ることもあります。したがって、ハッシュテーブルには線形データ構造(配列、連結リスト)と非線形データ構造(木)が同時に含まれる場合があります。

Q:char 型の長さは 1 バイトですか?

char 型の長さは、プログラミング言語が採用する符号化方式によって決まります。たとえば、Java、JavaScript、TypeScript、C# はいずれも UTF-16 符号化(Unicode コードポイントを保持)を採用しているため、char 型の長さは 2 バイトです。

Q:配列ベースで実装されたデータ構造を「静的データ構造」と呼ぶのは曖昧ではありませんか? スタックも push や pop などの操作ができ、これらの操作はどれも「動的」です。

スタックは確かに動的なデータ操作を実現できますが、データ構造自体は依然として「静的」(長さが不変)です。配列ベースのデータ構造でも要素を動的に追加または削除できますが、その容量は固定です。データ量が事前に確保した大きさを超えた場合は、より大きな新しい配列を作成し、古い配列の内容を新しい配列にコピーする必要があります。

Q:スタック(キュー)を構築するときにサイズを指定していないのに、なぜそれらは「静的データ構造」なのですか?

高水準プログラミング言語では、スタック(キュー)の初期容量を人手で指定する必要はなく、この作業はクラス内部で自動的に行われます。たとえば、Java の ArrayList の初期容量は通常 10 です。また、容量拡張も自動的に実装されています。詳しくは後続の「リスト」の章を参照してください。

Q:符号付き絶対値表現から 2 の補数への変換方法は「先にビット反転してから 1 を加える」ですが、2 の補数から符号付き絶対値表現への変換は逆演算である「先に 1 を引いてからビット反転する」べきなのに、同じく「先にビット反転してから 1 を加える」でも求められます。これはなぜですか?

これは、符号付き絶対値表現と 2 の補数の相互変換が、実際には「補数」を計算する過程だからです。まず補数の定義を示します。\\(a + b = c\\) とすると、\\(a\\) を \\(b\\) から \\(c\\) への補数と呼び、逆に \\(b\\) も \\(a\\) から \\(c\\) への補数と呼びます。

長さ \\(n = 4\\) ビットの 2 進数 \\(0010\\) が与えられたとします。この数を符号付き絶対値表現(符号ビットは考慮しない)とみなすと、その 2 の補数は「先にビット反転してから 1 を加える」ことで得られます。

\\[ 0010 \\rightarrow 1101 \\rightarrow 1110 \\]

ここで、符号付き絶対値表現と 2 の補数の和は \\(0010 + 1110 = 10000\\) となります。つまり、2 の補数 \\(1110\\) は符号付き絶対値表現 \\(0010\\) から \\(10000\\) への「補数」です。これは、上記の「先にビット反転してから 1 を加える」が、実際には \\(10000\\) への補数を計算する過程であることを意味します。

では、2 の補数 \\(1110\\) から \\(10000\\) への「補数」はいくつでしょうか。これもやはり「先にビット反転してから 1 を加える」ことで求められます。

\\[ 1110 \\rightarrow 0001 \\rightarrow 0010 \\]

言い換えると、符号付き絶対値表現と 2 の補数は互いに相手から \\(10000\\) への「補数」なので、「符号付き絶対値表現から 2 の補数への変換」と「2 の補数から符号付き絶対値表現への変換」は同じ操作(先にビット反転してから 1 を加える)で実現できます。

もちろん、逆演算を用いて 2 の補数 \\(1110\\) の符号付き絶対値表現を求めることもでき、その場合は「先に 1 を引いてからビット反転する」ことになります。

\\[ 1110 \\rightarrow 1101 \\rightarrow 0010 \\]

まとめると、「先にビット反転してから 1 を加える」と「先に 1 を引いてからビット反転する」の 2 つの演算は、どちらも \\(10000\\) への補数を計算しており、等価です。

本質的には、「ビット反転」という操作は実際には \\(1111\\) への補数を求めています(常に 符号付き絶対値表現 + 1 の補数 = 1111 が成り立つため)。そして、1 の補数にさらに 1 を加えて得られる 2 の補数が、\\(10000\\) への補数です。

上記では \\(n = 4\\) を例にしましたが、この考え方は任意のビット長の 2 進数に一般化できます。

","path":["第 3 章   データ構造","3.5   まとめ"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"第 12 章   分割統治","text":"

Abstract

難題は段階的に分解され、そのたびにより単純になっていく。

分割統治は一つの重要な事実を示している。単純なことから始めれば、すべてはもはや複雑ではない。

","path":["第 12 章   分割統治"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"章の内容","text":"
  • 12.1   分割統治法
  • 12.2   分割統治探索戦略
  • 12.3   二分木の構築問題
  • 12.4   ハノイの塔の問題
  • 12.5   まとめ
","path":["第 12 章   分割統治"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/","level":1,"title":"12.2   分割統治探索戦略","text":"

私たちはすでに学んだように、探索アルゴリズムは大きく二つに分けられる。

  • 力ずく探索:データ構造を走査することで実現され、時間計算量は \\(O(n)\\) である。
  • 適応的探索:固有のデータ構造や事前情報を利用し、時間計算量は \\(O(\\log n)\\) 、さらには \\(O(1)\\) に達しうる。

実際、時間計算量が \\(O(\\log n)\\) の探索アルゴリズムは通常、分割統治戦略に基づいて実装される。たとえば二分探索や木構造である。

  • 二分探索の各ステップでは、問題(配列内で目標要素を探索すること)を小さな問題(配列の半分で目標要素を探索すること)に分解し、この過程は配列が空になるか目標要素が見つかるまで続く。
  • 木構造は分割統治の考え方を代表するものであり、二分探索木、AVL 木、ヒープなどのデータ構造では、さまざまな操作の時間計算量はいずれも \\(O(\\log n)\\) である。

二分探索の分割統治戦略は以下のとおりである。

  • 問題は分解できる:二分探索は、元の問題(配列内で探索すること)を部分問題(配列の半分で探索すること)へ再帰的に分解する。これは中央要素と目標要素を比較することで実現される。
  • 部分問題は独立している:二分探索では、各ラウンドで一つの部分問題だけを処理し、ほかの部分問題の影響を受けない。
  • 部分問題の解を統合する必要はない:二分探索は特定の要素を探すことを目的としているため、部分問題の解を統合する必要がない。部分問題が解決されると、元の問題も同時に解決される。

分割統治が探索効率を高められる本質的な理由は、力ずく探索では各ラウンドで一つの候補しか除外できないのに対し、**分割統治による探索では各ラウンドで候補の半分を除外できる**からである。

","path":["第 12 章   分割統治","12.2   分割統治探索戦略"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/#1","level":3,"title":"1.   分割統治に基づく二分探索","text":"

前の章では、二分探索を漸化式(反復)に基づいて実装した。ここでは分割統治(再帰)に基づいてこれを実装する。

Question

長さ \\(n\\) の昇順配列 nums が与えられ、そのすべての要素は一意である。要素 target を探索せよ。

分割統治の観点から、探索区間 \\([i, j]\\) に対応する部分問題を \\(f(i, j)\\) と記す。

元の問題 \\(f(0, n-1)\\) を出発点として、次の手順で二分探索を行う。

  1. 探索区間 \\([i, j]\\) の中点 \\(m\\) を計算し、それに基づいて探索区間の半分を除外する。
  2. 規模が半分に縮小された部分問題を再帰的に解く。候補は \\(f(i, m-1)\\) または \\(f(m+1, j)\\) である。
  3. 1.2. の手順を繰り返し、target が見つかるか区間が空になったら返す。

次の図は、配列内で要素 \\(6\\) を二分探索する分割統治の過程を示している。

図 12-4   二分探索の分割統治の過程

実装コードでは、再帰関数 dfs() を宣言して問題 \\(f(i, j)\\) を解く。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_recur.py
def dfs(nums: list[int], target: int, i: int, j: int) -> int:\n    \"\"\"二分探索:問題 f(i, j)\"\"\"\n    # 区間が空なら対象要素は存在しないので -1 を返す\n    if i > j:\n        return -1\n    # 中点インデックス m を計算\n    m = (i + j) // 2\n    if nums[m] < target:\n        # 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j)\n    elif nums[m] > target:\n        # 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1)\n    else:\n        # 目標要素が見つかったらそのインデックスを返す\n        return m\n\ndef binary_search(nums: list[int], target: int) -> int:\n    \"\"\"二分探索\"\"\"\n    n = len(nums)\n    # 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1)\n
binary_search_recur.cpp
/* 二分探索:問題 f(i, j) */\nint dfs(vector<int> &nums, int target, int i, int j) {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nint binarySearch(vector<int> &nums, int target) {\n    int n = nums.size();\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.java
/* 二分探索:問題 f(i, j) */\nint dfs(int[] nums, int target, int i, int j) {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nint binarySearch(int[] nums, int target) {\n    int n = nums.length;\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.cs
/* 二分探索:問題 f(i, j) */\nint DFS(int[] nums, int target, int i, int j) {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return DFS(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return DFS(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nint BinarySearch(int[] nums, int target) {\n    int n = nums.Length;\n    // 問題 f(0, n-1) を解く\n    return DFS(nums, target, 0, n - 1);\n}\n
binary_search_recur.go
/* 二分探索:問題 f(i, j) */\nfunc dfs(nums []int, target, i, j int) int {\n    // 区間が空なら対象要素は存在しないため、-1 を返す\n    if i > j {\n        return -1\n    }\n    // 中点インデックスを計算する\n    m := i + ((j - i) >> 1)\n    // 中点の要素と目標要素の大小を判定する\n    if nums[m] < target {\n        // 小さければ右半分の配列を再帰\n        // 部分問題 f(m+1, j) を解く\n        return dfs(nums, target, m+1, j)\n    } else if nums[m] > target {\n        // 大きければ左半分の配列を再帰\n        // 部分問題 f(i, m-1) を解く\n        return dfs(nums, target, i, m-1)\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m\n    }\n}\n\n/* 二分探索 */\nfunc binarySearch(nums []int, target int) int {\n    n := len(nums)\n    return dfs(nums, target, 0, n-1)\n}\n
binary_search_recur.swift
/* 二分探索:問題 f(i, j) */\nfunc dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if i > j {\n        return -1\n    }\n    // 中点インデックス m を計算\n    let m = (i + j) / 2\n    if nums[m] < target {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums: nums, target: target, i: m + 1, j: j)\n    } else if nums[m] > target {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums: nums, target: target, i: i, j: m - 1)\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m\n    }\n}\n\n/* 二分探索 */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // 問題 f(0, n-1) を解く\n    dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)\n}\n
binary_search_recur.js
/* 二分探索:問題 f(i, j) */\nfunction dfs(nums, target, i, j) {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nfunction binarySearch(nums, target) {\n    const n = nums.length;\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.ts
/* 二分探索:問題 f(i, j) */\nfunction dfs(nums: number[], target: number, i: number, j: number): number {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nfunction binarySearch(nums: number[], target: number): number {\n    const n = nums.length;\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.dart
/* 二分探索:問題 f(i, j) */\nint dfs(List<int> nums, int target, int i, int j) {\n  // 区間が空なら対象要素は存在しないので -1 を返す\n  if (i > j) {\n    return -1;\n  }\n  // 中点インデックス m を計算\n  int m = (i + j) ~/ 2;\n  if (nums[m] < target) {\n    // 部分問題 f(m+1, j) を再帰的に解く\n    return dfs(nums, target, m + 1, j);\n  } else if (nums[m] > target) {\n    // 部分問題 f(i, m-1) を再帰的に解く\n    return dfs(nums, target, i, m - 1);\n  } else {\n    // 目標要素が見つかったらそのインデックスを返す\n    return m;\n  }\n}\n\n/* 二分探索 */\nint binarySearch(List<int> nums, int target) {\n  int n = nums.length;\n  // 問題 f(0, n-1) を解く\n  return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.rs
/* 二分探索:問題 f(i, j) */\nfn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if i > j {\n        return -1;\n    }\n    let m: i32 = i + (j - i) / 2;\n    if nums[m as usize] < target {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if nums[m as usize] > target {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    let n = nums.len() as i32;\n    // 問題 f(0, n-1) を解く\n    dfs(nums, target, 0, n - 1)\n}\n
binary_search_recur.c
/* 二分探索:問題 f(i, j) */\nint dfs(int nums[], int target, int i, int j) {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1;\n    }\n    // 中点インデックス m を計算\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        return m;\n    }\n}\n\n/* 二分探索 */\nint binarySearch(int nums[], int target, int numsSize) {\n    int n = numsSize;\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.kt
/* 二分探索:問題 f(i, j) */\nfun dfs(\n    nums: IntArray,\n    target: Int,\n    i: Int,\n    j: Int\n): Int {\n    // 区間が空なら対象要素は存在しないので -1 を返す\n    if (i > j) {\n        return -1\n    }\n    // 中点インデックス m を計算\n    val m = (i + j) / 2\n    return if (nums[m] < target) {\n        // 部分問題 f(m+1, j) を再帰的に解く\n        dfs(nums, target, m + 1, j)\n    } else if (nums[m] > target) {\n        // 部分問題 f(i, m-1) を再帰的に解く\n        dfs(nums, target, i, m - 1)\n    } else {\n        // 目標要素が見つかったらそのインデックスを返す\n        m\n    }\n}\n\n/* 二分探索 */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    val n = nums.size\n    // 問題 f(0, n-1) を解く\n    return dfs(nums, target, 0, n - 1)\n}\n
binary_search_recur.rb
### 二分探索: 問題 f(i, j) ###\ndef dfs(nums, target, i, j)\n  # 区間が空なら対象要素は存在しないので -1 を返す\n  return -1 if i > j\n\n  # 中点インデックス m を計算\n  m = (i + j) / 2\n\n  if nums[m] < target\n    # 部分問題 f(m+1, j) を再帰的に解く\n    return dfs(nums, target, m + 1, j)\n  elsif nums[m] > target\n    # 部分問題 f(i, m-1) を再帰的に解く\n    return dfs(nums, target, i, m - 1)\n  else\n    # 目標要素が見つかったらそのインデックスを返す\n    return m\n  end\nend\n\n### 二分探索 ###\ndef binary_search(nums, target)\n  n = nums.length\n  # 問題 f(0, n-1) を解く\n  dfs(nums, target, 0, n - 1)\nend\n
コードの可視化

全画面で見る >

","path":["第 12 章   分割統治","12.2   分割統治探索戦略"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/","level":1,"title":"12.3   二分木の構築問題","text":"

Question

二分木の前順走査 preorder と中順走査 inorder が与えられたとき、これらから二分木を構築し、その根ノードを返してください。二分木には値が重複するノードが存在しないものとします(下図のとおり)。

図 12-5   二分木を構築する例のデータ

","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1","level":3,"title":"1.   分割統治問題かどうかを判断する","text":"

元の問題は preorderinorder から二分木を構築することであり、典型的な分割統治問題です。

  • 問題は分解できる:分割統治の観点から見ると、元の問題は 2 つの部分問題、すなわち左部分木の構築と右部分木の構築に分けられ、さらに根ノードを初期化する 1 ステップが加わります。各部分木(部分問題)に対しても、同じ分割方法を再利用してより小さな部分木(部分問題)へと分けていき、最小の部分問題(空部分木)に達した時点で終了します。
  • 部分問題は独立している:左部分木と右部分木は互いに独立しており、両者の間に重なりはありません。左部分木を構築するときは、中順走査と前順走査のうち左部分木に対応する部分だけを見れば十分です。右部分木も同様です。
  • 部分問題の解は統合できる:左部分木と右部分木(部分問題の解)が得られたら、それらを根ノードに接続することで元の問題の解を得られます。
","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   部分木をどのように分割するか","text":"

以上の分析より、この問題は分割統治で解けます。では、前順走査 preorder と中順走査 inorder を使って左部分木と右部分木をどのように分割すればよいのでしょうか?

定義に従うと、preorderinorder はいずれも 3 つの部分に分けられます。

  • 前順走査:[ 根ノード | 左部分木 | 右部分木 ] ,例えば上図の木は [ 3 | 9 | 2 1 7 ] に対応します。
  • 中順走査:[ 左部分木 | 根ノード | 右部分木 ] ,例えば上図の木は [ 9 | 3 | 1 2 7 ] に対応します。

上図のデータを例にすると、下図の手順によって分割結果を得られます。

  1. 前順走査の先頭要素 3 が根ノードの値です。
  2. 根ノード 3 の inorder におけるインデックスを探すと、そのインデックスを用いて inorder[ 9 | 3 | 1 2 7 ] に分割できます。
  3. inorder の分割結果から、左部分木と右部分木のノード数はそれぞれ 1 と 3 であることがわかり、したがって preorder[ 3 | 9 | 2 1 7 ] に分割できます。

図 12-6   前順走査と中順走査で部分木を分割する

","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#3","level":3,"title":"3.   変数を用いて部分木区間を記述する","text":"

以上の分割方法により、**根ノード、左部分木、右部分木が preorderinorder の中で占めるインデックス区間**が得られました。これらのインデックス区間を表すために、いくつかのポインタ変数を導入します。

  • 現在の木の根ノードが preorder に現れるインデックスを \\(i\\) とします。
  • 現在の木の根ノードが inorder に現れるインデックスを \\(m\\) とします。
  • 現在の木が inorder において占めるインデックス区間を \\([l, r]\\) とします。

次の表のように、これらの変数を用いれば根ノードの preorder におけるインデックスと、部分木の inorder におけるインデックス区間を表せます。

表 12-1   根ノードと部分木の前順走査・中順走査におけるインデックス

根ノードの preorder におけるインデックス 部分木の inorder におけるインデックス区間 現在の木 \\(i\\) \\([l, r]\\) 左部分木 \\(i + 1\\) \\([l, m-1]\\) 右部分木 \\(i + 1 + (m - l)\\) \\([m+1, r]\\)

右部分木の根ノードのインデックスにある \\((m-l)\\) は「左部分木のノード数」を意味します。下図と合わせて理解することを勧めます。

図 12-7   根ノードと左右部分木のインデックス区間の表し方

","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#4","level":3,"title":"4.   コードの実装","text":"

\\(m\\) の検索効率を高めるために、ハッシュテーブル hmap を用いて配列 inorder の要素からインデックスへの対応を保存します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby build_tree.py
def dfs(\n    preorder: list[int],\n    inorder_map: dict[int, int],\n    i: int,\n    l: int,\n    r: int,\n) -> TreeNode | None:\n    \"\"\"二分木を構築:分割統治\"\"\"\n    # 部分木区間が空なら終了する\n    if r - l < 0:\n        return None\n    # ルートノードを初期化する\n    root = TreeNode(preorder[i])\n    # m を求めて左右部分木を分割する\n    m = inorder_map[preorder[i]]\n    # 部分問題:左部分木を構築する\n    root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n    # 部分問題:右部分木を構築する\n    root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n    # 根ノードを返す\n    return root\n\ndef build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:\n    \"\"\"二分木を構築\"\"\"\n    # inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    inorder_map = {val: i for i, val in enumerate(inorder)}\n    root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)\n    return root\n
build_tree.cpp
/* 二分木を構築:分割統治 */\nTreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {\n    // 部分木区間が空なら終了する\n    if (r - l < 0)\n        return NULL;\n    // ルートノードを初期化する\n    TreeNode *root = new TreeNode(preorder[i]);\n    // m を求めて左右部分木を分割する\n    int m = inorderMap[preorder[i]];\n    // 部分問題:左部分木を構築する\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nTreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    unordered_map<int, int> inorderMap;\n    for (int i = 0; i < inorder.size(); i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);\n    return root;\n}\n
build_tree.java
/* 二分木を構築:分割統治 */\nTreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {\n    // 部分木区間が空なら終了する\n    if (r - l < 0)\n        return null;\n    // ルートノードを初期化する\n    TreeNode root = new TreeNode(preorder[i]);\n    // m を求めて左右部分木を分割する\n    int m = inorderMap.get(preorder[i]);\n    // 部分問題:左部分木を構築する\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nTreeNode buildTree(int[] preorder, int[] inorder) {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    Map<Integer, Integer> inorderMap = new HashMap<>();\n    for (int i = 0; i < inorder.length; i++) {\n        inorderMap.put(inorder[i], i);\n    }\n    TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.cs
/* 二分木を構築:分割統治 */\nTreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {\n    // 部分木区間が空なら終了する\n    if (r - l < 0)\n        return null;\n    // ルートノードを初期化する\n    TreeNode root = new(preorder[i]);\n    // m を求めて左右部分木を分割する\n    int m = inorderMap[preorder[i]];\n    // 部分問題:左部分木を構築する\n    root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nTreeNode? BuildTree(int[] preorder, int[] inorder) {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    Dictionary<int, int> inorderMap = [];\n    for (int i = 0; i < inorder.Length; i++) {\n        inorderMap.TryAdd(inorder[i], i);\n    }\n    TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);\n    return root;\n}\n
build_tree.go
/* 二分木を構築:分割統治 */\nfunc dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {\n    // 部分木区間が空なら終了する\n    if r-l < 0 {\n        return nil\n    }\n    // ルートノードを初期化する\n    root := NewTreeNode(preorder[i])\n    // m を求めて左右部分木を分割する\n    m := inorderMap[preorder[i]]\n    // 部分問題:左部分木を構築する\n    root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)\n    // 部分問題:右部分木を構築する\n    root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)\n    // 根ノードを返す\n    return root\n}\n\n/* 二分木を構築 */\nfunc buildTree(preorder, inorder []int) *TreeNode {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    inorderMap := make(map[int]int, len(inorder))\n    for i := 0; i < len(inorder); i++ {\n        inorderMap[inorder[i]] = i\n    }\n\n    root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)\n    return root\n}\n
build_tree.swift
/* 二分木を構築:分割統治 */\nfunc dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {\n    // 部分木区間が空なら終了する\n    if r - l < 0 {\n        return nil\n    }\n    // ルートノードを初期化する\n    let root = TreeNode(x: preorder[i])\n    // m を求めて左右部分木を分割する\n    let m = inorderMap[preorder[i]]!\n    // 部分問題:左部分木を構築する\n    root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)\n    // 部分問題:右部分木を構築する\n    root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)\n    // 根ノードを返す\n    return root\n}\n\n/* 二分木を構築 */\nfunc buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }\n    return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)\n}\n
build_tree.js
/* 二分木を構築:分割統治 */\nfunction dfs(preorder, inorderMap, i, l, r) {\n    // 部分木区間が空なら終了する\n    if (r - l < 0) return null;\n    // ルートノードを初期化する\n    const root = new TreeNode(preorder[i]);\n    // m を求めて左右部分木を分割する\n    const m = inorderMap.get(preorder[i]);\n    // 部分問題:左部分木を構築する\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nfunction buildTree(preorder, inorder) {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    let inorderMap = new Map();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.ts
/* 二分木を構築:分割統治 */\nfunction dfs(\n    preorder: number[],\n    inorderMap: Map<number, number>,\n    i: number,\n    l: number,\n    r: number\n): TreeNode | null {\n    // 部分木区間が空なら終了する\n    if (r - l < 0) return null;\n    // ルートノードを初期化する\n    const root: TreeNode = new TreeNode(preorder[i]);\n    // m を求めて左右部分木を分割する\n    const m = inorderMap.get(preorder[i]);\n    // 部分問題:左部分木を構築する\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nfunction buildTree(preorder: number[], inorder: number[]): TreeNode | null {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    let inorderMap = new Map<number, number>();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.dart
/* 二分木を構築:分割統治 */\nTreeNode? dfs(\n  List<int> preorder,\n  Map<int, int> inorderMap,\n  int i,\n  int l,\n  int r,\n) {\n  // 部分木区間が空なら終了する\n  if (r - l < 0) {\n    return null;\n  }\n  // ルートノードを初期化する\n  TreeNode? root = TreeNode(preorder[i]);\n  // m を求めて左右部分木を分割する\n  int m = inorderMap[preorder[i]]!;\n  // 部分問題:左部分木を構築する\n  root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n  // 部分問題:右部分木を構築する\n  root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n  // 根ノードを返す\n  return root;\n}\n\n/* 二分木を構築 */\nTreeNode? buildTree(List<int> preorder, List<int> inorder) {\n  // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n  Map<int, int> inorderMap = {};\n  for (int i = 0; i < inorder.length; i++) {\n    inorderMap[inorder[i]] = i;\n  }\n  TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n  return root;\n}\n
build_tree.rs
/* 二分木を構築:分割統治 */\nfn dfs(\n    preorder: &[i32],\n    inorder_map: &HashMap<i32, i32>,\n    i: i32,\n    l: i32,\n    r: i32,\n) -> Option<Rc<RefCell<TreeNode>>> {\n    // 部分木区間が空なら終了する\n    if r - l < 0 {\n        return None;\n    }\n    // ルートノードを初期化する\n    let root = TreeNode::new(preorder[i as usize]);\n    // m を求めて左右部分木を分割する\n    let m = inorder_map.get(&preorder[i as usize]).unwrap();\n    // 部分問題:左部分木を構築する\n    root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);\n    // 部分問題:右部分木を構築する\n    root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);\n    // 根ノードを返す\n    Some(root)\n}\n\n/* 二分木を構築 */\nfn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    let mut inorder_map: HashMap<i32, i32> = HashMap::new();\n    for i in 0..inorder.len() {\n        inorder_map.insert(inorder[i], i as i32);\n    }\n    let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);\n    root\n}\n
build_tree.c
/* 二分木を構築:分割統治 */\nTreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {\n    // 部分木区間が空なら終了する\n    if (r - l < 0)\n        return NULL;\n    // ルートノードを初期化する\n    TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));\n    root->val = preorder[i];\n    root->left = NULL;\n    root->right = NULL;\n    // m を求めて左右部分木を分割する\n    int m = inorderMap[preorder[i]];\n    // 部分問題:左部分木を構築する\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);\n    // 部分問題:右部分木を構築する\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);\n    // 根ノードを返す\n    return root;\n}\n\n/* 二分木を構築 */\nTreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);\n    for (int i = 0; i < inorderSize; i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);\n    free(inorderMap);\n    return root;\n}\n
build_tree.kt
/* 二分木を構築:分割統治 */\nfun dfs(\n    preorder: IntArray,\n    inorderMap: Map<Int?, Int?>,\n    i: Int,\n    l: Int,\n    r: Int\n): TreeNode? {\n    // 部分木区間が空なら終了する\n    if (r - l < 0) return null\n    // ルートノードを初期化する\n    val root = TreeNode(preorder[i])\n    // m を求めて左右部分木を分割する\n    val m = inorderMap[preorder[i]]!!\n    // 部分問題:左部分木を構築する\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)\n    // 部分問題:右部分木を構築する\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)\n    // 根ノードを返す\n    return root\n}\n\n/* 二分木を構築 */\nfun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {\n    // inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n    val inorderMap = HashMap<Int?, Int?>()\n    for (i in inorder.indices) {\n        inorderMap[inorder[i]] = i\n    }\n    val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)\n    return root\n}\n
build_tree.rb
### 二分木を構築:分割統治 ###\ndef dfs(preorder, inorder_map, i, l, r)\n  # 部分木区間が空なら終了する\n  return if r - l < 0\n\n  # ルートノードを初期化する\n  root = TreeNode.new(preorder[i])\n  # m を求めて左右部分木を分割する\n  m = inorder_map[preorder[i]]\n  # 部分問題:左部分木を構築する\n  root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n  # 部分問題:右部分木を構築する\n  root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n\n  # 根ノードを返す\n  root\nend\n\n### 二分木を構築 ###\ndef build_tree(preorder, inorder)\n  # inorder の要素からインデックスへの対応を格納するハッシュテーブルを初期化する\n  inorder_map = {}\n  inorder.each_with_index { |val, i| inorder_map[val] = i }\n  dfs(preorder, inorder_map, 0, 0, inorder.length - 1)\nend\n
コードの可視化

全画面で見る >

下図は二分木を構築する再帰過程を示しています。各ノードは下向きに「再帰していく」過程で生成され、各辺(参照)は上向きに「戻る」過程で張られます。

<1><2><3><4><5><6><7><8><9>

図 12-8   二分木を構築する再帰過程

各再帰関数における前順走査 preorder と中順走査 inorder の分割結果を下図に示します。

図 12-9   各再帰関数での分割結果

木のノード数を \\(n\\) とすると、各ノードの初期化(再帰関数 dfs() の 1 回の実行)には \\(O(1)\\) 時間かかります。したがって、全体の時間計算量は \\(O(n)\\) です。

ハッシュテーブルには inorder の要素からインデックスへの対応を保存するため、空間計算量は \\(O(n)\\) です。最悪の場合、すなわち二分木が連結リストに退化すると、再帰の深さは \\(n\\) に達し、\\(O(n)\\) のスタックフレーム空間を使用します。したがって、全体の空間計算量は \\(O(n)\\) です。

","path":["第 12 章   分割統治","12.3   二分木の構築問題"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/","level":1,"title":"12.1   分割統治法","text":"

分割統治法(divide and conquer)は、問題を分けて統べるという意味であり、非常に重要で一般的なアルゴリズム戦略です。分割統治法は通常、再帰に基づいて実装され、「分」と「治」の 2 つのステップから構成されます。

  1. 分(分割段階):元の問題を 2 つ以上の部分問題へ再帰的に分解し、最小の部分問題に到達した時点で停止します。
  2. 治(統合段階):解が既知である最小の部分問題から始めて、部分問題の解を下から上へ統合し、元の問題の解を構築します。

以下の図に示すように、「マージソート」は分割統治戦略の典型的な応用の一つです。

  1. 分:元の配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割し、部分配列に要素が 1 つだけ残るまで続けます。
  2. 治:整列済みの部分配列(部分問題の解)を下から上へ統合し、整列済みの元の配列(元の問題の解)を得ます。

図 12-1   マージソートの分割統治戦略

","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1211","level":2,"title":"12.1.1   分割統治法の問題を見極めるには","text":"

ある問題が分割統治法で解くのに適しているかどうかは、通常、次の判断基準を参考にできます。

  1. 問題は分解できる:元の問題は、より小さく類似した部分問題に分解でき、同じ方法で再帰的に分割できます。
  2. 部分問題は独立している:部分問題同士に重複がなく、相互依存もないため、独立して解決できます。
  3. 部分問題の解は統合できる:元の問題の解は、部分問題の解を統合することで得られます。

明らかに、マージソートは以上の 3 つの判断基準を満たしています。

  1. 問題は分解できる:配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割します。
  2. 部分問題は独立している:各部分配列は独立にソートできます(部分問題は独立に解けます)。
  3. 部分問題の解は統合できる:2 つの整列済み部分配列(部分問題の解)は、1 つの整列済み配列(元の問題の解)に統合できます。
","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212","level":2,"title":"12.1.2   分割統治法で効率を高める","text":"

分割統治法はアルゴリズムの問題を効果的に解けるだけでなく、多くの場合アルゴリズムの効率も高められます。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソートが選択ソート、バブルソート、挿入ソートより高速ですが、これは分割統治戦略を適用しているためです。

ここで次の疑問が生じます。なぜ分割統治法はアルゴリズム効率を高められるのでしょうか。その根本的な仕組みは何でしょうか?言い換えると、大きな問題を複数の部分問題に分解し、部分問題を解き、それらの解を統合して元の問題の解にするという手順は、なぜ元の問題を直接解くより効率的なのでしょうか。この問題は、操作回数と並列計算の 2 つの観点から議論できます。

","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1","level":3,"title":"1.   操作回数の最適化","text":"

「バブルソート」を例に取ると、長さ \\(n\\) の配列を処理するのに \\(O(n^2)\\) の時間がかかります。以下の図のように、配列を中央で 2 つの部分配列に分けると仮定すると、分割には \\(O(n)\\) の時間、各部分配列のソートには \\(O((n / 2)^2)\\) の時間、2 つの部分配列の統合には \\(O(n)\\) の時間が必要で、全体の時間計算量は次のようになります:

\\[ O(n + (\\frac{n}{2})^2 \\times 2 + n) = O(\\frac{n^2}{2} + 2n) \\]

図 12-2   配列分割前後のバブルソート

次に、以下の不等式を計算します。左辺と右辺はそれぞれ、分割前と分割後の操作総数です:

\\[ \\begin{aligned} n^2 & > \\frac{n^2}{2} + 2n \\newline n^2 - \\frac{n^2}{2} - 2n & > 0 \\newline n(n - 4) & > 0 \\end{aligned} \\]

これは、\\(n > 4\\) のときに分割後の操作回数の方が少なくなり、ソート効率が高くなることを意味します。ただし、分割後の時間計算量は依然として 2 次の \\(O(n^2)\\) であり、計算量の定数項が小さくなっただけです。

さらに考えると、部分配列を中央からさらに 2 つの部分配列へと分割し続け、部分配列に要素が 1 つだけ残るまで分割を止めないとしたらどうでしょうか。この考え方がまさに「マージソート」であり、時間計算量は \\(O(n \\log n)\\) です。

さらに、分割点をいくつか増やして、元の配列を平均的に \\(k\\) 個の部分配列に分けるとしたらどうでしょうか。この状況は「バケットソート」と非常によく似ており、大量データのソートに非常に適しています。理論上の時間計算量は \\(O(n + k)\\) に達します。

","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#2","level":3,"title":"2.   並列計算の最適化","text":"

分割統治法で生成される部分問題は互いに独立しているため、通常は並列に解くことができます。つまり、分割統治法はアルゴリズムの時間計算量を下げられるだけでなく、オペレーティングシステムの並列最適化にも有利です。

並列最適化は、マルチコアまたはマルチプロセッサ環境で特に有効です。システムが複数の部分問題を同時に処理でき、計算資源をより十分に活用できるため、全体の実行時間を大幅に短縮できます。

たとえば、以下の図に示す「バケットソート」では、大量のデータを各バケットに均等に割り当てることで、すべてのバケットのソート処理を各計算ユニットに分散し、完了後に結果を統合できます。

図 12-3   バケットソートの並列計算

","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1213","level":2,"title":"12.1.3   分割統治法の代表的な応用","text":"

一方では、分割統治法は多くの古典的なアルゴリズム問題を解くのに使えます。

  • 最近点対探索:このアルゴリズムは、まず点集合を 2 つに分け、それぞれの部分における最近点対を求め、最後に 2 つの部分をまたぐ最近点対を求めます。
  • 大整数乗算:たとえば Karatsuba 法では、大整数の乗算を、より小さな整数どうしのいくつかの乗算と加算に分解します。
  • 行列乗算:たとえば Strassen 法では、大きな行列の乗算を、複数の小さな行列の乗算と加算に分解します。
  • ハノイの塔問題:ハノイの塔問題は再帰によって解くことができ、これは典型的な分割統治戦略の応用です。
  • 反転対の計算:ある数列で前の数が後ろの数より大きい場合、その 2 つの数は反転対を構成します。反転対の問題は、分割統治の考え方を利用し、マージソートを用いて解けます。

他方で、分割統治法はアルゴリズムとデータ構造の設計にも非常に広く応用されています。

  • 二分探索:二分探索では、整列済み配列を中央のインデックスで 2 つに分け、目標値と中央要素の比較結果に基づいてどちらの半区間を除外するかを決め、残った区間で同じ二分操作を行います。
  • マージソート:本節の冒頭で紹介したため、ここでは繰り返しません。
  • クイックソート:クイックソートは基準値を 1 つ選び、配列を、基準値より小さい要素の部分配列と、基準値より大きい要素の部分配列に分け、その後それぞれに対して同じ分割操作を行い、部分配列に要素が 1 つだけ残るまで続けます。
  • バケットソート:バケットソートの基本的な考え方は、データを複数のバケットに分散し、各バケット内の要素をソートしたうえで、各バケットの要素を順に取り出して整列済み配列を得ることです。
  • 木構造:たとえば二分探索木、AVL 木、赤黒木、B 木、B+ 木などでは、探索・挿入・削除などの操作をいずれも分割統治戦略の応用とみなせます。
  • ヒープ:ヒープは特殊な完全二分木であり、挿入、削除、ヒープ化などの各種操作には、実際には分割統治の考え方が含まれています。
  • ハッシュテーブル:ハッシュテーブル自体は分割統治を直接適用しているわけではありませんが、いくつかのハッシュ衝突解決法では間接的に分割統治戦略が使われています。たとえば、連鎖アドレス法における長い連結リストは、検索効率を高めるために赤黒木へ変換されます。

このように、**分割統治法は「静かに物を潤す」ようなアルゴリズム思想**であり、さまざまなアルゴリズムやデータ構造の中に潜んでいます。

","path":["第 12 章   分割統治","12.1   分割統治法"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   ハノイの塔の問題","text":"

マージソートや二分木の構築では、いずれも元の問題を元問題の半分の規模をもつ 2 つの部分問題に分解していました。しかし、ハノイの塔の問題では、異なる分解戦略を採用します。

Question

3 本の柱があり、それぞれを ABC とします。初期状態では、柱 A に \\(n\\) 枚の円盤が通されており、上から下へ小さい順に並んでいます。私たちの課題は、この \\(n\\) 枚の円盤を柱 C に移し、元の順序を保つことです(以下の図のとおり)。円盤を移動する際には、次のルールに従う必要があります。

  1. 円盤は 1 本の柱の頂上から取り出し、別の柱の頂上に置くことしかできません。
  2. 1 回に移動できる円盤は 1 枚だけです。
  3. 小さい円盤は常に大きい円盤の上になければなりません。

図 12-10   ハノイの塔の問題の例

規模が \\(i\\) のハノイの塔の問題を \\(f(i)\\) と表します 。たとえば \\(f(3)\\) は、\\(3\\) 枚の円盤を A から C へ移動するハノイの塔の問題を表します。

","path":["第 12 章   分割統治","12.4   ハノイの塔の問題"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#1","level":3,"title":"1.   基本ケースを考える","text":"

以下の図に示すように、問題 \\(f(1)\\) 、すなわち円盤が 1 枚だけの場合は、それを A から C へ直接移動すれば済みます。

<1><2>

図 12-11   規模 1 の問題の解

以下の図に示すように、問題 \\(f(2)\\) 、すなわち円盤が 2 枚ある場合は、小さい円盤が常に大きい円盤の上にある条件を満たすため、B を借りて移動を行う必要があります。

  1. まず上の小さい円盤を A から B へ移します。
  2. 次に大きい円盤を A から C へ移します。
  3. 最後に小さい円盤を B から C へ移します。
<1><2><3><4>

図 12-12   規模 2 の問題の解

問題 \\(f(2)\\) を解く過程は、**2 枚の円盤を B を介して A から C へ移す**と要約できます。このとき、C を目標の柱、B を補助の柱と呼びます。

","path":["第 12 章   分割統治","12.4   ハノイの塔の問題"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2","level":3,"title":"2.   部分問題への分解","text":"

問題 \\(f(3)\\) 、すなわち円盤が 3 枚ある場合になると、状況はやや複雑になります。

\\(f(1)\\) と \\(f(2)\\) の解が既知なので、分割統治の観点から、A の上部にある 2 枚の円盤をひとまとまりとみなして、次の図の手順を実行できます。こうして 3 枚の円盤を A から C へ順調に移動できます。

  1. B を目標の柱、C を補助の柱として、2 枚の円盤を A から B へ移します。
  2. A に残った 1 枚の円盤を A から C へ直接移動します。
  3. C を目標の柱、A を補助の柱として、2 枚の円盤を B から C へ移します。
<1><2><3><4>

図 12-13   規模 3 の問題の解

本質的には、問題 \\(f(3)\\) を 2 つの部分問題 \\(f(2)\\) と 1 つの部分問題 \\(f(1)\\) に分けています 。この 3 つの部分問題を順に解けば、元の問題も解決されます。これは、部分問題が独立しており、解を組み合わせられることを示しています。

ここまでで、次の図に示すハノイの塔の問題を解く分割統治戦略をまとめられます。元の問題 \\(f(n)\\) を 2 つの部分問題 \\(f(n-1)\\) と 1 つの部分問題 \\(f(1)\\) に分け、次の順序でこの 3 つの部分問題を解きます。

  1. \\(n-1\\) 枚の円盤を C を介して A から B へ移します。
  2. 残り \\(1\\) 枚の円盤を A から C へ直接移します。
  3. \\(n-1\\) 枚の円盤を A を介して B から C へ移します。

この 2 つの部分問題 \\(f(n-1)\\) は、同じ方法で再帰的に分割できます。最小の部分問題 \\(f(1)\\) に到達するまでこれを続けます。一方、\\(f(1)\\) の解は既知であり、1 回の移動操作だけで済みます。

図 12-14   ハノイの塔の問題を解く分割統治戦略

","path":["第 12 章   分割統治","12.4   ハノイの塔の問題"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#3","level":3,"title":"3.   コードの実装","text":"

コードでは、再帰関数 dfs(i, src, buf, tar) を定義します。その役割は、柱 src の上部にある \\(i\\) 枚の円盤を、補助の柱 buf を使って目標の柱 tar へ移動することです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py
def move(src: list[int], tar: list[int]):\n    \"\"\"円盤を 1 枚移動\"\"\"\n    # src の上から円盤を1枚取り出す\n    pan = src.pop()\n    # 円盤を tar の上に置く\n    tar.append(pan)\n\ndef dfs(i: int, src: list[int], buf: list[int], tar: list[int]):\n    \"\"\"ハノイの塔の問題 f(i) を解く\"\"\"\n    # src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1:\n        move(src, tar)\n        return\n    # 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf)\n    # 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    # 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar)\n\ndef solve_hanota(A: list[int], B: list[int], C: list[int]):\n    \"\"\"ハノイの塔を解く\"\"\"\n    n = len(A)\n    # A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C)\n
hanota.cpp
/* 円盤を 1 枚移動 */\nvoid move(vector<int> &src, vector<int> &tar) {\n    // src の上から円盤を1枚取り出す\n    int pan = src.back();\n    src.pop_back();\n    // 円盤を tar の上に置く\n    tar.push_back(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nvoid dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nvoid solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {\n    int n = A.size();\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
hanota.java
/* 円盤を 1 枚移動 */\nvoid move(List<Integer> src, List<Integer> tar) {\n    // src の上から円盤を1枚取り出す\n    Integer pan = src.remove(src.size() - 1);\n    // 円盤を tar の上に置く\n    tar.add(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nvoid dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nvoid solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {\n    int n = A.size();\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
hanota.cs
/* 円盤を 1 枚移動 */\nvoid Move(List<int> src, List<int> tar) {\n    // src の上から円盤を1枚取り出す\n    int pan = src[^1];\n    src.RemoveAt(src.Count - 1);\n    // 円盤を tar の上に置く\n    tar.Add(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nvoid DFS(int i, List<int> src, List<int> buf, List<int> tar) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        Move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    DFS(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    Move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    DFS(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nvoid SolveHanota(List<int> A, List<int> B, List<int> C) {\n    int n = A.Count;\n    // A の上から n 枚の円盤を B を介して C へ移す\n    DFS(n, A, B, C);\n}\n
hanota.go
/* 円盤を 1 枚移動 */\nfunc move(src, tar *list.List) {\n    // src の上から円盤を1枚取り出す\n    pan := src.Back()\n    // 円盤を tar の上に置く\n    tar.PushBack(pan.Value)\n    // `src` の最上部の円盤を取り外す\n    src.Remove(pan)\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfunc dfsHanota(i int, src, buf, tar *list.List) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move(src, tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfsHanota(i-1, src, tar, buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* ハノイの塔を解く */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfsHanota(n, A, B, C)\n}\n
hanota.swift
/* 円盤を 1 枚移動 */\nfunc move(src: inout [Int], tar: inout [Int]) {\n    // src の上から円盤を1枚取り出す\n    let pan = src.popLast()!\n    // 円盤を tar の上に置く\n    tar.append(pan)\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfunc dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move(src: &src, tar: &tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src: &src, tar: &tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)\n}\n\n/* ハノイの塔を解く */\nfunc solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {\n    let n = A.count\n    // リストの末尾が柱の上端に対応する\n    // src の上から n 個の円盤を、B を介して C に移動する\n    dfs(i: n, src: &A, buf: &B, tar: &C)\n}\n
hanota.js
/* 円盤を 1 枚移動 */\nfunction move(src, tar) {\n    // src の上から円盤を1枚取り出す\n    const pan = src.pop();\n    // 円盤を tar の上に置く\n    tar.push(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfunction dfs(i, src, buf, tar) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
hanota.ts
/* 円盤を 1 枚移動 */\nfunction move(src: number[], tar: number[]): void {\n    // src の上から円盤を1枚取り出す\n    const pan = src.pop();\n    // 円盤を tar の上に置く\n    tar.push(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfunction dfs(i: number, src: number[], buf: number[], tar: number[]): void {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nfunction solveHanota(A: number[], B: number[], C: number[]): void {\n    const n = A.length;\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
hanota.dart
/* 円盤を 1 枚移動 */\nvoid move(List<int> src, List<int> tar) {\n  // src の上から円盤を1枚取り出す\n  int pan = src.removeLast();\n  // 円盤を tar の上に置く\n  tar.add(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nvoid dfs(int i, List<int> src, List<int> buf, List<int> tar) {\n  // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n  if (i == 1) {\n    move(src, tar);\n    return;\n  }\n  // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n  dfs(i - 1, src, tar, buf);\n  // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n  move(src, tar);\n  // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n  dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nvoid solveHanota(List<int> A, List<int> B, List<int> C) {\n  int n = A.length;\n  // A の上から n 枚の円盤を B を介して C へ移す\n  dfs(n, A, B, C);\n}\n
hanota.rs
/* 円盤を 1 枚移動 */\nfn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // src の上から円盤を1枚取り出す\n    let pan = src.pop().unwrap();\n    // 円盤を tar の上に置く\n    tar.push(pan);\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if i == 1 {\n        move_pan(src, tar);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move_pan(src, tar);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar);\n}\n\n/* ハノイの塔を解く */\nfn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {\n    let n = A.len() as i32;\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C);\n}\n
hanota.c
/* 円盤を 1 枚移動 */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n    // src の上から円盤を1枚取り出す\n    int pan = src[*srcSize - 1];\n    src[*srcSize - 1] = 0;\n    (*srcSize)--;\n    // 円盤を tar の上に置く\n    tar[*tarSize] = pan;\n    (*tarSize)++;\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nvoid dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, srcSize, tar, tarSize);\n        return;\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, srcSize, tar, tarSize);\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);\n}\n\n/* ハノイの塔を解く */\nvoid solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n
hanota.kt
/* 円盤を 1 枚移動 */\nfun move(src: MutableList<Int>, tar: MutableList<Int>) {\n    // src の上から円盤を1枚取り出す\n    val pan = src.removeAt(src.size - 1)\n    // 円盤を tar の上に置く\n    tar.add(pan)\n}\n\n/* ハノイの塔の問題 f(i) を解く */\nfun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {\n    // src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n    if (i == 1) {\n        move(src, tar)\n        return\n    }\n    // 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n    dfs(i - 1, src, tar, buf)\n    // 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n    move(src, tar)\n    // 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n    dfs(i - 1, buf, src, tar)\n}\n\n/* ハノイの塔を解く */\nfun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {\n    val n = A.size\n    // A の上から n 枚の円盤を B を介して C へ移す\n    dfs(n, A, B, C)\n}\n
hanota.rb
### 円盤を1枚移動 ###\ndef move(src, tar)\n  # src の上から円盤を1枚取り出す\n  pan = src.pop\n  # 円盤を tar の上に置く\n  tar << pan\nend\n\n### ハノイの塔 f(i) を解く ###\ndef dfs(i, src, buf, tar)\n  # src に円盤が 1 枚だけ残っている場合は、そのまま tar へ移す\n  if i == 1\n    move(src, tar)\n    return\n  end\n\n  # 部分問題 f(i-1):src の上部 i-1 枚の円盤を tar を補助にして buf へ移す\n  dfs(i - 1, src, tar, buf)\n  # 部分問題 f(1):src に残る 1 枚の円盤を tar に移す\n  move(src, tar)\n  # 部分問題 f(i-1):buf の上部 i-1 枚の円盤を src を補助にして tar へ移す\n  dfs(i - 1, buf, src, tar)\nend\n\n### ハノイの塔を解く ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # A の上から n 枚の円盤を B を介して C へ移す\n  dfs(n, _A, _B, _C)\nend\n
コードの可視化

全画面で見る >

以下の図に示すように、ハノイの塔の問題は高さ \\(n\\) の再帰木を形成し、各ノードは 1 つの部分問題、すなわち 1 つ起動された dfs() 関数に対応します。したがって時間計算量は \\(O(2^n)\\) 、空間計算量は \\(O(n)\\) です。

図 12-15   ハノイの塔の問題の再帰木

Quote

ハノイの塔の問題は古い伝説に由来します。古代インドのある寺院で、僧侶たちは 3 本の高いダイヤモンドの柱と、\\(64\\) 枚の大きさの異なる金の円盤を持っていました。僧侶たちは絶えず円盤を動かし、最後の 1 枚が正しく置かれた瞬間に世界が終わると信じていました。

しかし、たとえ僧侶たちが 1 秒に 1 回移動するとしても、合計でおよそ \\(2^{64} \\approx 1.84×10^{19}\\) 秒、約 \\(5850\\) 億年が必要で、現在推定されている宇宙の年齢をはるかに上回ります。したがって、この伝説が本当だったとしても、世界の終わりを心配する必要はなさそうです。

","path":["第 12 章   分割統治","12.4   ハノイの塔の問題"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/","level":1,"title":"12.5   まとめ","text":"","path":["第 12 章   分割統治","12.5   まとめ"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • 分割統治法は一般的なアルゴリズム設計戦略であり、分(分割)と治(統合)の 2 つの段階からなり、通常は再帰に基づいて実装されます。
  • それが分割統治法の問題かどうかを判断する基準には、問題を分解できるか、部分問題が独立しているか、部分問題を統合できるかが含まれます。
  • マージソートは分割統治法の典型的な応用であり、配列を再帰的に同じ長さの 2 つの部分配列に分割し、要素が 1 つだけになるまで続け、その後で各層を順に統合してソートを完了します。
  • 分割統治法を導入すると、多くの場合アルゴリズムの効率を高められます。一方では操作回数が減り、他方では分割後にシステムの並列最適化を行いやすくなります。
  • 分割統治法は多くのアルゴリズム問題を解決できるだけでなく、データ構造やアルゴリズム設計にも広く応用されており、至る所でその姿を見ることができます。
  • 総当たり探索と比べて、適応的な探索のほうが効率的です。時間計算量が \\(O(\\log n)\\) の探索アルゴリズムは、通常は分割統治法に基づいて実装されます。
  • 二分探索は分割統治法のもう 1 つの典型的な応用であり、部分問題の解を統合する手順を含みません。再帰的な分割統治によって二分探索を実現できます。
  • 二分木を構築する問題では、木の構築(元の問題)を左部分木と右部分木の構築(部分問題)に分けられます。これは、先行順序走査と中間順序走査のインデックス区間を分割することで実現できます。
  • ハノイの塔の問題では、規模 \\(n\\) の問題を、規模 \\(n-1\\) の 2 つの部分問題と規模 \\(1\\) の 1 つの部分問題に分けられます。これら 3 つの部分問題を順に解くと、元の問題も解決されます。
","path":["第 12 章   分割統治","12.5   まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"第 14 章   動的計画法","text":"

Abstract

小川は川へと注ぎ、河川は大海へと注ぐ。

動的計画法は小さな問題の解を集めて大きな問題の答えとし、一歩ずつ私たちを問題解決の彼岸へと導く。

","path":["第 14 章   動的計画法"],"tags":[]},{"location":"chapter_dynamic_programming/#_1","level":2,"title":"章の内容","text":"
  • 14.1   動的計画法入門
  • 14.2   動的計画法の問題特性
  • 14.3   動的計画法の問題解決の考え方
  • 14.4   0-1 ナップサック問題
  • 14.5   完全ナップサック問題
  • 14.6   編集距離問題
  • 14.7   まとめ
","path":["第 14 章   動的計画法"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/","level":1,"title":"14.2   動的計画法の問題特性","text":"

前節では、動的計画法が部分問題への分解によってどのように元の問題を解くのかを学びました。実際、部分問題への分解は汎用的なアルゴリズムの考え方であり、分割統治法、動的計画法、バックトラッキングでは重視点が異なります。

  • 分割統治法は、元の問題を再帰的に複数の互いに独立した部分問題へ分割し、最小の部分問題に至るまで分解したうえで、バックトラック時に部分問題の解を統合し、最終的に元の問題の解を得ます。
  • 動的計画法も問題を再帰的に分解しますが、分割統治法との主な違いは、動的計画法における部分問題が相互依存しており、分解の過程で多数の重複部分問題が現れることです。
  • バックトラッキング法は、試行と巻き戻しの中ですべての可能な解を列挙し、枝刈りによって不要な探索分岐を避けます。元の問題の解は一連の意思決定ステップから構成されるため、各決定ステップ以前の部分系列を一つの部分問題と見なせます。

実際、動的計画法は最適化問題を解くためによく用いられます。これらは重複部分問題を含むだけでなく、さらに二つの大きな特性、すなわち最適部分構造と無後効性を備えています。

","path":["第 14 章   動的計画法","14.2   動的計画法の問題特性"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1421","level":2,"title":"14.2.1   最適部分構造","text":"

階段昇り問題を少し変更し、最適部分構造の概念をより示しやすくします。

階段昇りの最小コスト

階段が与えられ、各ステップで \\(1\\) 段または \\(2\\) 段上ることができます。各段には非負整数が貼られており、その段に到達するために支払う必要があるコストを表します。非負整数配列 \\(cost\\) が与えられ、\\(cost[i]\\) は第 \\(i\\) 段で支払うコストを表し、\\(cost[0]\\) は地面(開始地点)です。頂上に到達するために必要な最小コストを求めてください。

下図に示すように、第 \\(1\\)、\\(2\\)、\\(3\\) 段のコストがそれぞれ \\(1\\)、\\(10\\)、\\(1\\) である場合、地面から第 \\(3\\) 段まで上る最小コストは \\(2\\) です。

図 14-6   第 3 段まで上る最小コスト

\\(dp[i]\\) を第 \\(i\\) 段まで上るのに累積して支払ったコストとします。第 \\(i\\) 段には \\(i - 1\\) 段または \\(i - 2\\) 段からしか到達できないため、\\(dp[i]\\) は \\(dp[i - 1] + cost[i]\\) または \\(dp[i - 2] + cost[i]\\) のいずれかになります。コストをできるだけ小さくするには、この二つのうち小さいほうを選べばよいです。

\\[ dp[i] = \\min(dp[i-1], dp[i-2]) + cost[i] \\]

ここから最適部分構造の意味を導けます。**元の問題の最適解は、部分問題の最適解から構築される**ということです。

この問題が最適部分構造を持つことは明らかです。二つの部分問題の最適解 \\(dp[i-1]\\) と \\(dp[i-2]\\) からより良いほうを選び、それを用いて元の問題 \\(dp[i]\\) の最適解を構築しています。

では、前節の階段昇り問題には最適部分構造があるのでしょうか。その目的は方法数を求めることで、一見すると計数問題です。しかし問い方を変えて「最大の方法数を求める」とすると、意外にも、問題の変更前後は等価であるにもかかわらず、最適部分構造が現れます。すなわち、第 \\(n\\) 段の最大方法数は第 \\(n-1\\) 段と第 \\(n-2\\) 段の最大方法数の和に等しいのです。このように、最適部分構造の解釈は比較的柔軟であり、問題によって意味合いが異なります。

状態遷移方程式と初期状態 \\(dp[1] = cost[1]\\) および \\(dp[2] = cost[2]\\) に基づいて、次の動的計画法コードが得られます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
def min_cost_climbing_stairs_dp(cost: list[int]) -> int:\n    \"\"\"階段登りの最小コスト:動的計画法\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    # 部分問題の解を保存するために dp テーブルを初期化\n    dp = [0] * (n + 1)\n    # 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1], dp[2] = cost[1], cost[2]\n    # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in range(3, n + 1):\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    return dp[n]\n
min_cost_climbing_stairs_dp.cpp
/* 階段登りの最小コスト:動的計画法 */\nint minCostClimbingStairsDP(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 部分問題の解を保存するために dp テーブルを初期化\n    vector<int> dp(n + 1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.java
/* 階段登りの最小コスト:動的計画法 */\nint minCostClimbingStairsDP(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[] dp = new int[n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.cs
/* 階段登りの最小コスト:動的計画法 */\nint MinCostClimbingStairsDP(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[] dp = new int[n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.go
/* 階段登りの最小コスト:動的計画法 */\nfunc minCostClimbingStairsDP(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    dp := make([]int, n+1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i := 3; i <= n; i++ {\n        dp[i] = min(dp[i-1], dp[i-2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.swift
/* 階段登りの最小コスト:動的計画法 */\nfunc minCostClimbingStairsDP(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    var dp = Array(repeating: 0, count: n + 1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3 ... n {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.js
/* 階段登りの最小コスト:動的計画法 */\nfunction minCostClimbingStairsDP(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = new Array(n + 1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.ts
/* 階段登りの最小コスト:動的計画法 */\nfunction minCostClimbingStairsDP(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = new Array(n + 1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.dart
/* 階段登りの最小コスト:動的計画法 */\nint minCostClimbingStairsDP(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  // 部分問題の解を保存するために dp テーブルを初期化\n  List<int> dp = List.filled(n + 1, 0);\n  // 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1] = cost[1];\n  dp[2] = cost[2];\n  // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  for (int i = 3; i <= n; i++) {\n    dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n  }\n  return dp[n];\n}\n
min_cost_climbing_stairs_dp.rs
/* 階段登りの最小コスト:動的計画法 */\nfn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    let mut dp = vec![-1; n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3..=n {\n        dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    dp[n]\n}\n
min_cost_climbing_stairs_dp.c
/* 階段登りの最小コスト:動的計画法 */\nint minCostClimbingStairsDP(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int *dp = calloc(n + 1, sizeof(int));\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    int res = dp[n];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
min_cost_climbing_stairs_dp.kt
/* 階段登りの最小コスト:動的計画法 */\nfun minCostClimbingStairsDP(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    // 部分問題の解を保存するために dp テーブルを初期化\n    val dp = IntArray(n + 1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (i in 3..n) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.rb
### 階段登りの最小コスト:動的計画法 ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # 部分問題の解を保存するために dp テーブルを初期化\n  dp = Array.new(n + 1, 0)\n  # 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1], dp[2] = cost[1], cost[2]\n  # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n
コードの可視化

全画面で見る >

下図は上記コードの動的計画法の過程を示しています。

図 14-7   階段昇り最小コストの動的計画法の過程

この問題では空間最適化も可能であり、一次元をゼロ次元まで圧縮することで、空間計算量を \\(O(n)\\) から \\(O(1)\\) に削減できます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:\n    \"\"\"階段昇りの最小コスト:空間最適化後の動的計画法\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    a, b = cost[1], cost[2]\n    for i in range(3, n + 1):\n        a, b = b, min(a, b) + cost[i]\n    return b\n
min_cost_climbing_stairs_dp.cpp
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nint minCostClimbingStairsDPComp(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.java
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nint minCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.cs
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nint MinCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.Min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.go
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfunc minCostClimbingStairsDPComp(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    a, b := cost[1], cost[2]\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i := 3; i <= n; i++ {\n        tmp := b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.swift
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfunc minCostClimbingStairsDPComp(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    var (a, b) = (cost[1], cost[2])\n    for i in 3 ... n {\n        (a, b) = (b, min(a, b) + cost[i])\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.js
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfunction minCostClimbingStairsDPComp(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.ts
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfunction minCostClimbingStairsDPComp(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.dart
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nint minCostClimbingStairsDPComp(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  int a = cost[1], b = cost[2];\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = min(a, tmp) + cost[i];\n    a = tmp;\n  }\n  return b;\n}\n
min_cost_climbing_stairs_dp.rs
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    };\n    let (mut a, mut b) = (cost[1], cost[2]);\n    for i in 3..=n {\n        let tmp = b;\n        b = cmp::min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    b\n}\n
min_cost_climbing_stairs_dp.c
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nint minCostClimbingStairsDPComp(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = myMin(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.kt
/* 階段昇りの最小コスト:空間最適化後の動的計画法 */\nfun minCostClimbingStairsDPComp(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    var a = cost[1]\n    var b = cost[2]\n    for (i in 3..n) {\n        val tmp = b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.rb
### 階段登りの最小コスト:動的計画法 ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # 部分問題の解を保存するために dp テーブルを初期化\n  dp = Array.new(n + 1, 0)\n  # 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1], dp[2] = cost[1], cost[2]\n  # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n\n# 階段昇りの最小コスト:空間最適化後の動的計画法\ndef min_cost_climbing_stairs_dp_comp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  a, b = cost[1], cost[2]\n  (3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] }\n  b\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.2   動的計画法の問題特性"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1422","level":2,"title":"14.2.2   無後効性","text":"

無後効性は、動的計画法が問題を効率よく解ける重要な特性の一つであり、その定義は次のとおりです。ある確定した状態が与えられたとき、その後の発展は現在の状態のみに依存し、過去に経たすべての状態には依存しない。

階段昇り問題を例にすると、状態 \\(i\\) が与えられたとき、そこから状態 \\(i+1\\) と状態 \\(i+2\\) へ発展し、それぞれ \\(1\\) 段進む場合と \\(2\\) 段進む場合に対応します。この二つの選択を行う際、状態 \\(i\\) より前の状態を考慮する必要はなく、それらは状態 \\(i\\) の将来に影響を与えません。

しかし、階段昇り問題に制約を一つ追加すると、状況は変わります。

制約付き階段昇り

全部で \\(n\\) 段ある階段が与えられ、各ステップで \\(1\\) 段または \\(2\\) 段上ることができます。ただし、連続する 2 回で \\(1\\) 段ずつ上ることはできません。頂上まで上る方法は何通りあるでしょうか。

下図に示すように、第 \\(3\\) 段まで上る実行可能な方法は \\(2\\) 通りしか残りません。そのうち、\\(1\\) 段ずつ 3 回連続で上る方法は制約を満たさないため除外されます。

図 14-8   制約付きで第 3 段まで上る方法数

この問題では、前回が \\(1\\) 段上りだった場合、次回は必ず \\(2\\) 段上らなければなりません。これは、**次の一手が現在の状態(現在いる階段の段数)だけでは独立に決まらず、一つ前の状態(前回いた段数)にも関係する**ことを意味します。

容易に分かるように、この問題はもはや無後効性を満たしておらず、状態遷移方程式 \\(dp[i] = dp[i-1] + dp[i-2]\\) も成立しません。というのも、\\(dp[i-1]\\) は今回 \\(1\\) 段上る場合を表しますが、その中には「前回も \\(1\\) 段上ってきた」方法が多数含まれており、制約を満たすためには \\(dp[i-1]\\) をそのまま \\(dp[i]\\) に加えることができないからです。

このため、状態定義を拡張する必要があります。**状態 \\([i, j]\\) は、第 \\(i\\) 段にいて前回に \\(j\\) 段上ったことを表す**とし、ここで \\(j \\in \\{1, 2\\}\\) です。この状態定義により、前回が \\(1\\) 段上りか \\(2\\) 段上りかを有効に区別でき、現在の状態がどこから来たのかを判断できます。

  • 前回に \\(1\\) 段上った場合、その前の回は \\(2\\) 段上りしか選べないため、\\(dp[i, 1]\\) は \\(dp[i-1, 2]\\) からのみ遷移できます。
  • 前回に \\(2\\) 段上った場合、その前の回は \\(1\\) 段上りまたは \\(2\\) 段上りを選べるため、\\(dp[i, 2]\\) は \\(dp[i-2, 1]\\) または \\(dp[i-2, 2]\\) から遷移できます。

下図に示すように、この定義のもとでは \\(dp[i, j]\\) は状態 \\([i, j]\\) に対応する方法数を表します。このとき状態遷移方程式は次のようになります。

\\[ \\begin{cases} dp[i, 1] = dp[i-1, 2] \\\\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \\end{cases} \\]

図 14-9   制約を考慮した漸化関係

最終的に、\\(dp[n, 1] + dp[n, 2]\\) を返せば十分であり、その和が第 \\(n\\) 段まで上る方法の総数を表します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_constraint_dp.py
def climbing_stairs_constraint_dp(n: int) -> int:\n    \"\"\"制約付き階段登り:動的計画法\"\"\"\n    if n == 1 or n == 2:\n        return 1\n    # 部分問題の解を保存するために dp テーブルを初期化\n    dp = [[0] * 3 for _ in range(n + 1)]\n    # 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1], dp[1][2] = 1, 0\n    dp[2][1], dp[2][2] = 0, 1\n    # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in range(3, n + 1):\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    return dp[n][1] + dp[n][2]\n
climbing_stairs_constraint_dp.cpp
/* 制約付き階段登り:動的計画法 */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    vector<vector<int>> dp(n + 1, vector<int>(3, 0));\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.java
/* 制約付き階段登り:動的計画法 */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[][] dp = new int[n + 1][3];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.cs
/* 制約付き階段登り:動的計画法 */\nint ClimbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[,] dp = new int[n + 1, 3];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1, 1] = 1;\n    dp[1, 2] = 0;\n    dp[2, 1] = 0;\n    dp[2, 2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i, 1] = dp[i - 1, 2];\n        dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];\n    }\n    return dp[n, 1] + dp[n, 2];\n}\n
climbing_stairs_constraint_dp.go
/* 制約付き階段登り:動的計画法 */\nfunc climbingStairsConstraintDP(n int) int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    dp := make([][3]int, n+1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i := 3; i <= n; i++ {\n        dp[i][1] = dp[i-1][2]\n        dp[i][2] = dp[i-2][1] + dp[i-2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.swift
/* 制約付き階段登り:動的計画法 */\nfunc climbingStairsConstraintDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3 ... n {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.js
/* 制約付き階段登り:動的計画法 */\nfunction climbingStairsConstraintDP(n) {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = Array.from(new Array(n + 1), () => new Array(3));\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.ts
/* 制約付き階段登り:動的計画法 */\nfunction climbingStairsConstraintDP(n: number): number {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () => new Array(3));\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.dart
/* 制約付き階段登り:動的計画法 */\nint climbingStairsConstraintDP(int n) {\n  if (n == 1 || n == 2) {\n    return 1;\n  }\n  // 部分問題の解を保存するために dp テーブルを初期化\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));\n  // 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1][1] = 1;\n  dp[1][2] = 0;\n  dp[2][1] = 0;\n  dp[2][2] = 1;\n  // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  for (int i = 3; i <= n; i++) {\n    dp[i][1] = dp[i - 1][2];\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n  }\n  return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.rs
/* 制約付き階段登り:動的計画法 */\nfn climbing_stairs_constraint_dp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return 1;\n    };\n    // 部分問題の解を保存するために dp テーブルを初期化\n    let mut dp = vec![vec![-1; 3]; n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3..=n {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.c
/* 制約付き階段登り:動的計画法 */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(3, sizeof(int));\n    }\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    int res = dp[n][1] + dp[n][2];\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
climbing_stairs_constraint_dp.kt
/* 制約付き階段登り:動的計画法 */\nfun climbingStairsConstraintDP(n: Int): Int {\n    if (n == 1 || n == 2) {\n        return 1\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    val dp = Array(n + 1) { IntArray(3) }\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (i in 3..n) {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.rb
### 制約付き階段登り:動的計画法 ###\ndef climbing_stairs_constraint_dp(n)\n  return 1 if n == 1 || n == 2\n\n  # 部分問題の解を保存するために dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(3, 0) }\n  # 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1][1], dp[1][2] = 1, 0\n  dp[2][1], dp[2][2] = 0, 1\n  # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  for i in 3...(n + 1)\n    dp[i][1] = dp[i - 1][2]\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n  end\n\n  dp[n][1] + dp[n][2]\nend\n
コードの可視化

全画面で見る >

上の例では、追加で考慮すべきなのは一つ前の状態だけであるため、状態定義を拡張することで問題を再び無後効性に適合させることができます。しかし、問題によっては非常に強い「後効性」があります。

階段昇りと障害物生成

全部で \\(n\\) 段ある階段が与えられ、各ステップで \\(1\\) 段または \\(2\\) 段上ることができます。**第 \\(i\\) 段に到達すると、システムは自動的に第 \\(2i\\) 段に障害物を置き、それ以降はどの回でも第 \\(2i\\) 段へ跳ぶことができない**とします。例えば、最初の 2 回でそれぞれ第 \\(2\\) 段、第 \\(3\\) 段に到達した場合、その後は第 \\(4\\) 段と第 \\(6\\) 段に跳ぶことはできません。頂上まで上る方法は何通りあるでしょうか。

この問題では、次の跳躍が過去のすべての状態に依存します。なぜなら、各跳躍がより高い段に障害物を設置し、将来の跳躍に影響するからです。この種の問題は、動的計画法では解きにくいことが多いです。

実際、多くの複雑な組合せ最適化問題(例えば巡回セールスマン問題)は無後効性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を用いて、限られた時間内に実用的な局所最適解を得ます。

","path":["第 14 章   動的計画法","14.2   動的計画法の問題特性"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   動的計画法の問題解決の考え方","text":"

前の 2 節では動的計画法の問題の主要な特徴を紹介しました。ここからは、さらに実用的な 2 つの問題を一緒に考えていきます。

  1. ある問題が動的計画法の問題かどうかを、どのように判断すればよいでしょうか?
  2. 動的計画法の問題を解くには、どこから着手し、完全な手順はどのようなものでしょうか?
","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431","level":2,"title":"14.3.1   問題の判定","text":"

一般に、ある問題が重複部分問題と最適部分構造を含み、さらに無後效性を満たしているなら、通常は動的計画法で解くのに適しています。しかし、問題文からこれらの性質を直接読み取るのは簡単ではありません。そのため通常は条件を少し緩めて、**まずその問題がバックトラッキング(全探索)で解くのに適しているか**を観察します。

バックトラッキングで解くのに適した問題は、通常「決定木モデル」を満たします。この種の問題は木構造で表現でき、各ノードは 1 つの決定を表し、各経路は 1 つの決定列を表します。

言い換えると、問題に明確な決定の概念が含まれており、解が一連の決定によって生成されるなら、その問題は決定木モデルを満たし、通常はバックトラッキングで解くことができます。

これに加えて、動的計画法の問題には判定のための「加点要素」もあります。

  • 問題文に最大(最小)や最多(最少)などの最適化に関する記述がある。
  • 問題の状態が配列、多次元行列、または木で表現でき、ある状態とその周辺の状態の間に漸化的な関係がある。

反対に、「減点要素」もあります。

  • 問題の目的が最適解を求めることではなく、あり得るすべての解を列挙することである。
  • 問題文に明確な順列・組合せの特徴があり、具体的な複数の解を返す必要がある。

ある問題が決定木モデルを満たし、さらに比較的明確な「加点要素」を備えているなら、その問題は動的計画法の問題であると仮定し、解く過程でそれを検証できます。

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432","level":2,"title":"14.3.2   問題を解く手順","text":"

動的計画法の解法の流れは問題の性質や難易度によって異なりますが、通常は次の手順に従います。すなわち、決定を記述し、状態を定義し、\\(dp\\) テーブルを構築し、状態遷移方程式を導出し、境界条件を定めます。

解法の手順をより具体的に示すために、ここでは古典的な問題である「最小経路和」を例にします。

Question

\\(n \\times m\\) の 2 次元グリッド grid が与えられます。グリッドの各セルには非負整数が格納されており、そのセルのコストを表します。ロボットは左上のセルを始点とし、毎回下または右に 1 マスだけ移動して、右下のセルまで進みます。左上から右下までの最小経路和を返してください。

次の図は 1 つの例を示しており、このグリッドの最小経路和は \\(13\\) です。

図 14-10   最小経路和のサンプルデータ

ステップ 1:各ラウンドの決定を考え、状態を定義して、\\(dp\\) テーブルを得る

この問題における各ラウンドの決定は、現在のマスから下または右へ 1 マス進むことです。現在のマスの行・列インデックスを \\([i, j]\\) とすると、下または右へ 1 マス進んだ後のインデックスは \\([i+1, j]\\) または \\([i, j+1]\\) になります。したがって、状態には行インデックスと列インデックスの 2 つの変数を含め、\\([i, j]\\) と表します。

状態 \\([i, j]\\) に対応する部分問題は、始点 \\([0, 0]\\) から \\([i, j]\\) まで進む最小経路和であり、その解を \\(dp[i, j]\\) と記します。

これで、次の図に示す 2 次元の \\(dp\\) 行列が得られます。そのサイズは入力グリッド \\(grid\\) と同じです。

図 14-11   状態の定義と dp テーブル

Note

動的計画法とバックトラッキングの過程は、いずれも 1 つの決定列として記述できます。そして状態は、すべての決定変数から構成されます。状態には解法の進行状況を表すすべての変数が含まれているべきであり、次の状態を導くのに十分な情報を持っている必要があります。

各状態は 1 つの部分問題に対応しており、すべての部分問題の解を保存するために \\(dp\\) テーブルを定義します。状態の各独立変数は、\\(dp\\) テーブルの 1 つの次元に対応します。本質的に、\\(dp\\) テーブルは状態と部分問題の解との対応関係です。

ステップ 2:最適部分構造を見つけ、状態遷移方程式を導出する

状態 \\([i, j]\\) は、上のマス \\([i-1, j]\\) または左のマス \\([i, j-1]\\) からしか遷移してきません。したがって最適部分構造は、\\([i, j]\\) に到達する最小経路和が、\\([i, j-1]\\) の最小経路和と \\([i-1, j]\\) の最小経路和のうち小さい方によって決まる、ということです。

以上の分析から、次の図に示す状態遷移方程式を導くことができます。

\\[ dp[i, j] = \\min(dp[i-1, j], dp[i, j-1]) + grid[i, j] \\]

図 14-12   最適部分構造と状態遷移方程式

Note

定義済みの \\(dp\\) テーブルに基づいて、元の問題と部分問題の関係を考え、部分問題の最適解から元の問題の最適解を構成する方法、すなわち最適部分構造を見つけます。

ひとたび最適部分構造が見つかれば、それを使って状態遷移方程式を構築できます。

ステップ 3:境界条件と状態遷移の順序を決める

この問題では、先頭行にある状態は左の状態からしか得られず、先頭列にある状態は上の状態からしか得られません。したがって、先頭行 \\(i = 0\\) と先頭列 \\(j = 0\\) が境界条件になります。

次の図に示すように、各マスは左のマスと上のマスから遷移してくるため、ループを用いて行列を走査します。外側のループで各行を、内側のループで各列を走査します。

図 14-13   境界条件と状態遷移の順序

Note

境界条件は、動的計画法では \\(dp\\) テーブルの初期化に使われ、探索では枝刈りに使われます。

状態遷移の順序で重要なのは、現在の問題の解を計算するときに、それが依存するより小さな部分問題の解がすべてすでに正しく計算済みであることを保証する点です。

以上の分析により、すでに動的計画法のコードを直接書くことができます。しかし、部分問題への分解はトップダウンの考え方であるため、「力任せ探索 \\(\\rightarrow\\) メモ化探索 \\(\\rightarrow\\) 動的計画法」の順に実装するほうが、思考の流れにはより自然です。

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1-1","level":3,"title":"1.   方法 1:力任せ探索","text":"

状態 \\([i, j]\\) から探索を開始し、より小さな状態 \\([i-1, j]\\) と \\([i, j-1]\\) へと分解していきます。再帰関数には次の要素が含まれます。

  • 再帰引数:状態 \\([i, j]\\) 。
  • 戻り値:\\([0, 0]\\) から \\([i, j]\\) までの最小経路和 \\(dp[i, j]\\) 。
  • 終了条件:\\(i = 0\\) かつ \\(j = 0\\) のとき、コスト \\(grid[0, 0]\\) を返す。
  • 枝刈り:\\(i < 0\\) または \\(j < 0\\) でインデックスが範囲外になった場合、コスト \\(+\\infty\\) を返し、実行不可能であることを表す。

実装コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:\n    \"\"\"最小経路和:全探索\"\"\"\n    # 左上のセルなら探索を終了する\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 or j < 0:\n        return inf\n    # 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    up = min_path_sum_dfs(grid, i - 1, j)\n    left = min_path_sum_dfs(grid, i, j - 1)\n    # 左上隅から (i, j) までの最小経路コストを返す\n    return min(left, up) + grid[i][j]\n
min_path_sum.cpp
/* 最小経路和:全探索 */\nint minPathSumDFS(vector<vector<int>> &grid, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n}\n
min_path_sum.java
/* 最小経路和:全探索 */\nint minPathSumDFS(int[][] grid, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.cs
/* 最小経路和:全探索 */\nint MinPathSumDFS(int[][] grid, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    int up = MinPathSumDFS(grid, i - 1, j);\n    int left = MinPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return Math.Min(left, up) + grid[i][j];\n}\n
min_path_sum.go
/* 最小経路和:全探索 */\nfunc minPathSumDFS(grid [][]int, i, j int) int {\n    // 左上のセルなら探索を終了する\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    up := minPathSumDFS(grid, i-1, j)\n    left := minPathSumDFS(grid, i, j-1)\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return int(math.Min(float64(left), float64(up))) + grid[i][j]\n}\n
min_path_sum.swift
/* 最小経路和:全探索 */\nfunc minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int {\n    // 左上のセルなら探索を終了する\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    let up = minPathSumDFS(grid: grid, i: i - 1, j: j)\n    let left = minPathSumDFS(grid: grid, i: i, j: j - 1)\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return min(left, up) + grid[i][j]\n}\n
min_path_sum.js
/* 最小経路和:全探索 */\nfunction minPathSumDFS(grid, i, j) {\n    // 左上のセルなら探索を終了する\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.ts
/* 最小経路和:全探索 */\nfunction minPathSumDFS(\n    grid: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // 左上のセルなら探索を終了する\n    if (i === 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.dart
/* 最小経路和:全探索 */\nint minPathSumDFS(List<List<int>> grid, int i, int j) {\n  // 左上のセルなら探索を終了する\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n  if (i < 0 || j < 0) {\n    // Dart では、int 型は固定範囲の整数であり、「無限大」を表す値は存在しない\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n  int up = minPathSumDFS(grid, i - 1, j);\n  int left = minPathSumDFS(grid, i, j - 1);\n  // 左上隅から (i, j) までの最小経路コストを返す\n  return min(left, up) + grid[i][j];\n}\n
min_path_sum.rs
/* 最小経路和:全探索 */\nfn min_path_sum_dfs(grid: &Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // 左上のセルなら探索を終了する\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    let up = min_path_sum_dfs(grid, i - 1, j);\n    let left = min_path_sum_dfs(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    std::cmp::min(left, up) + grid[i as usize][j as usize]\n}\n
min_path_sum.c
/* 最小経路和:全探索 */\nint minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n}\n
min_path_sum.kt
/* 最小経路和:全探索 */\nfun minPathSumDFS(grid: Array<IntArray>, i: Int, j: Int): Int {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n    val up = minPathSumDFS(grid, i - 1, j)\n    val left = minPathSumDFS(grid, i, j - 1)\n    // 左上隅から (i, j) までの最小経路コストを返す\n    return min(left, up) + grid[i][j]\n}\n
min_path_sum.rb
### 最小経路和:全探索 ###\ndef min_path_sum_dfs(grid, i, j)\n  # 左上のセルなら探索を終了する\n  return grid[i][j] if i == 0 && j == 0\n  # 行または列のインデックスが範囲外なら、コスト +∞ を返す\n  return Float::INFINITY if i < 0 || j < 0\n  # 左上から (i-1, j) および (i, j-1) までの最小経路コストを計算する\n  up = min_path_sum_dfs(grid, i - 1, j)\n  left = min_path_sum_dfs(grid, i, j - 1)\n  # 左上隅から (i, j) までの最小経路コストを返す\n  [left, up].min + grid[i][j]\nend\n
コードの可視化

全画面で見る >

次の図は、\\(dp[2, 1]\\) を根ノードとする再帰木を示しています。この中にはいくつかの重複部分問題が含まれており、その数はグリッド grid のサイズが大きくなるにつれて急激に増加します。

本質的に、重複部分問題が生じる理由は、**左上からあるセルへ到達する経路が複数存在すること**にあります。

図 14-14   力任せ探索の再帰木

各状態には下と右の 2 通りの選択肢があり、左上から右下まで進むには合計で \\(m + n - 2\\) 歩必要です。したがって最悪時間計算量は \\(O(2^{m + n})\\) です。ここで、\\(n\\) と \\(m\\) はそれぞれグリッドの行数と列数を表します。なお、この見積もりではグリッド境界付近の状況を考慮していません。境界に達すると選択肢は 1 つだけになるため、実際の経路数はこれより少なくなります。

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-2","level":3,"title":"2.   方法 2:メモ化探索","text":"

グリッド grid と同じサイズのメモ配列 mem を導入し、各部分問題の解を記録して、重複部分問題を枝刈りします。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dfs_mem(\n    grid: list[list[int]], mem: list[list[int]], i: int, j: int\n) -> int:\n    \"\"\"最小経路和:メモ化探索\"\"\"\n    # 左上のセルなら探索を終了する\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 or j < 0:\n        return inf\n    # 既に記録があればそのまま返す\n    if mem[i][j] != -1:\n        return mem[i][j]\n    # 左と上のセルからの最小経路コスト\n    up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n    left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n    # 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n
min_path_sum.cpp
/* 最小経路和:メモ化探索 */\nint minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
min_path_sum.java
/* 最小経路和:メモ化探索 */\nint minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.cs
/* 最小経路和:メモ化探索 */\nint MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    int up = MinPathSumDFSMem(grid, mem, i - 1, j);\n    int left = MinPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = Math.Min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.go
/* 最小経路和:メモ化探索 */\nfunc minPathSumDFSMem(grid, mem [][]int, i, j int) int {\n    // 左上のセルなら探索を終了する\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // 既に記録があればそのまま返す\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // 左と上のセルからの最小経路コスト\n    up := minPathSumDFSMem(grid, mem, i-1, j)\n    left := minPathSumDFSMem(grid, mem, i, j-1)\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = int(math.Min(float64(left), float64(up))) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.swift
/* 最小経路和:メモ化探索 */\nfunc minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int {\n    // 左上のセルなら探索を終了する\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // 既に記録があればそのまま返す\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // 左と上のセルからの最小経路コスト\n    let up = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j)\n    let left = minPathSumDFSMem(grid: grid, mem: &mem, i: i, j: j - 1)\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.js
/* 最小経路和:メモ化探索 */\nfunction minPathSumDFSMem(grid, mem, i, j) {\n    // 左上のセルなら探索を終了する\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] !== -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.ts
/* 最小経路和:メモ化探索 */\nfunction minPathSumDFSMem(\n    grid: Array<Array<number>>,\n    mem: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // 左上のセルなら探索を終了する\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.dart
/* 最小経路和:メモ化探索 */\nint minPathSumDFSMem(List<List<int>> grid, List<List<int>> mem, int i, int j) {\n  // 左上のセルなら探索を終了する\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n  if (i < 0 || j < 0) {\n    // Dart では、int 型は固定範囲の整数であり、「無限大」を表す値は存在しない\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // 既に記録があればそのまま返す\n  if (mem[i][j] != -1) {\n    return mem[i][j];\n  }\n  // 左と上のセルからの最小経路コスト\n  int up = minPathSumDFSMem(grid, mem, i - 1, j);\n  int left = minPathSumDFSMem(grid, mem, i, j - 1);\n  // 左上から (i, j) までの最小経路コストを記録して返す\n  mem[i][j] = min(left, up) + grid[i][j];\n  return mem[i][j];\n}\n
min_path_sum.rs
/* 最小経路和:メモ化探索 */\nfn min_path_sum_dfs_mem(grid: &Vec<Vec<i32>>, mem: &mut Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // 左上のセルなら探索を終了する\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // 既に記録があればそのまま返す\n    if mem[i as usize][j as usize] != -1 {\n        return mem[i as usize][j as usize];\n    }\n    // 左と上のセルからの最小経路コスト\n    let up = min_path_sum_dfs_mem(grid, mem, i - 1, j);\n    let left = min_path_sum_dfs_mem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize];\n    mem[i as usize][j as usize]\n}\n
min_path_sum.c
/* 最小経路和:メモ化探索 */\nint minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左と上のセルからの最小経路コスト\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
min_path_sum.kt
/* 最小経路和:メモ化探索 */\nfun minPathSumDFSMem(\n    grid: Array<IntArray>,\n    mem: Array<IntArray>,\n    i: Int,\n    j: Int\n): Int {\n    // 左上のセルなら探索を終了する\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // 行または列のインデックスが範囲外なら、コスト +∞ を返す\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][j] != -1) {\n        return mem[i][j]\n    }\n    // 左と上のセルからの最小経路コスト\n    val up = minPathSumDFSMem(grid, mem, i - 1, j)\n    val left = minPathSumDFSMem(grid, mem, i, j - 1)\n    // 左上から (i, j) までの最小経路コストを記録して返す\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.rb
### 最小経路和:メモ化探索 ###\ndef min_path_sum_dfs_mem(grid, mem, i, j)\n  # 左上のセルなら探索を終了する\n  return grid[0][0] if i == 0 && j == 0\n  # 行または列のインデックスが範囲外なら、コスト +∞ を返す\n  return Float::INFINITY if i < 0 || j < 0\n  # 既に記録があればそのまま返す\n  return mem[i][j] if mem[i][j] != -1\n  # 左と上のセルからの最小経路コスト\n  up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n  left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n  # 左上から (i, j) までの最小経路コストを記録して返す\n  mem[i][j] = [left, up].min + grid[i][j]\nend\n
コードの可視化

全画面で見る >

次の図に示すように、メモ化を導入すると、すべての部分問題の解は 1 回だけ計算すればよくなります。したがって時間計算量は状態総数、すなわちグリッドサイズの \\(O(nm)\\) に依存します。

図 14-15   メモ化探索の再帰木

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#3-3","level":3,"title":"3.   方法 3:動的計画法","text":"

反復に基づいて動的計画法の解法を実装すると、コードは次のようになります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dp(grid: list[list[int]]) -> int:\n    \"\"\"最小経路和:動的計画法\"\"\"\n    n, m = len(grid), len(grid[0])\n    # dp テーブルを初期化\n    dp = [[0] * m for _ in range(n)]\n    dp[0][0] = grid[0][0]\n    # 状態遷移:先頭行\n    for j in range(1, m):\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    # 状態遷移:先頭列\n    for i in range(1, n):\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    # 状態遷移: 残りの行と列\n    for i in range(1, n):\n        for j in range(1, m):\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n    return dp[n - 1][m - 1]\n
min_path_sum.cpp
/* 最小経路和:動的計画法 */\nint minPathSumDP(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // dp テーブルを初期化\n    vector<vector<int>> dp(n, vector<int>(m));\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.java
/* 最小経路和:動的計画法 */\nint minPathSumDP(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // dp テーブルを初期化\n    int[][] dp = new int[n][m];\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.cs
/* 最小経路和:動的計画法 */\nint MinPathSumDP(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // dp テーブルを初期化\n    int[,] dp = new int[n, m];\n    dp[0, 0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (int j = 1; j < m; j++) {\n        dp[0, j] = dp[0, j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (int i = 1; i < n; i++) {\n        dp[i, 0] = dp[i - 1, 0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i, j] = Math.Min(dp[i, j - 1], dp[i - 1, j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1, m - 1];\n}\n
min_path_sum.go
/* 最小経路和:動的計画法 */\nfunc minPathSumDP(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // dp テーブルを初期化\n    dp := make([][]int, n)\n    for i := 0; i < n; i++ {\n        dp[i] = make([]int, m)\n    }\n    dp[0][0] = grid[0][0]\n    // 状態遷移:先頭行\n    for j := 1; j < m; j++ {\n        dp[0][j] = dp[0][j-1] + grid[0][j]\n    }\n    // 状態遷移:先頭列\n    for i := 1; i < n; i++ {\n        dp[i][0] = dp[i-1][0] + grid[i][0]\n    }\n    // 状態遷移: 残りの行と列\n    for i := 1; i < n; i++ {\n        for j := 1; j < m; j++ {\n            dp[i][j] = int(math.Min(float64(dp[i][j-1]), float64(dp[i-1][j]))) + grid[i][j]\n        }\n    }\n    return dp[n-1][m-1]\n}\n
min_path_sum.swift
/* 最小経路和:動的計画法 */\nfunc minPathSumDP(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: m), count: n)\n    dp[0][0] = grid[0][0]\n    // 状態遷移:先頭行\n    for j in 1 ..< m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // 状態遷移:先頭列\n    for i in 1 ..< n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1 ..< n {\n        for j in 1 ..< m {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
min_path_sum.js
/* 最小経路和:動的計画法 */\nfunction minPathSumDP(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i < n; i++) {\n        for (let j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.ts
/* 最小経路和:動的計画法 */\nfunction minPathSumDP(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i < n; i++) {\n        for (let j: number = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.dart
/* 最小経路和:動的計画法 */\nint minPathSumDP(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // dp テーブルを初期化\n  List<List<int>> dp = List.generate(n, (i) => List.filled(m, 0));\n  dp[0][0] = grid[0][0];\n  // 状態遷移:先頭行\n  for (int j = 1; j < m; j++) {\n    dp[0][j] = dp[0][j - 1] + grid[0][j];\n  }\n  // 状態遷移:先頭列\n  for (int i = 1; i < n; i++) {\n    dp[i][0] = dp[i - 1][0] + grid[i][0];\n  }\n  // 状態遷移: 残りの行と列\n  for (int i = 1; i < n; i++) {\n    for (int j = 1; j < m; j++) {\n      dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n    }\n  }\n  return dp[n - 1][m - 1];\n}\n
min_path_sum.rs
/* 最小経路和:動的計画法 */\nfn min_path_sum_dp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // dp テーブルを初期化\n    let mut dp = vec![vec![0; m]; n];\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for j in 1..m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for i in 1..n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1..n {\n        for j in 1..m {\n            dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    dp[n - 1][m - 1]\n}\n
min_path_sum.c
/* 最小経路和:動的計画法 */\nint minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // dp テーブルを初期化\n    int **dp = malloc(n * sizeof(int *));\n    for (int i = 0; i < n; i++) {\n        dp[i] = calloc(m, sizeof(int));\n    }\n    dp[0][0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状態遷移:先頭列\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    int res = dp[n - 1][m - 1];\n    // メモリを解放する\n    for (int i = 0; i < n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
min_path_sum.kt
/* 最小経路和:動的計画法 */\nfun minPathSumDP(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // dp テーブルを初期化\n    val dp = Array(n) { IntArray(m) }\n    dp[0][0] = grid[0][0]\n    // 状態遷移:先頭行\n    for (j in 1..<m) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // 状態遷移:先頭列\n    for (i in 1..<n) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // 状態遷移: 残りの行と列\n    for (i in 1..<n) {\n        for (j in 1..<m) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
min_path_sum.rb
### 最小経路和:動的計画法 ###\ndef min_path_sum_dp(grid)\n  n, m = grid.length, grid.first.length\n  # dp テーブルを初期化\n  dp = Array.new(n) { Array.new(m, 0) }\n  dp[0][0] = grid[0][0]\n  # 状態遷移:先頭行\n  (1...m).each { |j| dp[0][j] = dp[0][j - 1] + grid[0][j] }\n  # 状態遷移:先頭列\n  (1...n).each { |i| dp[i][0] = dp[i - 1][0] + grid[i][0] }\n  # 状態遷移: 残りの行と列\n  for i in 1...n\n    for j in 1...m\n      dp[i][j] = [dp[i][j - 1], dp[i - 1][j]].min + grid[i][j]\n    end\n  end\n  dp[n -1][m -1]\nend\n
コードの可視化

全画面で見る >

次の図は最小経路和の状態遷移の過程を示しています。グリッド全体を走査するため、時間計算量は \\(O(nm)\\) です。

配列 dp のサイズは \\(n \\times m\\) であるため、空間計算量は \\(O(nm)\\) です。

<1><2><3><4><5><6><7><8><9><10><11><12>

図 14-16   最小経路和の動的計画法の過程

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#4","level":3,"title":"4.   空間最適化","text":"

各マスは左のマスと上のマスにのみ関係するため、1 行の配列だけを使って \\(dp\\) テーブルを実装できます。

ただし、配列 dp は 1 行分の状態しか表せないため、先頭列の状態を事前に初期化することはできず、各行を走査するときに更新する必要があります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dp_comp(grid: list[list[int]]) -> int:\n    \"\"\"最小経路和:空間最適化後の動的計画法\"\"\"\n    n, m = len(grid), len(grid[0])\n    # dp テーブルを初期化\n    dp = [0] * m\n    # 状態遷移:先頭行\n    dp[0] = grid[0][0]\n    for j in range(1, m):\n        dp[j] = dp[j - 1] + grid[0][j]\n    # 状態遷移:残りの行\n    for i in range(1, n):\n        # 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0]\n        # 状態遷移:残りの列\n        for j in range(1, m):\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n    return dp[m - 1]\n
min_path_sum.cpp
/* 最小経路和:空間最適化後の動的計画法 */\nint minPathSumDPComp(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // dp テーブルを初期化\n    vector<int> dp(m);\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (int j = 1; j < m; j++) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.java
/* 最小経路和:空間最適化後の動的計画法 */\nint minPathSumDPComp(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // dp テーブルを初期化\n    int[] dp = new int[m];\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.cs
/* 最小経路和:空間最適化後の動的計画法 */\nint MinPathSumDPComp(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // dp テーブルを初期化\n    int[] dp = new int[m];\n    dp[0] = grid[0][0];\n    // 状態遷移:先頭行\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.Min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.go
/* 最小経路和:空間最適化後の動的計画法 */\nfunc minPathSumDPComp(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // dp テーブルを初期化\n    dp := make([]int, m)\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0]\n    for j := 1; j < m; j++ {\n        dp[j] = dp[j-1] + grid[0][j]\n    }\n    // 状態遷移: 残りの行と列\n    for i := 1; i < n; i++ {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0]\n        // 状態遷移:残りの列\n        for j := 1; j < m; j++ {\n            dp[j] = int(math.Min(float64(dp[j-1]), float64(dp[j]))) + grid[i][j]\n        }\n    }\n    return dp[m-1]\n}\n
min_path_sum.swift
/* 最小経路和:空間最適化後の動的計画法 */\nfunc minPathSumDPComp(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // dp テーブルを初期化\n    var dp = Array(repeating: 0, count: m)\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0]\n    for j in 1 ..< m {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // 状態遷移:残りの行\n    for i in 1 ..< n {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0]\n        // 状態遷移:残りの列\n        for j in 1 ..< m {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
min_path_sum.js
/* 最小経路和:空間最適化後の動的計画法 */\nfunction minPathSumDPComp(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // dp テーブルを初期化\n    const dp = new Array(m);\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (let i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.ts
/* 最小経路和:空間最適化後の動的計画法 */\nfunction minPathSumDPComp(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // dp テーブルを初期化\n    const dp = new Array(m);\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (let i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.dart
/* 最小経路和:空間最適化後の動的計画法 */\nint minPathSumDPComp(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // dp テーブルを初期化\n  List<int> dp = List.filled(m, 0);\n  dp[0] = grid[0][0];\n  for (int j = 1; j < m; j++) {\n    dp[j] = dp[j - 1] + grid[0][j];\n  }\n  // 状態遷移:残りの行\n  for (int i = 1; i < n; i++) {\n    // 状態遷移:先頭列\n    dp[0] = dp[0] + grid[i][0];\n    // 状態遷移:残りの列\n    for (int j = 1; j < m; j++) {\n      dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n    }\n  }\n  return dp[m - 1];\n}\n
min_path_sum.rs
/* 最小経路和:空間最適化後の動的計画法 */\nfn min_path_sum_dp_comp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // dp テーブルを初期化\n    let mut dp = vec![0; m];\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for j in 1..m {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for i in 1..n {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for j in 1..m {\n            dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    dp[m - 1]\n}\n
min_path_sum.c
/* 最小経路和:空間最適化後の動的計画法 */\nint minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // dp テーブルを初期化\n    int *dp = calloc(m, sizeof(int));\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i < n; i++) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0];\n        // 状態遷移:残りの列\n        for (int j = 1; j < m; j++) {\n            dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    int res = dp[m - 1];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
min_path_sum.kt
/* 最小経路和:空間最適化後の動的計画法 */\nfun minPathSumDPComp(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // dp テーブルを初期化\n    val dp = IntArray(m)\n    // 状態遷移:先頭行\n    dp[0] = grid[0][0]\n    for (j in 1..<m) {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // 状態遷移:残りの行\n    for (i in 1..<n) {\n        // 状態遷移:先頭列\n        dp[0] = dp[0] + grid[i][0]\n        // 状態遷移:残りの列\n        for (j in 1..<m) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
min_path_sum.rb
### 最小経路和:空間最適化後の動的計画法 ###\ndef min_path_sum_dp_comp(grid)\n  n, m = grid.length, grid.first.length\n  # dp テーブルを初期化\n  dp = Array.new(m, 0)\n  # 状態遷移:先頭行\n  dp[0] = grid[0][0]\n  (1...m).each { |j| dp[j] = dp[j - 1] + grid[0][j] }\n  # 状態遷移:残りの行\n  for i in 1...n\n    # 状態遷移:先頭列\n    dp[0] = dp[0] + grid[i][0]\n    # 状態遷移:残りの列\n    (1...m).each { |j| dp[j] = [dp[j - 1], dp[j]].min + grid[i][j] }\n  end\n  dp[m - 1]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.3   動的計画法の問題解決の考え方"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/","level":1,"title":"14.6   編集距離問題","text":"

編集距離は、Levenshtein 距離とも呼ばれ、2つの文字列の相互変換に必要な最小の編集回数を指し、通常は情報検索や自然言語処理において2つの系列の類似度を測るために用いられます。

Question

2つの文字列 \\(s\\) と \\(t\\) を入力し、\\(s\\) を \\(t\\) に変換するのに必要な最小編集回数を返してください。

1つの文字列に対して3種類の編集操作を行えます。1文字の挿入、1文字の削除、任意の文字への置換です。

下図に示すように、kittensitting に変換するには 3 回の編集が必要で、内訳は 2 回の置換と 1 回の挿入です。helloalgo に変換する場合も 3 回必要で、内訳は 2 回の置換と 1 回の削除です。

図 14-27   編集距離のサンプルデータ

編集距離問題は決定木モデルで自然に説明できます。文字列が木のノードに対応し、1回の決定(1回の編集操作)が木の1本の辺に対応します。

下図に示すように、操作に制限がない場合、各ノードからは多くの辺を派生でき、それぞれの辺が1種類の操作に対応します。これは hello から algo への変換に多くの経路があり得ることを意味します。

決定木の観点から見ると、本問の目標はノード hello とノード algo の間の最短経路を求めることです。

図 14-28   決定木モデルに基づく編集距離問題の表現

","path":["第 14 章   動的計画法","14.6   編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#1","level":3,"title":"1.   動的計画法の考え方","text":"

第1ステップ:各ラウンドの決定を考え、状態を定義して、\\(dp\\) テーブルを得る

各ラウンドの決定は、文字列 \\(s\\) に対して1回の編集操作を行うことです。

編集操作の過程で問題の規模が徐々に小さくなることを期待します。そうして初めて部分問題を構築できます。文字列 \\(s\\) と \\(t\\) の長さをそれぞれ \\(n\\) と \\(m\\) とし、まず両文字列の末尾の文字 \\(s[n-1]\\) と \\(t[m-1]\\) を考えます。

  • \\(s[n-1]\\) と \\(t[m-1]\\) が同じなら、それらをスキップして、直接 \\(s[n-2]\\) と \\(t[m-2]\\) を考えます。
  • \\(s[n-1]\\) と \\(t[m-1]\\) が異なるなら、\\(s\\) に対して1回の編集(挿入、削除、置換)を行い、両文字列の末尾の文字を同じにします。そうすることでそれらをスキップし、より小さい問題を考えられます。

つまり、文字列 \\(s\\) に対する各ラウンドの決定(編集操作)は、\\(s\\) と \\(t\\) における残りの未一致文字を変化させます。したがって、状態は現在 \\(s\\) と \\(t\\) で考えている第 \\(i\\) と第 \\(j\\) 文字とし、\\([i, j]\\) と記します。

状態 \\([i, j]\\) に対応する部分問題は、**\\(s\\) の先頭 \\(i\\) 文字を \\(t\\) の先頭 \\(j\\) 文字に変換するのに必要な最小編集回数**です。

これにより、サイズが \\((i+1) \\times (j+1)\\) の2次元 \\(dp\\) テーブルが得られます。

第2ステップ:最適部分構造を見つけ、状態遷移方程式を導く

部分問題 \\(dp[i, j]\\) を考えます。これに対応する2つの文字列の末尾文字は \\(s[i-1]\\) と \\(t[j-1]\\) であり、編集操作の違いに応じて下図の3つの場合に分けられます。

  1. \\(s[i-1]\\) の後ろに \\(t[j-1]\\) を追加する。このとき残る部分問題は \\(dp[i, j-1]\\) です。
  2. \\(s[i-1]\\) を削除する。このとき残る部分問題は \\(dp[i-1, j]\\) です。
  3. \\(s[i-1]\\) を \\(t[j-1]\\) に置き換える。このとき残る部分問題は \\(dp[i-1, j-1]\\) です。

図 14-29   編集距離の状態遷移

以上の分析から、最適部分構造は次のように得られます。\\(dp[i, j]\\) の最小編集回数は、\\(dp[i, j-1]\\)、\\(dp[i-1, j]\\)、\\(dp[i-1, j-1]\\) の3つのうち最小の編集回数に、今回の編集回数 \\(1\\) を加えたものです。対応する状態遷移方程式は次のとおりです:

\\[ dp[i, j] = \\min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \\]

注意すべき点として、\\(s[i-1]\\) と \\(t[j-1]\\) が同じ場合、現在の文字を編集する必要はありません。この場合の状態遷移方程式は次のとおりです:

\\[ dp[i, j] = dp[i-1, j-1] \\]

第3ステップ:境界条件と状態遷移の順序を決める

2つの文字列がともに空のとき、編集回数は \\(0\\)、すなわち \\(dp[0, 0] = 0\\) です。\\(s\\) が空で \\(t\\) が空でないとき、最小編集回数は \\(t\\) の長さに等しいため、先頭行は \\(dp[0, j] = j\\) です。\\(s\\) が空でなく \\(t\\) が空のとき、最小編集回数は \\(s\\) の長さに等しいため、先頭列は \\(dp[i, 0] = i\\) です。

状態遷移方程式を観察すると、\\(dp[i, j]\\) の解は左、上、左上の解に依存します。そのため、2重ループで \\(dp\\) テーブル全体を順方向に走査すれば十分です。

","path":["第 14 章   動的計画法","14.6   編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#2","level":3,"title":"2.   コードの実装","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
def edit_distance_dp(s: str, t: str) -> int:\n    \"\"\"編集距離:動的計画法\"\"\"\n    n, m = len(s), len(t)\n    dp = [[0] * (m + 1) for _ in range(n + 1)]\n    # 状態遷移:先頭行と先頭列\n    for i in range(1, n + 1):\n        dp[i][0] = i\n    for j in range(1, m + 1):\n        dp[0][j] = j\n    # 状態遷移: 残りの行と列\n    for i in range(1, n + 1):\n        for j in range(1, m + 1):\n            if s[i - 1] == t[j - 1]:\n                # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            else:\n                # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1\n    return dp[n][m]\n
edit_distance.cpp
/* 編集距離:動的計画法 */\nint editDistanceDP(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));\n    // 状態遷移:先頭行と先頭列\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.java
/* 編集距離:動的計画法 */\nint editDistanceDP(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[][] dp = new int[n + 1][m + 1];\n    // 状態遷移:先頭行と先頭列\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.cs
/* 編集距離:動的計画法 */\nint EditDistanceDP(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[,] dp = new int[n + 1, m + 1];\n    // 状態遷移:先頭行と先頭列\n    for (int i = 1; i <= n; i++) {\n        dp[i, 0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0, j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i, j] = dp[i - 1, j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n, m];\n}\n
edit_distance.go
/* 編集距離:動的計画法 */\nfunc editDistanceDP(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, m+1)\n    }\n    // 状態遷移:先頭行と先頭列\n    for i := 1; i <= n; i++ {\n        dp[i][0] = i\n    }\n    for j := 1; j <= m; j++ {\n        dp[0][j] = j\n    }\n    // 状態遷移: 残りの行と列\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= m; j++ {\n            if s[i-1] == t[j-1] {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i-1][j-1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.swift
/* 編集距離:動的計画法 */\nfunc editDistanceDP(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)\n    // 状態遷移:先頭行と先頭列\n    for i in 1 ... n {\n        dp[i][0] = i\n    }\n    for j in 1 ... m {\n        dp[0][j] = j\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1 ... n {\n        for j in 1 ... m {\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.js
/* 編集距離:動的計画法 */\nfunction editDistanceDP(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));\n    // 状態遷移:先頭行と先頭列\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.ts
/* 編集距離:動的計画法 */\nfunction editDistanceDP(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: m + 1 }, () => 0)\n    );\n    // 状態遷移:先頭行と先頭列\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.dart
/* 編集距離:動的計画法 */\nint editDistanceDP(String s, String t) {\n  int n = s.length, m = t.length;\n  List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));\n  // 状態遷移:先頭行と先頭列\n  for (int i = 1; i <= n; i++) {\n    dp[i][0] = i;\n  }\n  for (int j = 1; j <= m; j++) {\n    dp[0][j] = j;\n  }\n  // 状態遷移: 残りの行と列\n  for (int i = 1; i <= n; i++) {\n    for (int j = 1; j <= m; j++) {\n      if (s[i - 1] == t[j - 1]) {\n        // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[i][j] = dp[i - 1][j - 1];\n      } else {\n        // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n        dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n      }\n    }\n  }\n  return dp[n][m];\n}\n
edit_distance.rs
/* 編集距離:動的計画法 */\nfn edit_distance_dp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![vec![0; m + 1]; n + 1];\n    // 状態遷移:先頭行と先頭列\n    for i in 1..=n {\n        dp[i][0] = i as i32;\n    }\n    for j in 1..m {\n        dp[0][j] = j as i32;\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1..=n {\n        for j in 1..=m {\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] =\n                    std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    dp[n][m]\n}\n
edit_distance.c
/* 編集距離:動的計画法 */\nint editDistanceDP(char *s, char *t, int n, int m) {\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(m + 1, sizeof(int));\n    }\n    // 状態遷移:先頭行と先頭列\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    int res = dp[n][m];\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
edit_distance.kt
/* 編集距離:動的計画法 */\nfun editDistanceDP(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = Array(n + 1) { IntArray(m + 1) }\n    // 状態遷移:先頭行と先頭列\n    for (i in 1..n) {\n        dp[i][0] = i\n    }\n    for (j in 1..m) {\n        dp[0][j] = j\n    }\n    // 状態遷移: 残りの行と列\n    for (i in 1..n) {\n        for (j in 1..m) {\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.rb
### 編集距離:動的計画法 ###\ndef edit_distance_dp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(n + 1) { Array.new(m + 1, 0) }\n  # 状態遷移:先頭行と先頭列\n  (1...(n + 1)).each { |i| dp[i][0] = i }\n  (1...(m + 1)).each { |j| dp[0][j] = j }\n  # 状態遷移: 残りの行と列\n  for i in 1...(n + 1)\n    for j in 1...(m +1)\n      if s[i - 1] == t[j - 1]\n        # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[i][j] = dp[i - 1][j - 1]\n      else\n        # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n        dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1\n      end\n    end\n  end\n  dp[n][m]\nend\n
コードの可視化

全画面で見る >

下図に示すように、編集距離問題の状態遷移の過程はナップサック問題と非常によく似ており、どちらも2次元グリッドを埋めていく過程とみなせます。

<1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

図 14-30   編集距離の動的計画法の過程

","path":["第 14 章   動的計画法","14.6   編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#3","level":3,"title":"3.   空間最適化","text":"

\\(dp[i,j]\\) は上の \\(dp[i-1, j]\\)、左の \\(dp[i, j-1]\\)、左上の \\(dp[i-1, j-1]\\) から遷移されますが、順方向走査では左上の \\(dp[i-1, j-1]\\) を失い、逆方向走査では \\(dp[i, j-1]\\) を事前に構築できません。そのため、どちらの走査順序も適切ではありません。

そのため、変数 leftup を用いて左上の解 \\(dp[i-1, j-1]\\) を一時保存し、左と上の解だけを考えればよくなります。このときの状況は完全ナップサック問題と同じであり、順方向走査を用いることができます。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
def edit_distance_dp_comp(s: str, t: str) -> int:\n    \"\"\"編集距離:空間最適化した動的計画法\"\"\"\n    n, m = len(s), len(t)\n    dp = [0] * (m + 1)\n    # 状態遷移:先頭行\n    for j in range(1, m + 1):\n        dp[j] = j\n    # 状態遷移:残りの行\n    for i in range(1, n + 1):\n        # 状態遷移:先頭列\n        leftup = dp[0]  # dp[i-1, j-1] を一時保存する\n        dp[0] += 1\n        # 状態遷移:残りの列\n        for j in range(1, m + 1):\n            temp = dp[j]\n            if s[i - 1] == t[j - 1]:\n                # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            else:\n                # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = min(dp[j - 1], dp[j], leftup) + 1\n            leftup = temp  # 次の反復の dp[i-1, j-1] に更新する\n    return dp[m]\n
edit_distance.cpp
/* 編集距離:空間最適化した動的計画法 */\nint editDistanceDPComp(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<int> dp(m + 1, 0);\n    // 状態遷移:先頭行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m];\n}\n
edit_distance.java
/* 編集距離:空間最適化した動的計画法 */\nint editDistanceDPComp(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[] dp = new int[m + 1];\n    // 状態遷移:先頭行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m];\n}\n
edit_distance.cs
/* 編集距離:空間最適化した動的計画法 */\nint EditDistanceDPComp(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[] dp = new int[m + 1];\n    // 状態遷移:先頭行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m];\n}\n
edit_distance.go
/* 編集距離:空間最適化した動的計画法 */\nfunc editDistanceDPComp(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([]int, m+1)\n    // 状態遷移:先頭行\n    for j := 1; j <= m; j++ {\n        dp[j] = j\n    }\n    // 状態遷移:残りの行\n    for i := 1; i <= n; i++ {\n        // 状態遷移:先頭列\n        leftUp := dp[0] // dp[i-1, j-1] を一時保存する\n        dp[0] = i\n        // 状態遷移:残りの列\n        for j := 1; j <= m; j++ {\n            temp := dp[j]\n            if s[i-1] == t[j-1] {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftUp\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1\n            }\n            leftUp = temp // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m]\n}\n
edit_distance.swift
/* 編集距離:空間最適化した動的計画法 */\nfunc editDistanceDPComp(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: 0, count: m + 1)\n    // 状態遷移:先頭行\n    for j in 1 ... m {\n        dp[j] = j\n    }\n    // 状態遷移:残りの行\n    for i in 1 ... n {\n        // 状態遷移:先頭列\n        var leftup = dp[0] // dp[i-1, j-1] を一時保存する\n        dp[0] = i\n        // 状態遷移:残りの列\n        for j in 1 ... m {\n            let temp = dp[j]\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m]\n}\n
edit_distance.js
/* 編集距離:空間最適化した動的計画法 */\nfunction editDistanceDPComp(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // 状態遷移:先頭行\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (let i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        let leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m];\n}\n
edit_distance.ts
/* 編集距離:空間最適化した動的計画法 */\nfunction editDistanceDPComp(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // 状態遷移:先頭行\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (let i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        let leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m];\n}\n
edit_distance.dart
/* 編集距離:空間最適化した動的計画法 */\nint editDistanceDPComp(String s, String t) {\n  int n = s.length, m = t.length;\n  List<int> dp = List.filled(m + 1, 0);\n  // 状態遷移:先頭行\n  for (int j = 1; j <= m; j++) {\n    dp[j] = j;\n  }\n  // 状態遷移:残りの行\n  for (int i = 1; i <= n; i++) {\n    // 状態遷移:先頭列\n    int leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n    dp[0] = i;\n    // 状態遷移:残りの列\n    for (int j = 1; j <= m; j++) {\n      int temp = dp[j];\n      if (s[i - 1] == t[j - 1]) {\n        // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[j] = leftup;\n      } else {\n        // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n        dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n      }\n      leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n    }\n  }\n  return dp[m];\n}\n
edit_distance.rs
/* 編集距離:空間最適化した動的計画法 */\nfn edit_distance_dp_comp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![0; m + 1];\n    // 状態遷移:先頭行\n    for j in 1..m {\n        dp[j] = j as i32;\n    }\n    // 状態遷移:残りの行\n    for i in 1..=n {\n        // 状態遷移:先頭列\n        let mut leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i as i32;\n        // 状態遷移:残りの列\n        for j in 1..=m {\n            let temp = dp[j];\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    dp[m]\n}\n
edit_distance.c
/* 編集距離:空間最適化した動的計画法 */\nint editDistanceDPComp(char *s, char *t, int n, int m) {\n    int *dp = calloc(m + 1, sizeof(int));\n    // 状態遷移:先頭行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状態遷移:残りの行\n    for (int i = 1; i <= n; i++) {\n        // 状態遷移:先頭列\n        int leftup = dp[0]; // dp[i-1, j-1] を一時保存する\n        dp[0] = i;\n        // 状態遷移:残りの列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup;\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    int res = dp[m];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
edit_distance.kt
/* 編集距離:空間最適化した動的計画法 */\nfun editDistanceDPComp(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = IntArray(m + 1)\n    // 状態遷移:先頭行\n    for (j in 1..m) {\n        dp[j] = j\n    }\n    // 状態遷移:残りの行\n    for (i in 1..n) {\n        // 状態遷移:先頭列\n        var leftup = dp[0] // dp[i-1, j-1] を一時保存する\n        dp[0] = i\n        // 状態遷移:残りの列\n        for (j in 1..m) {\n            val temp = dp[j]\n            if (s[i - 1] == t[j - 1]) {\n                // 2 つの文字が等しければ、その 2 文字をそのままスキップする\n                dp[j] = leftup\n            } else {\n                // 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // 次の反復の dp[i-1, j-1] に更新する\n        }\n    }\n    return dp[m]\n}\n
edit_distance.rb
### 編集距離:空間最適化した動的計画法 ###\ndef edit_distance_dp_comp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(m + 1, 0)\n  # 状態遷移:先頭行\n  (1...(m + 1)).each { |j| dp[j] = j }\n  # 状態遷移:残りの行\n  for i in 1...(n + 1)\n    # 状態遷移:先頭列\n    leftup = dp.first # dp[i-1, j-1] を一時保存する\n    dp[0] += 1\n    # 状態遷移:残りの列\n    for j in 1...(m + 1)\n      temp = dp[j]\n      if s[i - 1] == t[j - 1]\n        # 2 つの文字が等しければ、その 2 文字をそのままスキップする\n        dp[j] = leftup\n      else\n        # 最小編集回数 = 挿入・削除・置換の 3 操作における最小編集回数 + 1\n        dp[j] = [dp[j - 1], dp[j], leftup].min + 1\n      end\n      leftup = temp # 次の反復の dp[i-1, j-1] に更新する\n    end\n  end\n  dp[m]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.6   編集距離問題"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/","level":1,"title":"14.1   動的計画法入門","text":"

動的計画法(dynamic programming)は重要なアルゴリズムパラダイムであり、問題をより小さな部分問題の列に分解し、それらの解を保存して重複計算を避けることで、時間効率を大幅に向上させます。

本節では、古典的な例題から始めて、まずその力任せのバックトラッキング解法を示し、そこに含まれる重複部分問題を観察したうえで、より効率的な動的計画法の解法を段階的に導きます。

階段を上る

全体で \\(n\\) 段ある階段が与えられ、各ステップで \\(1\\) 段または \\(2\\) 段上ることができます。頂上まで到達する方法は何通りあるでしょうか?

次の図に示すように、\\(3\\) 段の階段では、頂上まで到達する方法は全部で \\(3\\) 通りあります。

図 14-1   3 段の階段を上る方法の数

この問題の目的は方法の総数を求めることです。考えられるすべての可能性をバックトラッキングで総当たりすることができます。具体的には、階段を上ることを複数ラウンドの選択過程とみなし、地面から出発して各ラウンドで \\(1\\) 段または \\(2\\) 段上ります。階段の頂上に到達するたびに方法数を \\(1\\) 増やし、頂上を越えた場合は枝刈りします。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_backtrack.py
def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:\n    \"\"\"バックトラッキング\"\"\"\n    # 第 n 段に到達したら、方法数を 1 増やす\n    if state == n:\n        res[0] += 1\n    # すべての選択肢を走査\n    for choice in choices:\n        # 枝刈り: 第 n 段を超えないようにする\n        if state + choice > n:\n            continue\n        # 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res)\n        # バックトラック\n\ndef climbing_stairs_backtrack(n: int) -> int:\n    \"\"\"階段登り:バックトラッキング\"\"\"\n    choices = [1, 2]  # 1 段または 2 段上ることを選べる\n    state = 0  # 第 0 段から上り始める\n    res = [0]  # res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res)\n    return res[0]\n
climbing_stairs_backtrack.cpp
/* バックトラッキング */\nvoid backtrack(vector<int> &choices, int state, int n, vector<int> &res) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state == n)\n        res[0]++;\n    // すべての選択肢を走査\n    for (auto &choice : choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n)\n            continue;\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nint climbingStairsBacktrack(int n) {\n    vector<int> choices = {1, 2}; // 1 段または 2 段上ることを選べる\n    int state = 0;                // 第 0 段から上り始める\n    vector<int> res = {0};        // res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res);\n    return res[0];\n}\n
climbing_stairs_backtrack.java
/* バックトラッキング */\nvoid backtrack(List<Integer> choices, int state, int n, List<Integer> res) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state == n)\n        res.set(0, res.get(0) + 1);\n    // すべての選択肢を走査\n    for (Integer choice : choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n)\n            continue;\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nint climbingStairsBacktrack(int n) {\n    List<Integer> choices = Arrays.asList(1, 2); // 1 段または 2 段上ることを選べる\n    int state = 0; // 第 0 段から上り始める\n    List<Integer> res = new ArrayList<>();\n    res.add(0); // res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.cs
/* バックトラッキング */\nvoid Backtrack(List<int> choices, int state, int n, List<int> res) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state == n)\n        res[0]++;\n    // すべての選択肢を走査\n    foreach (int choice in choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n)\n            continue;\n        // 試行: 選択を行い、状態を更新\n        Backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nint ClimbingStairsBacktrack(int n) {\n    List<int> choices = [1, 2]; // 1 段または 2 段上ることを選べる\n    int state = 0; // 第 0 段から上り始める\n    List<int> res = [0]; // res[0] を使って方法数を記録する\n    Backtrack(choices, state, n, res);\n    return res[0];\n}\n
climbing_stairs_backtrack.go
/* バックトラッキング */\nfunc backtrack(choices []int, state, n int, res []int) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if state == n {\n        res[0] = res[0] + 1\n    }\n    // すべての選択肢を走査\n    for _, choice := range choices {\n        // 枝刈り: 第 n 段を超えないようにする\n        if state+choice > n {\n            continue\n        }\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state+choice, n, res)\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfunc climbingStairsBacktrack(n int) int {\n    // 1 段または 2 段上ることを選べる\n    choices := []int{1, 2}\n    // 第 0 段から上り始める\n    state := 0\n    res := make([]int, 1)\n    // res[0] を使って方法数を記録する\n    res[0] = 0\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
climbing_stairs_backtrack.swift
/* バックトラッキング */\nfunc backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if state == n {\n        res[0] += 1\n    }\n    // すべての選択肢を走査\n    for choice in choices {\n        // 枝刈り: 第 n 段を超えないようにする\n        if state + choice > n {\n            continue\n        }\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices: choices, state: state + choice, n: n, res: &res)\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfunc climbingStairsBacktrack(n: Int) -> Int {\n    let choices = [1, 2] // 1 段または 2 段上ることを選べる\n    let state = 0 // 第 0 段から上り始める\n    var res: [Int] = []\n    res.append(0) // res[0] を使って方法数を記録する\n    backtrack(choices: choices, state: state, n: n, res: &res)\n    return res[0]\n}\n
climbing_stairs_backtrack.js
/* バックトラッキング */\nfunction backtrack(choices, state, n, res) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state === n) res.set(0, res.get(0) + 1);\n    // すべての選択肢を走査\n    for (const choice of choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n) continue;\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfunction climbingStairsBacktrack(n) {\n    const choices = [1, 2]; // 1 段または 2 段上ることを選べる\n    const state = 0; // 第 0 段から上り始める\n    const res = new Map();\n    res.set(0, 0); // res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.ts
/* バックトラッキング */\nfunction backtrack(\n    choices: number[],\n    state: number,\n    n: number,\n    res: Map<0, any>\n): void {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state === n) res.set(0, res.get(0) + 1);\n    // すべての選択肢を走査\n    for (const choice of choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n) continue;\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfunction climbingStairsBacktrack(n: number): number {\n    const choices = [1, 2]; // 1 段または 2 段上ることを選べる\n    const state = 0; // 第 0 段から上り始める\n    const res = new Map();\n    res.set(0, 0); // res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.dart
/* バックトラッキング */\nvoid backtrack(List<int> choices, int state, int n, List<int> res) {\n  // 第 n 段に到達したら、方法数を 1 増やす\n  if (state == n) {\n    res[0]++;\n  }\n  // すべての選択肢を走査\n  for (int choice in choices) {\n    // 枝刈り: 第 n 段を超えないようにする\n    if (state + choice > n) continue;\n    // 試行: 選択を行い、状態を更新\n    backtrack(choices, state + choice, n, res);\n    // バックトラック\n  }\n}\n\n/* 階段登り:バックトラッキング */\nint climbingStairsBacktrack(int n) {\n  List<int> choices = [1, 2]; // 1 段または 2 段上ることを選べる\n  int state = 0; // 第 0 段から上り始める\n  List<int> res = [];\n  res.add(0); // res[0] を使って方法数を記録する\n  backtrack(choices, state, n, res);\n  return res[0];\n}\n
climbing_stairs_backtrack.rs
/* バックトラッキング */\nfn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if state == n {\n        res[0] = res[0] + 1;\n    }\n    // すべての選択肢を走査\n    for &choice in choices {\n        // 枝刈り: 第 n 段を超えないようにする\n        if state + choice > n {\n            continue;\n        }\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfn climbing_stairs_backtrack(n: usize) -> i32 {\n    let choices = vec![1, 2]; // 1 段または 2 段上ることを選べる\n    let state = 0; // 第 0 段から上り始める\n    let mut res = Vec::new();\n    res.push(0); // res[0] を使って方法数を記録する\n    backtrack(&choices, state, n as i32, &mut res);\n    res[0]\n}\n
climbing_stairs_backtrack.c
/* バックトラッキング */\nvoid backtrack(int *choices, int state, int n, int *res, int len) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state == n)\n        res[0]++;\n    // すべての選択肢を走査\n    for (int i = 0; i < len; i++) {\n        int choice = choices[i];\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n)\n            continue;\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res, len);\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nint climbingStairsBacktrack(int n) {\n    int choices[2] = {1, 2}; // 1 段または 2 段上ることを選べる\n    int state = 0;           // 第 0 段から上り始める\n    int *res = (int *)malloc(sizeof(int));\n    *res = 0; // res[0] を使って方法数を記録する\n    int len = sizeof(choices) / sizeof(int);\n    backtrack(choices, state, n, res, len);\n    int result = *res;\n    free(res);\n    return result;\n}\n
climbing_stairs_backtrack.kt
/* バックトラッキング */\nfun backtrack(\n    choices: MutableList<Int>,\n    state: Int,\n    n: Int,\n    res: MutableList<Int>\n) {\n    // 第 n 段に到達したら、方法数を 1 増やす\n    if (state == n)\n        res[0] = res[0] + 1\n    // すべての選択肢を走査\n    for (choice in choices) {\n        // 枝刈り: 第 n 段を超えないようにする\n        if (state + choice > n) continue\n        // 試行: 選択を行い、状態を更新\n        backtrack(choices, state + choice, n, res)\n        // バックトラック\n    }\n}\n\n/* 階段登り:バックトラッキング */\nfun climbingStairsBacktrack(n: Int): Int {\n    val choices = mutableListOf(1, 2) // 1 段または 2 段上ることを選べる\n    val state = 0 // 第 0 段から上り始める\n    val res = mutableListOf<Int>()\n    res.add(0) // res[0] を使って方法数を記録する\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
climbing_stairs_backtrack.rb
### バックトラッキング ###\ndef backtrack(choices, state, n, res)\n  # 第 n 段に到達したら、方法数を 1 増やす\n  res[0] += 1 if state == n\n  # すべての選択肢を走査\n  for choice in choices\n    # 枝刈り: 第 n 段を超えないようにする\n    next if state + choice > n\n\n    # 試行: 選択を行い、状態を更新\n    backtrack(choices, state + choice, n, res)\n  end\n  # バックトラック\nend\n\n### 階段登り:バックトラッキング ###\ndef climbing_stairs_backtrack(n)\n  choices = [1, 2] # 1 段または 2 段上ることを選べる\n  state = 0 # 第 0 段から上り始める\n  res = [0] # res[0] を使って方法数を記録する\n  backtrack(choices, state, n, res)\n  res.first\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.1   動的計画法入門"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1411-1","level":2,"title":"14.1.1   方法 1:総当たり探索","text":"

バックトラッキング法は通常、問題を明示的に分解するのではなく、問題解決を一連の意思決定ステップとみなし、試行と枝刈りによってあらゆる可能な解を探索します。

この問題を問題分解の観点から分析してみましょう。\\(i\\) 段目まで上る方法が全部で \\(dp[i]\\) 通りあるとすると、\\(dp[i]\\) が元の問題であり、その部分問題には次が含まれます:

\\[ dp[i-1], dp[i-2], \\dots, dp[2], dp[1] \\]

各ラウンドでは \\(1\\) 段または \\(2\\) 段しか上れないため、\\(i\\) 段目の階段に立っているとき、直前のラウンドでは \\(i - 1\\) 段目または \\(i - 2\\) 段目にしか立てません。言い換えると、\\(i -1\\) 段目または \\(i - 2\\) 段目からしか \\(i\\) 段目へ進めません。

ここから重要な帰結が得られます。**\\(i - 1\\) 段目まで上る方法数と \\(i - 2\\) 段目まで上る方法数の和が、\\(i\\) 段目まで上る方法数に等しい**のです。式は次のとおりです:

\\[ dp[i] = dp[i-1] + dp[i-2] \\]

これは、階段を上る問題では各部分問題の間に漸化関係があり、**元の問題の解は部分問題の解から構築できる**ことを意味します。次の図はこの漸化関係を示しています。

図 14-2   方法数の漸化関係

漸化式に基づいて総当たり探索の解法を得ることができます。\\(dp[n]\\) を出発点とし、**より大きな問題を再帰的に 2 つのより小さな問題の和へ分解**していき、最小部分問題 \\(dp[1]\\) と \\(dp[2]\\) に到達したら返します。ここで最小部分問題の解は既知であり、\\(dp[1] = 1\\)、\\(dp[2] = 2\\) です。これは、第 \\(1\\) 段目と第 \\(2\\) 段目まで上る方法がそれぞれ \\(1\\) 通り、\\(2\\) 通りであることを表します。

次のコードを見ると、標準的なバックトラッキングコードと同じく深さ優先探索に属しますが、より簡潔です:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs.py
def dfs(i: int) -> int:\n    \"\"\"検索\"\"\"\n    # dp[1] と dp[2] は既知なので返す\n    if i == 1 or i == 2:\n        return i\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1) + dfs(i - 2)\n    return count\n\ndef climbing_stairs_dfs(n: int) -> int:\n    \"\"\"階段登り:探索\"\"\"\n    return dfs(n)\n
climbing_stairs_dfs.cpp
/* 検索 */\nint dfs(int i) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.java
/* 検索 */\nint dfs(int i) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.cs
/* 検索 */\nint DFS(int i) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1) + DFS(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nint ClimbingStairsDFS(int n) {\n    return DFS(n);\n}\n
climbing_stairs_dfs.go
/* 検索 */\nfunc dfs(i int) int {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfs(i-1) + dfs(i-2)\n    return count\n}\n\n/* 階段登り:探索 */\nfunc climbingStairsDFS(n int) int {\n    return dfs(n)\n}\n
climbing_stairs_dfs.swift
/* 検索 */\nfunc dfs(i: Int) -> Int {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1) + dfs(i: i - 2)\n    return count\n}\n\n/* 階段登り:探索 */\nfunc climbingStairsDFS(n: Int) -> Int {\n    dfs(i: n)\n}\n
climbing_stairs_dfs.js
/* 検索 */\nfunction dfs(i) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nfunction climbingStairsDFS(n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.ts
/* 検索 */\nfunction dfs(i: number): number {\n    // dp[1] と dp[2] は既知なので返す\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nfunction climbingStairsDFS(n: number): number {\n    return dfs(n);\n}\n
climbing_stairs_dfs.dart
/* 検索 */\nint dfs(int i) {\n  // dp[1] と dp[2] は既知なので返す\n  if (i == 1 || i == 2) return i;\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1) + dfs(i - 2);\n  return count;\n}\n\n/* 階段登り:探索 */\nint climbingStairsDFS(int n) {\n  return dfs(n);\n}\n
climbing_stairs_dfs.rs
/* 検索 */\nfn dfs(i: usize) -> i32 {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1) + dfs(i - 2);\n    count\n}\n\n/* 階段登り:探索 */\nfn climbing_stairs_dfs(n: usize) -> i32 {\n    dfs(n)\n}\n
climbing_stairs_dfs.c
/* 検索 */\nint dfs(int i) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 階段登り:探索 */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.kt
/* 検索 */\nfun dfs(i: Int): Int {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2) return i\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1) + dfs(i - 2)\n    return count\n}\n\n/* 階段登り:探索 */\nfun climbingStairsDFS(n: Int): Int {\n    return dfs(n)\n}\n
climbing_stairs_dfs.rb
### 探索 ###\ndef dfs(i)\n  # dp[1] と dp[2] は既知なので返す\n  return i if i == 1 || i == 2\n  # dp[i] = dp[i-1] + dp[i-2]\n  dfs(i - 1) + dfs(i - 2)\nend\n\n### 階段登り:探索 ###\ndef climbing_stairs_dfs(n)\n  dfs(n)\nend\n
コードの可視化

全画面で見る >

次の図は総当たり探索によって形成される再帰木を示しています。問題 \\(dp[n]\\) に対して、その再帰木の深さは \\(n\\)、時間計算量は \\(O(2^n)\\) です。指数オーダーは爆発的に増加するため、比較的大きな \\(n\\) を入力すると長時間待たされることになります。

図 14-3   階段上りに対応する再帰木

上の図を見ると、指数オーダーの時間計算量は「重複部分問題」によって生じています。たとえば \\(dp[9]\\) は \\(dp[8]\\) と \\(dp[7]\\) に分解され、\\(dp[8]\\) は \\(dp[7]\\) と \\(dp[6]\\) に分解されるため、どちらにも部分問題 \\(dp[7]\\) が含まれています。

このように、部分問題の中にはさらに小さな重複部分問題が含まれ、それが際限なく続いていきます。計算資源の大部分は、こうした重複部分問題に浪費されています。

","path":["第 14 章   動的計画法","14.1   動的計画法入門"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1412-2","level":2,"title":"14.1.2   方法 2:メモ化探索","text":"

アルゴリズム効率を高めるため、**すべての重複部分問題を 1 回だけ計算したい**と考えます。そのために、各部分問題の解を記録する配列 mem を宣言し、探索の過程で重複部分問題を枝刈りします。

  1. \\(dp[i]\\) を初めて計算したとき、その結果を mem[i] に記録して後で使えるようにします。
  2. 再び \\(dp[i]\\) を計算する必要が生じたときは、mem[i] から直接結果を取得し、その部分問題の重複計算を避けます。

コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs_mem.py
def dfs(i: int, mem: list[int]) -> int:\n    \"\"\"メモ化探索\"\"\"\n    # dp[1] と dp[2] は既知なので返す\n    if i == 1 or i == 2:\n        return i\n    # dp[i] の記録があれば、それをそのまま返す\n    if mem[i] != -1:\n        return mem[i]\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    # dp[i] を記録する\n    mem[i] = count\n    return count\n\ndef climbing_stairs_dfs_mem(n: int) -> int:\n    \"\"\"階段登り:メモ化探索\"\"\"\n    # mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    mem = [-1] * (n + 1)\n    return dfs(n, mem)\n
climbing_stairs_dfs_mem.cpp
/* メモ化探索 */\nint dfs(int i, vector<int> &mem) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nint climbingStairsDFSMem(int n) {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    vector<int> mem(n + 1, -1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.java
/* メモ化探索 */\nint dfs(int i, int[] mem) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nint climbingStairsDFSMem(int n) {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    int[] mem = new int[n + 1];\n    Arrays.fill(mem, -1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.cs
/* メモ化探索 */\nint DFS(int i, int[] mem) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1, mem) + DFS(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nint ClimbingStairsDFSMem(int n) {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    int[] mem = new int[n + 1];\n    Array.Fill(mem, -1);\n    return DFS(n, mem);\n}\n
climbing_stairs_dfs_mem.go
/* メモ化探索 */\nfunc dfsMem(i int, mem []int) int {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] の記録があれば、それをそのまま返す\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfsMem(i-1, mem) + dfsMem(i-2, mem)\n    // dp[i] を記録する\n    mem[i] = count\n    return count\n}\n\n/* 階段登り:メモ化探索 */\nfunc climbingStairsDFSMem(n int) int {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    mem := make([]int, n+1)\n    for i := range mem {\n        mem[i] = -1\n    }\n    return dfsMem(n, mem)\n}\n
climbing_stairs_dfs_mem.swift
/* メモ化探索 */\nfunc dfs(i: Int, mem: inout [Int]) -> Int {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] の記録があれば、それをそのまま返す\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1, mem: &mem) + dfs(i: i - 2, mem: &mem)\n    // dp[i] を記録する\n    mem[i] = count\n    return count\n}\n\n/* 階段登り:メモ化探索 */\nfunc climbingStairsDFSMem(n: Int) -> Int {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    var mem = Array(repeating: -1, count: n + 1)\n    return dfs(i: n, mem: &mem)\n}\n
climbing_stairs_dfs_mem.js
/* メモ化探索 */\nfunction dfs(i, mem) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i === 1 || i === 2) return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nfunction climbingStairsDFSMem(n) {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.ts
/* メモ化探索 */\nfunction dfs(i: number, mem: number[]): number {\n    // dp[1] と dp[2] は既知なので返す\n    if (i === 1 || i === 2) return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nfunction climbingStairsDFSMem(n: number): number {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.dart
/* メモ化探索 */\nint dfs(int i, List<int> mem) {\n  // dp[1] と dp[2] は既知なので返す\n  if (i == 1 || i == 2) return i;\n  // dp[i] の記録があれば、それをそのまま返す\n  if (mem[i] != -1) return mem[i];\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n  // dp[i] を記録する\n  mem[i] = count;\n  return count;\n}\n\n/* 階段登り:メモ化探索 */\nint climbingStairsDFSMem(int n) {\n  // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n  List<int> mem = List.filled(n + 1, -1);\n  return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.rs
/* メモ化探索 */\nfn dfs(i: usize, mem: &mut [i32]) -> i32 {\n    // dp[1] と dp[2] は既知なので返す\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // dp[i] の記録があれば、それをそのまま返す\n    if mem[i] != -1 {\n        return mem[i];\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    count\n}\n\n/* 階段登り:メモ化探索 */\nfn climbing_stairs_dfs_mem(n: usize) -> i32 {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    let mut mem = vec![-1; n + 1];\n    dfs(n, &mut mem)\n}\n
climbing_stairs_dfs_mem.c
/* メモ化探索 */\nint dfs(int i, int *mem) {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // dp[i] を記録する\n    mem[i] = count;\n    return count;\n}\n\n/* 階段登り:メモ化探索 */\nint climbingStairsDFSMem(int n) {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    int *mem = (int *)malloc((n + 1) * sizeof(int));\n    for (int i = 0; i <= n; i++) {\n        mem[i] = -1;\n    }\n    int result = dfs(n, mem);\n    free(mem);\n    return result;\n}\n
climbing_stairs_dfs_mem.kt
/* メモ化探索 */\nfun dfs(i: Int, mem: IntArray): Int {\n    // dp[1] と dp[2] は既知なので返す\n    if (i == 1 || i == 2) return i\n    // dp[i] の記録があれば、それをそのまま返す\n    if (mem[i] != -1) return mem[i]\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    // dp[i] を記録する\n    mem[i] = count\n    return count\n}\n\n/* 階段登り:メモ化探索 */\nfun climbingStairsDFSMem(n: Int): Int {\n    // mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n    val mem = IntArray(n + 1)\n    mem.fill(-1)\n    return dfs(n, mem)\n}\n
climbing_stairs_dfs_mem.rb
### メモ化探索 ###\ndef dfs(i, mem)\n  # dp[1] と dp[2] は既知なので返す\n  return i if i == 1 || i == 2\n  # dp[i] の記録があれば、それをそのまま返す\n  return mem[i] if mem[i] != -1\n\n  # dp[i] = dp[i-1] + dp[i-2]\n  count = dfs(i - 1, mem) + dfs(i - 2, mem)\n  # dp[i] を記録する\n  mem[i] = count\nend\n\n### 階段登り:メモ化探索 ###\ndef climbing_stairs_dfs_mem(n)\n  # mem[i] は第 i 段まで上る方法の総数を記録し、-1 は未記録を表す\n  mem = Array.new(n + 1, -1)\n  dfs(n, mem)\nend\n
コードの可視化

全画面で見る >

次の図を見ると、メモ化を行うことで、すべての重複部分問題は 1 回だけ計算すればよくなり、時間計算量は \\(O(n)\\) まで改善されます。これは大きな飛躍です。

図 14-4   メモ化探索に対応する再帰木

","path":["第 14 章   動的計画法","14.1   動的計画法入門"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1413-3","level":2,"title":"14.1.3   方法 3:動的計画法","text":"

**メモ化探索は「トップダウン」の方法**です。元の問題(根ノード)から始めて、より大きな部分問題を再帰的により小さな部分問題へ分解し、解が既知である最小部分問題(葉ノード)に至ります。その後、バックトラックしながら各層で部分問題の解を集め、元の問題の解を構築します。

これとは対照的に、**動的計画法は「ボトムアップ」の方法**です。最小部分問題の解から始めて、より大きな部分問題の解を反復的に構築し、最終的に元の問題の解を得ます。

動的計画法にはバックトラックの過程が含まれないため、再帰を使う必要はなく、ループによる反復だけで実装できます。次のコードでは、部分問題の解を保存する配列 dp を初期化しており、これはメモ化探索における配列 mem と同じ記録の役割を果たします:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
def climbing_stairs_dp(n: int) -> int:\n    \"\"\"階段登り:動的計画法\"\"\"\n    if n == 1 or n == 2:\n        return n\n    # 部分問題の解を保存するために dp テーブルを初期化\n    dp = [0] * (n + 1)\n    # 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1], dp[2] = 1, 2\n    # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in range(3, n + 1):\n        dp[i] = dp[i - 1] + dp[i - 2]\n    return dp[n]\n
climbing_stairs_dp.cpp
/* 階段登り:動的計画法 */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    vector<int> dp(n + 1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.java
/* 階段登り:動的計画法 */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[] dp = new int[n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.cs
/* 階段登り:動的計画法 */\nint ClimbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int[] dp = new int[n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.go
/* 階段登り:動的計画法 */\nfunc climbingStairsDP(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    dp := make([]int, n+1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1\n    dp[2] = 2\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i := 3; i <= n; i++ {\n        dp[i] = dp[i-1] + dp[i-2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.swift
/* 階段登り:動的計画法 */\nfunc climbingStairsDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    var dp = Array(repeating: 0, count: n + 1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1\n    dp[2] = 2\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3 ... n {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.js
/* 階段登り:動的計画法 */\nfunction climbingStairsDP(n) {\n    if (n === 1 || n === 2) return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = new Array(n + 1).fill(-1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.ts
/* 階段登り:動的計画法 */\nfunction climbingStairsDP(n: number): number {\n    if (n === 1 || n === 2) return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    const dp = new Array(n + 1).fill(-1);\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.dart
/* 階段登り:動的計画法 */\nint climbingStairsDP(int n) {\n  if (n == 1 || n == 2) return n;\n  // 部分問題の解を保存するために dp テーブルを初期化\n  List<int> dp = List.filled(n + 1, 0);\n  // 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1] = 1;\n  dp[2] = 2;\n  // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  for (int i = 3; i <= n; i++) {\n    dp[i] = dp[i - 1] + dp[i - 2];\n  }\n  return dp[n];\n}\n
climbing_stairs_dp.rs
/* 階段登り:動的計画法 */\nfn climbing_stairs_dp(n: usize) -> i32 {\n    // dp[1] と dp[2] は既知なので返す\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    // 部分問題の解を保存するために dp テーブルを初期化\n    let mut dp = vec![-1; n + 1];\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i in 3..=n {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    dp[n]\n}\n
climbing_stairs_dp.c
/* 階段登り:動的計画法 */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 部分問題の解を保存するために dp テーブルを初期化\n    int *dp = (int *)malloc((n + 1) * sizeof(int));\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    int result = dp[n];\n    free(dp);\n    return result;\n}\n
climbing_stairs_dp.kt
/* 階段登り:動的計画法 */\nfun climbingStairsDP(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    // 部分問題の解を保存するために dp テーブルを初期化\n    val dp = IntArray(n + 1)\n    // 初期状態:最小部分問題の解をあらかじめ設定\n    dp[1] = 1\n    dp[2] = 2\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for (i in 3..n) {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.rb
### 階段登り:動的計画法 ###\ndef climbing_stairs_dp(n)\n  return n  if n == 1 || n == 2\n\n  # 部分問題の解を保存するために dp テーブルを初期化\n  dp = Array.new(n + 1, 0)\n  # 初期状態:最小部分問題の解をあらかじめ設定\n  dp[1], dp[2] = 1, 2\n  # 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n  (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] }\n\n  dp[n]\nend\n
コードの可視化

全画面で見る >

次の図は、以上のコードの実行過程をシミュレートしたものです。

図 14-5   階段上りの動的計画法の過程

バックトラッキング法と同様に、動的計画法でも問題解決の特定段階を表すために「状態」という概念を用います。各状態は 1 つの部分問題と、それに対応する局所最適解に対応します。たとえば、階段を上る問題では、状態は現在いる階段の段数 \\(i\\) と定義されます。

以上を踏まえると、動的計画法のよく使われる用語を次のようにまとめられます。

  • 配列 dp を dp テーブル と呼び、\\(dp[i]\\) は状態 \\(i\\) に対応する部分問題の解を表します。
  • 最小部分問題に対応する状態(第 \\(1\\) 段目と第 \\(2\\) 段目の階段)を初期状態と呼びます。
  • 漸化式 \\(dp[i] = dp[i-1] + dp[i-2]\\) を状態遷移方程式と呼びます。
","path":["第 14 章   動的計画法","14.1   動的計画法入門"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1414","level":2,"title":"14.1.4   空間最適化","text":"

注意深い読者は気づいたかもしれません。\\(dp[i]\\) は \\(dp[i-1]\\) と \\(dp[i-2]\\) にしか依存しないため、すべての部分問題の解を保存するために配列 dp を使う必要はありません。2 つの変数を順に更新していくだけで十分です。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
def climbing_stairs_dp_comp(n: int) -> int:\n    \"\"\"階段登り:空間最適化した動的計画法\"\"\"\n    if n == 1 or n == 2:\n        return n\n    a, b = 1, 2\n    for _ in range(3, n + 1):\n        a, b = b, a + b\n    return b\n
climbing_stairs_dp.cpp
/* 階段登り:空間最適化した動的計画法 */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.java
/* 階段登り:空間最適化した動的計画法 */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.cs
/* 階段登り:空間最適化した動的計画法 */\nint ClimbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.go
/* 階段登り:空間最適化した動的計画法 */\nfunc climbingStairsDPComp(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    a, b := 1, 2\n    // 状態遷移:小さい部分問題から大きい部分問題へ順に解く\n    for i := 3; i <= n; i++ {\n        a, b = b, a+b\n    }\n    return b\n}\n
climbing_stairs_dp.swift
/* 階段登り:空間最適化した動的計画法 */\nfunc climbingStairsDPComp(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    var a = 1\n    var b = 2\n    for _ in 3 ... n {\n        (a, b) = (b, a + b)\n    }\n    return b\n}\n
climbing_stairs_dp.js
/* 階段登り:空間最適化した動的計画法 */\nfunction climbingStairsDPComp(n) {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.ts
/* 階段登り:空間最適化した動的計画法 */\nfunction climbingStairsDPComp(n: number): number {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.dart
/* 階段登り:空間最適化した動的計画法 */\nint climbingStairsDPComp(int n) {\n  if (n == 1 || n == 2) return n;\n  int a = 1, b = 2;\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = a + b;\n    a = tmp;\n  }\n  return b;\n}\n
climbing_stairs_dp.rs
/* 階段登り:空間最適化した動的計画法 */\nfn climbing_stairs_dp_comp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    let (mut a, mut b) = (1, 2);\n    for _ in 3..=n {\n        let tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    b\n}\n
climbing_stairs_dp.c
/* 階段登り:空間最適化した動的計画法 */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.kt
/* 階段登り:空間最適化した動的計画法 */\nfun climbingStairsDPComp(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    var a = 1\n    var b = 2\n    for (i in 3..n) {\n        val temp = b\n        b += a\n        a = temp\n    }\n    return b\n}\n
climbing_stairs_dp.rb
### 階段登り:空間最適化後の動的計画法 ###\ndef climbing_stairs_dp_comp(n)\n  return n if n == 1 || n == 2\n\n  a, b = 1, 2\n  (3...(n + 1)).each { a, b = b, a + b }\n\n  b\nend\n
コードの可視化

全画面で見る >

上のコードを見ると、配列 dp が占めていた領域を省けるため、空間計算量は \\(O(n)\\) から \\(O(1)\\) へと下がります。

動的計画法の問題では、現在の状態はしばしば直前の限られた個数の状態にしか関係しません。このような場合は、必要な状態だけを保持し、「次元削減」によってメモリ空間を節約できます。この空間最適化の技巧は「ローリング変数」または「ローリング配列」と呼ばれます。

","path":["第 14 章   動的計画法","14.1   動的計画法入門"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/","level":1,"title":"14.4   0-1 ナップサック問題","text":"

ナップサック問題は、動的計画法の入門として非常に適した問題であり、動的計画法で最もよく見られる問題形式の1つです。これには 0-1 ナップサック問題、完全ナップサック問題、多重ナップサック問題など、多くの派生があります。

本節では、まず最も一般的な 0-1 ナップサック問題を解いていきます。

Question

\\(n\\) 個の品物が与えられ、\\(i\\) 番目の品物の重さは \\(wgt[i-1]\\)、価値は \\(val[i-1]\\) であり、容量 \\(cap\\) のナップサックがあります。各品物は1回しか選べないとき、ナップサック容量の制約下で入れられる品物の最大価値を求めてください。

以下の図を見てみましょう。品物番号 \\(i\\) は \\(1\\) から始まり、配列のインデックスは \\(0\\) から始まるため、品物 \\(i\\) は重さ \\(wgt[i-1]\\)、価値 \\(val[i-1]\\) に対応します。

図 14-17   0-1 ナップサックのサンプルデータ

0-1 ナップサック問題は、\\(n\\) 回の意思決定からなる過程とみなせます。各品物について「入れない」「入れる」という2つの選択肢があるため、この問題は決定木モデルを満たします。

この問題の目的は「ナップサック容量の制約下で入れられる品物の最大価値」を求めることなので、動的計画法の問題である可能性が高いです。

ステップ1:各ラウンドの選択を考え、状態を定義して、\\(dp\\) テーブルを得る

各品物について、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。ここから状態を、現在の品物番号 \\(i\\) とナップサック容量 \\(c\\) として定義し、\\([i, c]\\) と表せます。

状態 \\([i, c]\\) に対応する部分問題は、先頭 \\(i\\) 個の品物を容量 \\(c\\) のナップサックに入れるときの最大価値 であり、これを \\(dp[i, c]\\) と記します。

求めるべきものは \\(dp[n, cap]\\) なので、サイズ \\((n+1) \\times (cap+1)\\) の2次元 \\(dp\\) テーブルが必要です。

ステップ2:最適部分構造を見つけ、状態遷移方程式を導く

品物 \\(i\\) に対する選択を行った後に残るのは、先頭 \\(i-1\\) 個の品物に対する部分問題であり、次の2つのケースに分けられます。

  • 品物 \\(i\\) を入れない :ナップサック容量は変わらず、状態は \\([i-1, c]\\) に変化します。
  • 品物 \\(i\\) を入れる :ナップサック容量は \\(wgt[i-1]\\) だけ減少し、価値は \\(val[i-1]\\) だけ増加して、状態は \\([i-1, c-wgt[i-1]]\\) に変化します。

以上の分析から、この問題の最適部分構造が分かります。すなわち、最大価値 \\(dp[i, c]\\) は、品物 \\(i\\) を入れない場合と入れる場合のうち、より価値の大きい方に等しい ということです。これにより、次の状態遷移方程式を導けます。

\\[ dp[i, c] = \\max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \\]

注意すべき点として、現在の品物の重さ \\(wgt[i - 1]\\) が残りのナップサック容量 \\(c\\) を超える場合は、入れない選択しかできません。

ステップ3:境界条件と状態遷移の順序を決める

品物がない場合、またはナップサック容量が \\(0\\) の場合、最大価値は \\(0\\) です。すなわち、先頭列 \\(dp[i, 0]\\) と先頭行 \\(dp[0, c]\\) はいずれも \\(0\\) になります。

現在の状態 \\([i, c]\\) は、上側の状態 \\([i-1, c]\\) と左上の状態 \\([i-1, c-wgt[i-1]]\\) から遷移してくるため、2重ループで \\(dp\\) テーブル全体を順方向に走査すれば十分です。

以上の分析に基づき、次に全探索、メモ化探索、動的計画法の順で実装していきます。

","path":["第 14 章   動的計画法","14.4   0-1 ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#1-1","level":3,"title":"1.   方法1:全探索","text":"

探索コードには次の要素が含まれます。

  • 再帰引数:状態 \\([i, c]\\) です。
  • 戻り値:部分問題の解 \\(dp[i, c]\\) です。
  • 終了条件:品物番号が範囲外である \\(i = 0\\)、またはナップサックの残り容量が \\(0\\) のとき、再帰を終了して価値 \\(0\\) を返します。
  • 枝刈り:現在の品物の重さがナップサックの残り容量を超える場合、入れない選択しかできません。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:\n    \"\"\"0-1 ナップサック:総当たり探索\"\"\"\n    # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 or c == 0:\n        return 0\n    # ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c:\n        return knapsack_dfs(wgt, val, i - 1, c)\n    # 品物 i を入れない場合と入れる場合の最大価値を計算する\n    no = knapsack_dfs(wgt, val, i - 1, c)\n    yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # 2つの案のうち価値が大きいほうを返す\n    return max(no, yes)\n
knapsack.cpp
/* 0-1 ナップサック:総当たり探索 */\nint knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return max(no, yes);\n}\n
knapsack.java
/* 0-1 ナップサック:総当たり探索 */\nint knapsackDFS(int[] wgt, int[] val, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return Math.max(no, yes);\n}\n
knapsack.cs
/* 0-1 ナップサック:総当たり探索 */\nint KnapsackDFS(int[] weight, int[] val, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (weight[i - 1] > c) {\n        return KnapsackDFS(weight, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = KnapsackDFS(weight, val, i - 1, c);\n    int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return Math.Max(no, yes);\n}\n
knapsack.go
/* 0-1 ナップサック:総当たり探索 */\nfunc knapsackDFS(wgt, val []int, i, c int) int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i-1] > c {\n        return knapsackDFS(wgt, val, i-1, c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    no := knapsackDFS(wgt, val, i-1, c)\n    yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1]\n    // 2つの案のうち価値が大きいほうを返す\n    return int(math.Max(float64(no), float64(yes)))\n}\n
knapsack.swift
/* 0-1 ナップサック:総当たり探索 */\nfunc knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c {\n        return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    let no = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    let yes = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // 2つの案のうち価値が大きいほうを返す\n    return max(no, yes)\n}\n
knapsack.js
/* 0-1 ナップサック:総当たり探索 */\nfunction knapsackDFS(wgt, val, i, c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return Math.max(no, yes);\n}\n
knapsack.ts
/* 0-1 ナップサック:総当たり探索 */\nfunction knapsackDFS(\n    wgt: Array<number>,\n    val: Array<number>,\n    i: number,\n    c: number\n): number {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return Math.max(no, yes);\n}\n
knapsack.dart
/* 0-1 ナップサック:総当たり探索 */\nint knapsackDFS(List<int> wgt, List<int> val, int i, int c) {\n  // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // ナップサック容量を超える場合は、入れない選択しかできない\n  if (wgt[i - 1] > c) {\n    return knapsackDFS(wgt, val, i - 1, c);\n  }\n  // 品物 i を入れない場合と入れる場合の最大価値を計算する\n  int no = knapsackDFS(wgt, val, i - 1, c);\n  int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // 2つの案のうち価値が大きいほうを返す\n  return max(no, yes);\n}\n
knapsack.rs
/* 0-1 ナップサック:総当たり探索 */\nfn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    let no = knapsack_dfs(wgt, val, i - 1, c);\n    let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    std::cmp::max(no, yes)\n}\n
knapsack.c
/* 0-1 ナップサック:総当たり探索 */\nint knapsackDFS(int wgt[], int val[], int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2つの案のうち価値が大きいほうを返す\n    return myMax(no, yes);\n}\n
knapsack.kt
/* 0-1 ナップサック:総当たり探索 */\nfun knapsackDFS(\n    wgt: IntArray,\n    _val: IntArray,\n    i: Int,\n    c: Int\n): Int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, _val, i - 1, c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    val no = knapsackDFS(wgt, _val, i - 1, c)\n    val yes = knapsackDFS(wgt, _val, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // 2つの案のうち価値が大きいほうを返す\n    return max(no, yes)\n}\n
knapsack.rb
### 0-1 ナップサック: 全探索 ###\ndef knapsack_dfs(wgt, val, i, c)\n  # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n  return 0 if i == 0 || c == 0\n  # ナップサック容量を超える場合は、入れない選択しかできない\n  return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c\n  # 品物 i を入れない場合と入れる場合の最大価値を計算する\n  no = knapsack_dfs(wgt, val, i - 1, c)\n  yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # 2つの案のうち価値が大きいほうを返す\n  [no, yes].max\nend\n
コードの可視化

全画面で見る >

以下の図のように、各品物ごとに「選ばない」「選ぶ」の2つの探索分岐が生じるため、時間計算量は \\(O(2^n)\\) です。

再帰木を観察すると、\\(dp[1, 10]\\) などの重複部分問題が存在することが分かります。品物数が多く、ナップサック容量が大きく、特に同じ重さの品物が多い場合には、重複部分問題の数は大幅に増加します。

図 14-18   0-1 ナップサック問題の全探索の再帰木

","path":["第 14 章   動的計画法","14.4   0-1 ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#2-2","level":3,"title":"2.   方法2:メモ化探索","text":"

重複部分問題が一度だけ計算されるようにするため、メモ配列 mem を用いて部分問題の解を記録します。ここで mem[i][c] は \\(dp[i, c]\\) に対応します。

メモ化を導入すると、時間計算量は部分問題の数に依存し、すなわち \\(O(n \\times cap)\\) になります。実装コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dfs_mem(\n    wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int\n) -> int:\n    \"\"\"0-1 ナップサック:メモ化探索\"\"\"\n    # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 or c == 0:\n        return 0\n    # 既に記録があればそのまま返す\n    if mem[i][c] != -1:\n        return mem[i][c]\n    # ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c:\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    # 品物 i を入れない場合と入れる場合の最大価値を計算する\n    no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n
knapsack.cpp
/* 0-1 ナップサック:メモ化探索 */\nint knapsackDFSMem(vector<int> &wgt, vector<int> &val, vector<vector<int>> &mem, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = max(no, yes);\n    return mem[i][c];\n}\n
knapsack.java
/* 0-1 ナップサック:メモ化探索 */\nint knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.cs
/* 0-1 ナップサック:メモ化探索 */\nint KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (weight[i - 1] > c) {\n        return KnapsackDFSMem(weight, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = KnapsackDFSMem(weight, val, mem, i - 1, c);\n    int yes = KnapsackDFSMem(weight, val, mem, i - 1, c - weight[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = Math.Max(no, yes);\n    return mem[i][c];\n}\n
knapsack.go
/* 0-1 ナップサック:メモ化探索 */\nfunc knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // 既に記録があればそのまま返す\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i-1] > c {\n        return knapsackDFSMem(wgt, val, mem, i-1, c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    no := knapsackDFSMem(wgt, val, mem, i-1, c)\n    yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1]\n    // 2つの案のうち価値が大きいほうを返す\n    mem[i][c] = int(math.Max(float64(no), float64(yes)))\n    return mem[i][c]\n}\n
knapsack.swift
/* 0-1 ナップサック:メモ化探索 */\nfunc knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // 既に記録があればそのまま返す\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c {\n        return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    let no = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    let yes = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
knapsack.js
/* 0-1 ナップサック:メモ化探索 */\nfunction knapsackDFSMem(wgt, val, mem, i, c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.ts
/* 0-1 ナップサック:メモ化探索 */\nfunction knapsackDFSMem(\n    wgt: Array<number>,\n    val: Array<number>,\n    mem: Array<Array<number>>,\n    i: number,\n    c: number\n): number {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.dart
/* 0-1 ナップサック:メモ化探索 */\nint knapsackDFSMem(\n  List<int> wgt,\n  List<int> val,\n  List<List<int>> mem,\n  int i,\n  int c,\n) {\n  // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // 既に記録があればそのまま返す\n  if (mem[i][c] != -1) {\n    return mem[i][c];\n  }\n  // ナップサック容量を超える場合は、入れない選択しかできない\n  if (wgt[i - 1] > c) {\n    return knapsackDFSMem(wgt, val, mem, i - 1, c);\n  }\n  // 品物 i を入れない場合と入れる場合の最大価値を計算する\n  int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n  int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // 2 つの案のうち価値が大きい方を記録して返す\n  mem[i][c] = max(no, yes);\n  return mem[i][c];\n}\n
knapsack.rs
/* 0-1 ナップサック:メモ化探索 */\nfn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec<Vec<i32>>, i: usize, c: usize) -> i32 {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if mem[i][c] != -1 {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = std::cmp::max(no, yes);\n    mem[i][c]\n}\n
knapsack.c
/* 0-1 ナップサック:メモ化探索 */\nint knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = myMax(no, yes);\n    return mem[i][c];\n}\n
knapsack.kt
/* 0-1 ナップサック:メモ化探索 */\nfun knapsackDFSMem(\n    wgt: IntArray,\n    _val: IntArray,\n    mem: Array<IntArray>,\n    i: Int,\n    c: Int\n): Int {\n    // すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // 既に記録があればそのまま返す\n    if (mem[i][c] != -1) {\n        return mem[i][c]\n    }\n    // ナップサック容量を超える場合は、入れない選択しかできない\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    }\n    // 品物 i を入れない場合と入れる場合の最大価値を計算する\n    val no = knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    val yes = knapsackDFSMem(wgt, _val, mem, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // 2 つの案のうち価値が大きい方を記録して返す\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
knapsack.rb
### 0-1 ナップサック: メモ化探索 ###\ndef knapsack_dfs_mem(wgt, val, mem, i, c)\n  # すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す\n  return 0 if i == 0 || c == 0\n  # 既に記録があればそのまま返す\n  return mem[i][c] if mem[i][c] != -1\n  # ナップサック容量を超える場合は、入れない選択しかできない\n  return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c\n  # 品物 i を入れない場合と入れる場合の最大価値を計算する\n  no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n  yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # 2 つの案のうち価値が大きい方を記録して返す\n  mem[i][c] = [no, yes].max\nend\n
コードの可視化

全画面で見る >

次の図は、メモ化探索で剪定された探索分岐を示しています。

図 14-19   0-1 ナップサック問題のメモ化探索の再帰木

","path":["第 14 章   動的計画法","14.4   0-1 ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#3-3","level":3,"title":"3.   方法3:動的計画法","text":"

動的計画法の本質は、状態遷移に従って \\(dp\\) テーブルを埋めていく過程です。コードは次のようになります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 ナップサック:動的計画法\"\"\"\n    n = len(wgt)\n    # dp テーブルを初期化\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # 状態遷移\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
knapsack.cpp
/* 0-1 ナップサック:動的計画法 */\nint knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // dp テーブルを初期化\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.java
/* 0-1 ナップサック:動的計画法 */\nint knapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // dp テーブルを初期化\n    int[][] dp = new int[n + 1][cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.cs
/* 0-1 ナップサック:動的計画法 */\nint KnapsackDP(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // dp テーブルを初期化\n    int[,] dp = new int[n + 1, cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (weight[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i, c] = Math.Max(dp[i - 1, c - weight[i - 1]] + val[i - 1], dp[i - 1, c]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
knapsack.go
/* 0-1 ナップサック:動的計画法 */\nfunc knapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // dp テーブルを初期化\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i-1][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.swift
/* 0-1 ナップサック:動的計画法 */\nfunc knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // 状態遷移\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.js
/* 0-1 ナップサック:動的計画法 */\nfunction knapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array(n + 1)\n        .fill(0)\n        .map(() => Array(cap + 1).fill(0));\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.ts
/* 0-1 ナップサック:動的計画法 */\nfunction knapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.dart
/* 0-1 ナップサック:動的計画法 */\nint knapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // dp テーブルを初期化\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // ナップサック容量を超えるなら品物 i は選ばない\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
knapsack.rs
/* 0-1 ナップサック:動的計画法 */\nfn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // dp テーブルを初期化\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // 状態遷移\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = std::cmp::max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1],\n                );\n            }\n        }\n    }\n    dp[n][cap]\n}\n
knapsack.c
/* 0-1 ナップサック:動的計画法 */\nint knapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // dp テーブルを初期化\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
knapsack.kt
/* 0-1 ナップサック:動的計画法 */\nfun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // dp テーブルを初期化\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // 状態遷移\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.rb
### 0-1 ナップサック: 動的計画法 ###\ndef knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # 状態遷移\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # ナップサック容量を超えるなら品物 i は選ばない\n        dp[i][c] = dp[i - 1][c]\n      else\n        # 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[i][c] = [dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
コードの可視化

全画面で見る >

以下の図のように、時間計算量と空間計算量はいずれも配列 dp のサイズによって決まり、\\(O(n \\times cap)\\) です。

<1><2><3><4><5><6><7><8><9><10><11><12><13><14>

図 14-20   0-1 ナップサック問題の動的計画法の過程

","path":["第 14 章   動的計画法","14.4   0-1 ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#4","level":3,"title":"4.   空間最適化","text":"

各状態は直前の行の状態にしか依存しないため、2つの配列をローテーションして用いることで、空間計算量を \\(O(n^2)\\) から \\(O(n)\\) に削減できます。

さらに考えると、1つの配列だけで空間最適化を実現できるでしょうか。観察すると、各状態は真上または左上のマスから遷移してきます。配列が1つしかないと仮定すると、\\(i\\) 行目の走査を開始した時点では、その配列にはまだ \\(i-1\\) 行目の状態が格納されています。

  • 順方向に走査すると、\\(dp[i, j]\\) に到達した時点で、左上にある \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\) の値がすでに上書きされている可能性があり、正しい状態遷移結果を得られません。
  • 逆方向に走査すれば、上書きの問題は発生せず、状態遷移を正しく行えます。

次の図は、単一配列のもとで \\(i = 1\\) 行目から \\(i = 2\\) 行目へ変換する過程を示しています。順方向走査と逆方向走査の違いを考えてみてください。

<1><2><3><4><5><6>

図 14-21   0-1 ナップサックの空間最適化後の動的計画法の過程

コード実装では、配列 dp の第1次元 \\(i\\) をそのまま削除し、内側のループを逆方向走査に変更するだけで済みます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 ナップサック:空間最適化後の動的計画法\"\"\"\n    n = len(wgt)\n    # dp テーブルを初期化\n    dp = [0] * (cap + 1)\n    # 状態遷移\n    for i in range(1, n + 1):\n        # 逆順に走査する\n        for c in range(cap, 0, -1):\n            if wgt[i - 1] > c:\n                # ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c]\n            else:\n                # 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
knapsack.cpp
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nint knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // dp テーブルを初期化\n    vector<int> dp(cap + 1, 0);\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.java
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nint knapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // dp テーブルを初期化\n    int[] dp = new int[cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.cs
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nint KnapsackDPComp(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // dp テーブルを初期化\n    int[] dp = new int[cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (int c = cap; c > 0; c--) {\n            if (weight[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.go
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfunc knapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // dp テーブルを初期化\n    dp := make([]int, cap+1)\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        // 逆順に走査する\n        for c := cap; c >= 1; c-- {\n            if wgt[i-1] <= c {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.swift
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfunc knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: 0, count: cap + 1)\n    // 状態遷移\n    for i in 1 ... n {\n        // 逆順に走査する\n        for c in (1 ... cap).reversed() {\n            if wgt[i - 1] <= c {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.js
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfunction knapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array(cap + 1).fill(0);\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.ts
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfunction knapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array(cap + 1).fill(0);\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.dart
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nint knapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // dp テーブルを初期化\n  List<int> dp = List.filled(cap + 1, 0);\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    // 逆順に走査する\n    for (int c = cap; c >= 1; c--) {\n      if (wgt[i - 1] <= c) {\n        // 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
knapsack.rs
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // dp テーブルを初期化\n    let mut dp = vec![0; cap + 1];\n    // 状態遷移\n    for i in 1..=n {\n        // 逆順に走査する\n        for c in (1..=cap).rev() {\n            if wgt[i - 1] <= c as i32 {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
knapsack.c
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nint knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // dp テーブルを初期化\n    int *dp = calloc(cap + 1, sizeof(int));\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        // 逆順に走査する\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
knapsack.kt
/* 0-1 ナップサック:空間最適化後の動的計画法 */\nfun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // dp テーブルを初期化\n    val dp = IntArray(cap + 1)\n    // 状態遷移\n    for (i in 1..n) {\n        // 逆順に走査する\n        for (c in cap downTo 1) {\n            if (wgt[i - 1] <= c) {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.rb
### 0-1 ナップサック: 空間最適化後の動的計画法 ###\ndef knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # dp テーブルを初期化\n  dp = Array.new(cap + 1, 0)\n  # 状態遷移\n  for i in 1...(n + 1)\n    # 逆順に走査する\n    for c in cap.downto(1)\n      if wgt[i - 1] > c\n        # ナップサック容量を超えるなら品物 i は選ばない\n        dp[c] = dp[c]\n      else\n        # 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.4   0-1 ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/summary/","level":1,"title":"14.7   まとめ","text":"","path":["第 14 章   動的計画法","14.7   まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • 動的計画法は問題を分解し、部分問題の解を保存することで重複計算を避け、計算効率を高めます。
  • 時間を考慮しなければ、すべての動的計画法の問題はバックトラッキング(総当たり探索)で解けますが、再帰木には大量の重複部分問題が存在するため、効率はきわめて低くなります。メモ化配列を導入すると、計算済みのすべての部分問題の解を保存でき、重複部分問題が 1 回だけ計算されることを保証できます。
  • メモ化探索はトップダウンの再帰的解法であり、それに対応する動的計画法はボトムアップの漸化式による解法で、ちょうど「表を埋める」ようなものです。現在の状態は一部の局所状態にのみ依存するため、\\(dp\\) 表の 1 次元を削減して空間計算量を下げることができます。
  • 部分問題への分解は汎用的なアルゴリズムの考え方であり、分割統治、動的計画法、バックトラッキングではそれぞれ異なる性質を持ちます。
  • 動的計画法の問題には 3 つの大きな特徴があります。重複部分問題、最適部分構造、無後効性です。
  • 元の問題の最適解が部分問題の最適解から構築できるなら、その問題は最適部分構造を持ちます。
  • 無後効性とは、ある状態の将来の発展がその状態のみに関係し、過去に経たすべての状態とは無関係であることを指します。多くの組合せ最適化問題は無後効性を持たず、動的計画法で高速に解くことはできません。

ナップサック問題

  • ナップサック問題は最も典型的な動的計画法の問題の 1 つであり、0-1 ナップサック、完全ナップサック、多重ナップサックなどの派生があります。
  • 0-1 ナップサックの状態は、容量 \\(c\\) のナップサックに対して、前 \\(i\\) 個の品物で得られる最大価値として定義されます。ナップサックに入れない場合と入れる場合の 2 つの判断から最適部分構造を得て、状態遷移方程式を構築できます。空間最適化では、各状態が真上と左上の状態に依存するため、左上の状態が上書きされるのを避けるために配列を逆順に走査する必要があります。
  • 完全ナップサック問題では各品物の選択数に制限がないため、品物を入れる場合の状態遷移は 0-1 ナップサック問題とは異なります。状態は真上と真左の状態に依存するので、空間最適化では順方向に走査するべきです。
  • コイン両替問題は完全ナップサック問題の変種です。「最大」価値を求める問題から「最小」の硬貨枚数を求める問題へ変わるため、状態遷移方程式の \\(\\max()\\) は \\(\\min()\\) に置き換える必要があります。また、ナップサック容量を「超えない」ことを目指すのではなく、目標金額を「ちょうど」作ることを目指すため、\\(amt + 1\\) を「目標金額を作れない」無効解の表現として用います。
  • コイン両替問題 II では、「最少硬貨枚数」を求める問題から「硬貨の組合せ数」を求める問題へ変わるため、状態遷移方程式も \\(\\min()\\) から総和演算子へ対応して変わります。

編集距離問題

  • 編集距離(Levenshtein 距離)は 2 つの文字列間の類似度を測るために用いられ、ある文字列を別の文字列へ変換するための最小編集回数として定義されます。編集操作には追加、削除、置換が含まれます。
  • 編集距離問題の状態は、\\(s\\) の前 \\(i\\) 文字を \\(t\\) の前 \\(j\\) 文字へ変更するのに必要な最小編集回数として定義されます。\\(s[i] \\ne t[j]\\) のときは、追加、削除、置換の 3 つの判断があり、それぞれに対応する残りの部分問題があります。これにより最適部分構造を見いだし、状態遷移方程式を構築できます。一方、\\(s[i] = t[j]\\) のときは現在の文字を編集する必要はありません。
  • 編集距離では、状態は真上、真左、左上の状態に依存します。そのため、空間最適化後は順方向でも逆方向でも正しく状態遷移できません。そこで、変数を 1 つ用いて左上の状態を一時保存し、完全ナップサック問題と等価な形へ変換することで、空間最適化後に順方向走査を行えるようにします。
","path":["第 14 章   動的計画法","14.7   まとめ"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   完全ナップサック問題","text":"

本節では、まずもう 1 つの代表的なナップサック問題である完全ナップサック問題を解き、その特殊例である硬貨交換問題について見ていきます。

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1451","level":2,"title":"14.5.1   完全ナップサック問題","text":"

Question

\\(n\\) 個の品物が与えられ、\\(i\\) 番目の品物の重さは \\(wgt[i-1]\\)、価値は \\(val[i-1]\\) であり、容量 \\(cap\\) のナップサックがあります。各品物は繰り返し選択できます。ナップサック容量の制約下で入れられる品物の最大価値を求めてください。例を以下の図に示します。

図 14-22   完全ナップサック問題のサンプルデータ

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1","level":3,"title":"1.   動的計画法の考え方","text":"

完全ナップサック問題は 0-1 ナップサック問題と非常によく似ています。違いは、品物の選択回数に制限がない点だけです。

  • 0-1 ナップサック問題では、各品物は 1 つしかないため、品物 \\(i\\) をナップサックに入れた後は先頭 \\(i-1\\) 個の品物からしか選べません。
  • 完全ナップサック問題では、各品物の数は無限であるため、品物 \\(i\\) をナップサックに入れた後も、引き続き先頭 \\(i\\) 個の品物から選べます。

完全ナップサック問題では、状態 \\([i, c]\\) の変化は 2 つの場合に分けられます。

  • 品物 \\(i\\) を入れない :0-1 ナップサック問題と同様に、\\([i-1, c]\\) へ遷移します。
  • 品物 \\(i\\) を入れる :0-1 ナップサック問題とは異なり、\\([i, c-wgt[i-1]]\\) へ遷移します。

したがって、状態遷移方程式は次のようになります。

\\[ dp[i, c] = \\max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \\]","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2","level":3,"title":"2.   コード実装","text":"

2 つの問題のコードを比較すると、状態遷移の中で 1 か所だけ \\(i-1\\) が \\(i\\) に変わり、それ以外は完全に同じです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"完全ナップサック問題:動的計画法\"\"\"\n    n = len(wgt)\n    # dp テーブルを初期化\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # 状態遷移\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
unbounded_knapsack.cpp
/* 完全ナップサック問題:動的計画法 */\nint unboundedKnapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // dp テーブルを初期化\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.java
/* 完全ナップサック問題:動的計画法 */\nint unboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // dp テーブルを初期化\n    int[][] dp = new int[n + 1][cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.cs
/* 完全ナップサック問題:動的計画法 */\nint UnboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // dp テーブルを初期化\n    int[,] dp = new int[n + 1, cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i, c] = Math.Max(dp[i - 1, c], dp[i, c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
unbounded_knapsack.go
/* 完全ナップサック問題:動的計画法 */\nfunc unboundedKnapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // dp テーブルを初期化\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.swift
/* 完全ナップサック問題:動的計画法 */\nfunc unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // 状態遷移\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.js
/* 完全ナップサック問題:動的計画法 */\nfunction unboundedKnapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.ts
/* 完全ナップサック問題:動的計画法 */\nfunction unboundedKnapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.dart
/* 完全ナップサック問題:動的計画法 */\nint unboundedKnapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // dp テーブルを初期化\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // ナップサック容量を超えるなら品物 i は選ばない\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
unbounded_knapsack.rs
/* 完全ナップサック問題:動的計画法 */\nfn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // dp テーブルを初期化\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // 状態遷移\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.c
/* 完全ナップサック問題:動的計画法 */\nint unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // dp テーブルを初期化\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = myMax(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
unbounded_knapsack.kt
/* 完全ナップサック問題:動的計画法 */\nfun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // dp テーブルを初期化\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // 状態遷移\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.rb
### 完全ナップサック:動的計画法 ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # 状態遷移\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # ナップサック容量を超えるなら品物 i は選ばない\n        dp[i][c] = dp[i - 1][c]\n      else\n        # 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3","level":3,"title":"3.   空間最適化","text":"

現在の状態は左側と上側の状態から遷移してくるため、空間最適化後は \\(dp\\) テーブルの各行を順方向に走査する必要があります。

この走査順序は 0-1 ナップサックとはちょうど逆です。両者の違いは次の図を用いて理解してください。

<1><2><3><4><5><6>

図 14-23   完全ナップサック問題における空間最適化後の動的計画法の過程

コード実装は比較的簡単で、配列 dp の第 1 次元を削除するだけです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"完全ナップサック問題:空間最適化後の動的計画法\"\"\"\n    n = len(wgt)\n    # dp テーブルを初期化\n    dp = [0] * (cap + 1)\n    # 状態遷移\n    for i in range(1, n + 1):\n        # 順方向に走査する\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c]\n            else:\n                # 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
unbounded_knapsack.cpp
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nint unboundedKnapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // dp テーブルを初期化\n    vector<int> dp(cap + 1, 0);\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.java
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nint unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // dp テーブルを初期化\n    int[] dp = new int[cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.cs
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nint UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // dp テーブルを初期化\n    int[] dp = new int[cap + 1];\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.go
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfunc unboundedKnapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // dp テーブルを初期化\n    dp := make([]int, cap+1)\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.swift
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfunc unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: 0, count: cap + 1)\n    // 状態遷移\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.js
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfunction unboundedKnapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.ts
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfunction unboundedKnapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.dart
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nint unboundedKnapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // dp テーブルを初期化\n  List<int> dp = List.filled(cap + 1, 0);\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // ナップサック容量を超えるなら品物 i は選ばない\n        dp[c] = dp[c];\n      } else {\n        // 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
unbounded_knapsack.rs
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // dp テーブルを初期化\n    let mut dp = vec![0; cap + 1];\n    // 状態遷移\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
unbounded_knapsack.c
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nint unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // dp テーブルを初期化\n    int *dp = calloc(cap + 1, sizeof(int));\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c];\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
unbounded_knapsack.kt
/* 完全ナップサック問題:空間最適化後の動的計画法 */\nfun unboundedKnapsackDPComp(\n    wgt: IntArray,\n    _val: IntArray,\n    cap: Int\n): Int {\n    val n = wgt.size\n    // dp テーブルを初期化\n    val dp = IntArray(cap + 1)\n    // 状態遷移\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // ナップサック容量を超えるなら品物 i は選ばない\n                dp[c] = dp[c]\n            } else {\n                // 品物 i を選ばない場合と選ぶ場合の大きい方\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.rb
### 完全ナップサック:動的計画法 ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # 状態遷移\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # ナップサック容量を超えるなら品物 i は選ばない\n        dp[i][c] = dp[i - 1][c]\n      else\n        # 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n\n# ## 完全ナップサック: 空間最適化後の動的計画法 ##3\ndef unbounded_knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # dp テーブルを初期化\n  dp = Array.new(cap + 1, 0)\n  # 状態遷移\n  for i in 1...(n + 1)\n    # 順方向に走査する\n    for c in 1...(cap + 1)\n      if wgt[i -1] > c\n        # ナップサック容量を超えるなら品物 i は選ばない\n        dp[c] = dp[c]\n      else\n        # 品物 i を選ばない場合と選ぶ場合の大きい方\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1452","level":2,"title":"14.5.2   硬貨交換問題","text":"

ナップサック問題は動的計画法の代表的な問題群であり、多くの派生問題があります。硬貨交換問題もその 1 つです。

Question

\\(n\\) 種類の硬貨が与えられ、\\(i\\) 番目の硬貨の額面は \\(coins[i - 1]\\) 、目標金額は \\(amt\\) です。各硬貨は繰り返し選択できます。目標金額を作るために必要な最小の硬貨枚数を求めてください。目標金額を作れない場合は \\(-1\\) を返します。例を以下の図に示します。

図 14-24   硬貨交換問題のサンプルデータ

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_1","level":3,"title":"1.   動的計画法の考え方","text":"

硬貨交換は完全ナップサック問題の特殊なケースとみなせます。両者には次の対応関係と相違点があります。

  • 2 つの問題は相互に変換でき、「品物」は「硬貨」、「品物の重さ」は「硬貨の額面」、「ナップサック容量」は「目標金額」に対応します。
  • 最適化の目標は逆であり、完全ナップサック問題は品物価値の最大化、硬貨交換問題は硬貨枚数の最小化を目指します。
  • 完全ナップサック問題はナップサック容量を「超えない」解を求めますが、硬貨交換は目標金額に「ちょうど」一致する解を求めます。

ステップ 1:各ラウンドの選択を考え、状態を定義して、\\(dp\\) テーブルを得る

状態 \\([i, a]\\) に対応する部分問題は、**先頭 \\(i\\) 種類の硬貨で金額 \\(a\\) を作るための最小硬貨枚数**であり、これを \\(dp[i, a]\\) と表します。

2 次元 \\(dp\\) テーブルのサイズは \\((n+1) \\times (amt+1)\\) です。

ステップ 2:最適部分構造を見つけ、状態遷移方程式を導く

本問の状態遷移方程式は、完全ナップサック問題と比べて次の 2 点が異なります。

  • 本問では最小値を求めるため、演算子 \\(\\max()\\) を \\(\\min()\\) に変更する必要があります。
  • 最適化の対象は品物価値ではなく硬貨枚数であるため、硬貨を選んだときは \\(+1\\) すれば十分です。
\\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

ステップ 3:境界条件と状態遷移順序を決める

目標金額が \\(0\\) のとき、それを作るための最小硬貨枚数は \\(0\\) です。つまり、先頭列のすべての \\(dp[i, 0]\\) は \\(0\\) になります。

硬貨が 1 枚もない場合、任意の \\(> 0\\) の目標金額を作ることはできません。これは無効解です。状態遷移方程式内の \\(\\min()\\) 関数が無効解を識別して除外できるように、それらを \\(+ \\infty\\) で表すことを考えます。すなわち、先頭行のすべての \\(dp[0, a]\\) を \\(+ \\infty\\) とします。

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_1","level":3,"title":"2.   コード実装","text":"

多くのプログラミング言語には \\(+ \\infty\\) を表す変数が用意されていないため、通常は整数型 int の最大値で代用します。しかし、その場合は大きな数のオーバーフローが起こり得ます。状態遷移方程式中の \\(+ 1\\) 操作で桁あふれが発生する可能性があるためです。

そのため、ここでは数値 \\(amt + 1\\) を無効解の表現として用います。金額 \\(amt\\) を作るための硬貨枚数は最大でも \\(amt\\) 枚だからです。最後に返す前に、\\(dp[n, amt]\\) が \\(amt + 1\\) に等しいかを判定し、等しければ \\(-1\\) を返して目標金額を作れないことを表します。コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
def coin_change_dp(coins: list[int], amt: int) -> int:\n    \"\"\"コイン両替:動的計画法\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # dp テーブルを初期化\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # 状態遷移:先頭行と先頭列\n    for a in range(1, amt + 1):\n        dp[0][a] = MAX\n    # 状態遷移: 残りの行と列\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n    return dp[n][amt] if dp[n][amt] != MAX else -1\n
coin_change.cpp
/* コイン両替:動的計画法 */\nint coinChangeDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // 状態遷移:先頭行と先頭列\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.java
/* コイン両替:動的計画法 */\nint coinChangeDP(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int[][] dp = new int[n + 1][amt + 1];\n    // 状態遷移:先頭行と先頭列\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.cs
/* コイン両替:動的計画法 */\nint CoinChangeDP(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int[,] dp = new int[n + 1, amt + 1];\n    // 状態遷移:先頭行と先頭列\n    for (int a = 1; a <= amt; a++) {\n        dp[0, a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i, a] = Math.Min(dp[i - 1, a], dp[i, a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n, amt] != MAX ? dp[n, amt] : -1;\n}\n
coin_change.go
/* コイン両替:動的計画法 */\nfunc coinChangeDP(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // dp テーブルを初期化\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // 状態遷移:先頭行と先頭列\n    for a := 1; a <= amt; a++ {\n        dp[0][a] = max\n    }\n    // 状態遷移: 残りの行と列\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = int(math.Min(float64(dp[i-1][a]), float64(dp[i][a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt]\n    }\n    return -1\n}\n
coin_change.swift
/* コイン両替:動的計画法 */\nfunc coinChangeDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // 状態遷移:先頭行と先頭列\n    for a in 1 ... amt {\n        dp[0][a] = MAX\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1\n}\n
coin_change.js
/* コイン両替:動的計画法 */\nfunction coinChangeDP(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 状態遷移:先頭行と先頭列\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
coin_change.ts
/* コイン両替:動的計画法 */\nfunction coinChangeDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 状態遷移:先頭行と先頭列\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
coin_change.dart
/* コイン両替:動的計画法 */\nint coinChangeDP(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // dp テーブルを初期化\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // 状態遷移:先頭行と先頭列\n  for (int a = 1; a <= amt; a++) {\n    dp[0][a] = MAX;\n  }\n  // 状態遷移: 残りの行と列\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 目標金額を超えるなら硬貨 i は選ばない\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n        dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.rs
/* コイン両替:動的計画法 */\nfn coin_change_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // dp テーブルを初期化\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // 状態遷移:先頭行と先頭列\n    for a in 1..=amt {\n        dp[0][a] = max;\n    }\n    // 状態遷移: 残りの行と列\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt] as i32;\n    } else {\n        -1\n    }\n}\n
coin_change.c
/* コイン両替:動的計画法 */\nint coinChangeDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // 状態遷移:先頭行と先頭列\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状態遷移: 残りの行と列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[n][amt] != MAX ? dp[n][amt] : -1;\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
coin_change.kt
/* コイン両替:動的計画法 */\nfun coinChangeDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // dp テーブルを初期化\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // 状態遷移:先頭行と先頭列\n    for (a in 1..amt) {\n        dp[0][a] = MAX\n    }\n    // 状態遷移: 残りの行と列\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[n][amt] != MAX) dp[n][amt] else -1\n}\n
coin_change.rb
### コイン両替:動的計画法 ###\ndef coin_change_dp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # 状態遷移:先頭行と先頭列\n  (1...(amt + 1)).each { |a| dp[0][a] = _MAX }\n  # 状態遷移: 残りの行と列\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 目標金額を超えるなら硬貨 i は選ばない\n        dp[i][a] = dp[i - 1][a]\n      else\n        # 硬貨 i を選ばない場合と選ぶ場合の小さい方\n        dp[i][a] = [dp[i - 1][a], dp[i][a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[n][amt] != _MAX ? dp[n][amt] : -1\nend\n
コードの可視化

全画面で見る >

次の図は硬貨交換の動的計画法の過程を示しており、完全ナップサック問題と非常によく似ています。

<1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

図 14-25   硬貨交換問題の動的計画法の過程

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_1","level":3,"title":"3.   空間最適化","text":"

硬貨交換の空間最適化の方法は、完全ナップサック問題と同じです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
def coin_change_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"コイン交換:空間最適化後の動的計画法\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # dp テーブルを初期化\n    dp = [MAX] * (amt + 1)\n    dp[0] = 0\n    # 状態遷移\n    for i in range(1, n + 1):\n        # 順方向に走査する\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            else:\n                # 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n    return dp[amt] if dp[amt] != MAX else -1\n
coin_change.cpp
/* コイン交換:空間最適化後の動的計画法 */\nint coinChangeDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    vector<int> dp(amt + 1, MAX);\n    dp[0] = 0;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.java
/* コイン交換:空間最適化後の動的計画法 */\nint coinChangeDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int[] dp = new int[amt + 1];\n    Arrays.fill(dp, MAX);\n    dp[0] = 0;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.cs
/* コイン交換:空間最適化後の動的計画法 */\nint CoinChangeDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int[] dp = new int[amt + 1];\n    Array.Fill(dp, MAX);\n    dp[0] = 0;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = Math.Min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.go
/* コイン両替:動的計画法 */\nfunc coinChangeDPComp(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // dp テーブルを初期化\n    dp := make([]int, amt+1)\n    for i := 1; i <= amt; i++ {\n        dp[i] = max\n    }\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        // 順方向に走査する\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = int(math.Min(float64(dp[a]), float64(dp[a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt]\n    }\n    return -1\n}\n
coin_change.swift
/* コイン交換:空間最適化後の動的計画法 */\nfunc coinChangeDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // dp テーブルを初期化\n    var dp = Array(repeating: MAX, count: amt + 1)\n    dp[0] = 0\n    // 状態遷移\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1\n}\n
coin_change.js
/* コイン交換:空間最適化後の動的計画法 */\nfunction coinChangeDPComp(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
coin_change.ts
/* コイン交換:空間最適化後の動的計画法 */\nfunction coinChangeDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
coin_change.dart
/* コイン交換:空間最適化後の動的計画法 */\nint coinChangeDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // dp テーブルを初期化\n  List<int> dp = List.filled(amt + 1, MAX);\n  dp[0] = 0;\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 目標金額を超えるなら硬貨 i は選ばない\n        dp[a] = dp[a];\n      } else {\n        // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n        dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.rs
/* コイン交換:空間最適化後の動的計画法 */\nfn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // dp テーブルを初期化\n    let mut dp = vec![0; amt + 1];\n    dp.fill(max);\n    dp[0] = 0;\n    // 状態遷移\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt] as i32;\n    } else {\n        -1\n    }\n}\n
coin_change.c
/* コイン交換:空間最適化後の動的計画法 */\nint coinChangeDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // dp テーブルを初期化\n    int *dp = malloc((amt + 1) * sizeof(int));\n    for (int j = 1; j <= amt; j++) {\n        dp[j] = MAX;\n    } \n    dp[0] = 0;\n\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[amt] != MAX ? dp[amt] : -1;\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
coin_change.kt
/* コイン交換:空間最適化後の動的計画法 */\nfun coinChangeDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // dp テーブルを初期化\n    val dp = IntArray(amt + 1)\n    dp.fill(MAX)\n    dp[0] = 0\n    // 状態遷移\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // 硬貨 i を選ばない場合と選ぶ場合の小さい方\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[amt] != MAX) dp[amt] else -1\n}\n
coin_change.rb
### コイン両替:空間最適化した動的計画法 ###\ndef coin_change_dp_comp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # dp テーブルを初期化\n  dp = Array.new(amt + 1, _MAX)\n  dp[0] = 0\n  # 状態遷移\n  for i in 1...(n + 1)\n    # 順方向に走査する\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 目標金額を超えるなら硬貨 i は選ばない\n        dp[a] = dp[a]\n      else\n        # 硬貨 i を選ばない場合と選ぶ場合の小さい方\n        dp[a] = [dp[a], dp[a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[amt] != _MAX ? dp[amt] : -1\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1453-ii","level":2,"title":"14.5.3   硬貨交換問題 II","text":"

Question

\\(n\\) 種類の硬貨が与えられ、\\(i\\) 番目の硬貨の額面は \\(coins[i - 1]\\) 、目標金額は \\(amt\\) です。各硬貨は繰り返し選択できるとして、**目標金額を作る硬貨の組合せ数**を求めてください。例を以下の図に示します。

図 14-26   硬貨交換問題 II のサンプルデータ

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_2","level":3,"title":"1.   動的計画法の考え方","text":"

前問と比べて、本問の目的は組合せ数を求めることです。そのため、部分問題は 先頭 \\(i\\) 種類の硬貨で金額 \\(a\\) を作れる組合せ数 になります。一方、\\(dp\\) テーブルは引き続きサイズ \\((n+1) \\times (amt + 1)\\) の 2 次元行列です。

現在の状態における組合せ数は、現在の硬貨を選ばない場合と選ぶ場合の 2 つの選択肢の組合せ数の和に等しくなります。状態遷移方程式は次のとおりです。

\\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\]

目標金額が \\(0\\) のときは、どの硬貨も選ばなくても目標金額を作れるため、先頭列のすべての \\(dp[i, 0]\\) を \\(1\\) に初期化します。硬貨がないときは、任意の \\(>0\\) の目標金額を作れないため、先頭行のすべての \\(dp[0, a]\\) は \\(0\\) になります。

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_2","level":3,"title":"2.   コード実装","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
def coin_change_ii_dp(coins: list[int], amt: int) -> int:\n    \"\"\"コイン両替 II:動的計画法\"\"\"\n    n = len(coins)\n    # dp テーブルを初期化\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # 先頭列を初期化する\n    for i in range(n + 1):\n        dp[i][0] = 1\n    # 状態遷移\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n    return dp[n][amt]\n
coin_change_ii.cpp
/* コイン両替 II:動的計画法 */\nint coinChangeIIDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // dp テーブルを初期化\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // 先頭列を初期化する\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.java
/* コイン両替 II:動的計画法 */\nint coinChangeIIDP(int[] coins, int amt) {\n    int n = coins.length;\n    // dp テーブルを初期化\n    int[][] dp = new int[n + 1][amt + 1];\n    // 先頭列を初期化する\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.cs
/* コイン両替 II:動的計画法 */\nint CoinChangeIIDP(int[] coins, int amt) {\n    int n = coins.Length;\n    // dp テーブルを初期化\n    int[,] dp = new int[n + 1, amt + 1];\n    // 先頭列を初期化する\n    for (int i = 0; i <= n; i++) {\n        dp[i, 0] = 1;\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i, a] = dp[i - 1, a] + dp[i, a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n, amt];\n}\n
coin_change_ii.go
/* コイン両替 II:動的計画法 */\nfunc coinChangeIIDP(coins []int, amt int) int {\n    n := len(coins)\n    // dp テーブルを初期化\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // 先頭列を初期化する\n    for i := 0; i <= n; i++ {\n        dp[i][0] = 1\n    }\n    // 状態遷移: 残りの行と列\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.swift
/* コイン両替 II:動的計画法 */\nfunc coinChangeIIDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // 先頭列を初期化する\n    for i in 0 ... n {\n        dp[i][0] = 1\n    }\n    // 状態遷移\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.js
/* コイン両替 II:動的計画法 */\nfunction coinChangeIIDP(coins, amt) {\n    const n = coins.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 先頭列を初期化する\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.ts
/* コイン両替 II:動的計画法 */\nfunction coinChangeIIDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 先頭列を初期化する\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.dart
/* コイン両替 II:動的計画法 */\nint coinChangeIIDP(List<int> coins, int amt) {\n  int n = coins.length;\n  // dp テーブルを初期化\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // 先頭列を初期化する\n  for (int i = 0; i <= n; i++) {\n    dp[i][0] = 1;\n  }\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 目標金額を超えるなら硬貨 i は選ばない\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // コイン i を選ばない場合と選ぶ場合の和\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[n][amt];\n}\n
coin_change_ii.rs
/* コイン両替 II:動的計画法 */\nfn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // dp テーブルを初期化\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // 先頭列を初期化する\n    for i in 0..=n {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[n][amt]\n}\n
coin_change_ii.c
/* コイン両替 II:動的計画法 */\nint coinChangeIIDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // dp テーブルを初期化\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // 先頭列を初期化する\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[n][amt];\n    // メモリを解放する\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
coin_change_ii.kt
/* コイン両替 II:動的計画法 */\nfun coinChangeIIDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // dp テーブルを初期化\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // 先頭列を初期化する\n    for (i in 0..n) {\n        dp[i][0] = 1\n    }\n    // 状態遷移\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.rb
### コイン両替 II:動的計画法 ###\ndef coin_change_ii_dp(coins, amt)\n  n = coins.length\n  # dp テーブルを初期化\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # 先頭列を初期化する\n  (0...(n + 1)).each { |i| dp[i][0] = 1 }\n  # 状態遷移\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 目標金額を超えるなら硬貨 i は選ばない\n        dp[i][a] = dp[i - 1][a]\n      else\n        # コイン i を選ばない場合と選ぶ場合の和\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n      end\n    end\n  end\n  dp[n][amt]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_2","level":3,"title":"3.   空間最適化","text":"

空間最適化の方法も同様で、硬貨の次元を削除するだけです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"コイン両替 II:空間最適化した動的計画法\"\"\"\n    n = len(coins)\n    # dp テーブルを初期化\n    dp = [0] * (amt + 1)\n    dp[0] = 1\n    # 状態遷移\n    for i in range(1, n + 1):\n        # 順方向に走査する\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            else:\n                # コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n    return dp[amt]\n
coin_change_ii.cpp
/* コイン両替 II:空間最適化した動的計画法 */\nint coinChangeIIDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // dp テーブルを初期化\n    vector<int> dp(amt + 1, 0);\n    dp[0] = 1;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.java
/* コイン両替 II:空間最適化した動的計画法 */\nint coinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    // dp テーブルを初期化\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.cs
/* コイン両替 II:空間最適化した動的計画法 */\nint CoinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    // dp テーブルを初期化\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.go
/* コイン両替 II:空間最適化した動的計画法 */\nfunc coinChangeIIDPComp(coins []int, amt int) int {\n    n := len(coins)\n    // dp テーブルを初期化\n    dp := make([]int, amt+1)\n    dp[0] = 1\n    // 状態遷移\n    for i := 1; i <= n; i++ {\n        // 順方向に走査する\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a-coins[i-1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.swift
/* コイン両替 II:空間最適化した動的計画法 */\nfunc coinChangeIIDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // dp テーブルを初期化\n    var dp = Array(repeating: 0, count: amt + 1)\n    dp[0] = 1\n    // 状態遷移\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.js
/* コイン両替 II:空間最適化した動的計画法 */\nfunction coinChangeIIDPComp(coins, amt) {\n    const n = coins.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.ts
/* コイン両替 II:空間最適化した動的計画法 */\nfunction coinChangeIIDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // dp テーブルを初期化\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // 状態遷移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.dart
/* コイン両替 II:空間最適化した動的計画法 */\nint coinChangeIIDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  // dp テーブルを初期化\n  List<int> dp = List.filled(amt + 1, 0);\n  dp[0] = 1;\n  // 状態遷移\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 目標金額を超えるなら硬貨 i は選ばない\n        dp[a] = dp[a];\n      } else {\n        // コイン i を選ばない場合と選ぶ場合の和\n        dp[a] = dp[a] + dp[a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[amt];\n}\n
coin_change_ii.rs
/* コイン両替 II:空間最適化した動的計画法 */\nfn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // dp テーブルを初期化\n    let mut dp = vec![0; amt + 1];\n    dp[0] = 1;\n    // 状態遷移\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[amt]\n}\n
coin_change_ii.c
/* コイン両替 II:空間最適化した動的計画法 */\nint coinChangeIIDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // dp テーブルを初期化\n    int *dp = calloc(amt + 1, sizeof(int));\n    dp[0] = 1;\n    // 状態遷移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a];\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[amt];\n    // メモリを解放する\n    free(dp);\n    return res;\n}\n
coin_change_ii.kt
/* コイン両替 II:空間最適化した動的計画法 */\nfun coinChangeIIDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // dp テーブルを初期化\n    val dp = IntArray(amt + 1)\n    dp[0] = 1\n    // 状態遷移\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 目標金額を超えるなら硬貨 i は選ばない\n                dp[a] = dp[a]\n            } else {\n                // コイン i を選ばない場合と選ぶ場合の和\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.rb
### コイン両替 II:空間最適化した動的計画法 ###\ndef coin_change_ii_dp_comp(coins, amt)\n  n = coins.length\n  # dp テーブルを初期化\n  dp = Array.new(amt + 1, 0)\n  dp[0] = 1\n  # 状態遷移\n  for i in 1...(n + 1)\n    # 順方向に走査する\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 目標金額を超えるなら硬貨 i は選ばない\n        dp[a] = dp[a]\n      else\n        # コイン i を選ばない場合と選ぶ場合の和\n        dp[a] = dp[a] + dp[a - coins[i - 1]]\n      end\n    end\n  end\n  dp[amt]\nend\n
コードの可視化

全画面で見る >

","path":["第 14 章   動的計画法","14.5   完全ナップサック問題"],"tags":[]},{"location":"chapter_graph/","level":1,"title":"第 9 章   グラフ","text":"

Abstract

人生の旅路において、私たちはそれぞれ一つひとつのノードのようなものであり、無数の見えない辺によって結ばれています。

出会いと別れのたびに、この巨大なネットワークグラフの中に固有の足跡が刻まれます。

","path":["第 9 章   グラフ"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"章の内容","text":"
  • 9.1   グラフ
  • 9.2   グラフの基本操作
  • 9.3   グラフの走査
  • 9.4   まとめ
","path":["第 9 章   グラフ"],"tags":[]},{"location":"chapter_graph/graph/","level":1,"title":"9.1   グラフ","text":"

グラフ(graph)は、頂点(vertex)と辺(edge)から構成される非線形データ構造です。グラフ \\(G\\) は、頂点集合 \\(V\\) と辺集合 \\(E\\) からなる集合として抽象的に表せます。以下の例は、5 個の頂点と 7 本の辺を含むグラフを示しています。

\\[ \\begin{aligned} V & = \\{ 1, 2, 3, 4, 5 \\} \\newline E & = \\{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \\} \\newline G & = \\{ V, E \\} \\newline \\end{aligned} \\]

頂点をノード、辺を各ノードをつなぐ参照(ポインタ)とみなせば、グラフは連結リストを拡張したデータ構造の一種と捉えられます。次の図に示すように、線形関係(連結リスト)や分治関係(木)と比べて、ネットワーク関係(グラフ)は自由度が高く、そのぶん複雑です。

図 9-1   連結リスト、木、グラフの関係

","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#911","level":2,"title":"9.1.1   グラフの一般的な種類と用語","text":"

辺が方向性を持つかどうかに応じて、無向グラフ(undirected graph)と有向グラフ(directed graph)に分けられます。次の図のとおりです。

  • 無向グラフでは、辺は 2 つの頂点間の「双方向」の接続関係を表します。例えば WeChat や QQ における「友だち関係」です。
  • 有向グラフでは、辺は方向性を持ち、すなわち \\(A \\rightarrow B\\) と \\(A \\leftarrow B\\) の 2 方向の辺は互いに独立です。例えば Weibo や Douyin における「フォロー」と「フォロワー」の関係です。

図 9-2   有向グラフと無向グラフ

すべての頂点が連結しているかどうかに応じて、連結グラフ(connected graph)と非連結グラフ(disconnected graph)に分けられます。次の図のとおりです。

  • 連結グラフでは、ある頂点から出発すると、ほかの任意の頂点に到達できます。
  • 非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません。

図 9-3   連結グラフと非連結グラフ

辺に「重み」の変数を追加すると、次の図に示すような重み付きグラフ(weighted graph)が得られます。例えば『Honor of Kings』のようなモバイルゲームでは、システムが共にプレイした時間に基づいてプレイヤー間の「親密度」を計算します。この親密度ネットワークは重み付きグラフで表せます。

図 9-4   重み付きグラフと重みなしグラフ

グラフというデータ構造には、次のような基本用語があります。

  • 隣接(adjacency):2 つの頂点の間に辺が存在するとき、この 2 つの頂点は「隣接している」といいます。上図では、頂点 1 に隣接する頂点は 2、3、5 です。
  • 経路(path):頂点 A から頂点 B までに通過する辺で構成された列を、A から B への「経路」と呼びます。上図では、辺の列 1-5-2-4 は頂点 1 から頂点 4 への 1 本の経路です。
  • 次数(degree):ある頂点が持つ辺の本数です。有向グラフでは、入次数(in-degree)はその頂点に向かう辺の本数を表し、出次数(out-degree)はその頂点から出る辺の本数を表します。
","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#912","level":2,"title":"9.1.2   グラフの表現","text":"

グラフの一般的な表現方法には「隣接行列」と「隣接リスト」があります。以下では無向グラフを例に説明します。

","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#1","level":3,"title":"1.   隣接行列","text":"

グラフの頂点数を \\(n\\) とすると、隣接行列(adjacency matrix)は \\(n \\times n\\) の行列を用いてグラフを表します。各行(列)は 1 つの頂点を表し、行列要素は辺を表します。\\(1\\) または \\(0\\) を用いて、2 つの頂点の間に辺があるかどうかを示します。

次の図のように、隣接行列を \\(M\\)、頂点リストを \\(V\\) とすると、行列要素 \\(M[i, j] = 1\\) は頂点 \\(V[i]\\) から頂点 \\(V[j]\\) への辺が存在することを表し、逆に \\(M[i, j] = 0\\) は 2 つの頂点の間に辺がないことを表します。

図 9-5   グラフの隣接行列による表現

隣接行列には次の特徴があります。

  • 単純グラフでは、頂点は自分自身とは接続できないため、このとき隣接行列の主対角線上の要素には意味がありません。
  • 無向グラフでは、2 方向の辺は等価であるため、このとき隣接行列は主対角線に関して対称です。
  • 隣接行列の要素を \\(1\\) と \\(0\\) から重みに置き換えると、重み付きグラフを表せます。

隣接行列でグラフを表す場合、行列要素に直接アクセスして辺を取得できるため、追加・削除・検索・更新の操作効率は高く、時間計算量はいずれも \\(O(1)\\) です。しかし、行列の空間計算量は \\(O(n^2)\\) であり、メモリ使用量は多くなります。

","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#2","level":3,"title":"2.   隣接リスト","text":"

隣接リスト(adjacency list)は、\\(n\\) 本の連結リストを使ってグラフを表します。連結リストのノードは頂点を表します。第 \\(i\\) 本の連結リストは頂点 \\(i\\) に対応し、その頂点に隣接するすべての頂点(その頂点と接続された頂点)を格納します。次の図は、隣接リストで保存したグラフの例です。

図 9-6   グラフの隣接リストによる表現

隣接リストは実際に存在する辺だけを格納し、辺の総数は通常 \\(n^2\\) よりはるかに小さいため、より省スペースです。しかし、隣接リストでは辺を見つけるために連結リストを走査する必要があるため、時間効率は隣接行列に及びません。

上図を見ると、隣接リストの構造はハッシュテーブルにおける「連鎖アドレス法」と非常によく似ているため、同様の方法で効率を最適化できます。例えば、連結リストが長い場合は AVL 木や赤黒木に変換して時間効率を \\(O(n)\\) から \\(O(\\log n)\\) に改善できます。さらに、連結リストをハッシュテーブルに変換すれば、時間計算量を \\(O(1)\\) まで下げられます。

","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph/#913","level":2,"title":"9.1.3   グラフの一般的な応用","text":"

次の表のように、多くの現実のシステムはグラフでモデル化でき、対応する問題もグラフ計算の問題に帰着できます。

表 9-1   現実世界でよく見られるグラフ

頂点 辺 グラフ計算問題 ソーシャルネットワーク ユーザー 友だち関係 潜在的な友だちの推薦 地下鉄路線 駅 駅間の接続性 最短経路の推薦 太陽系 天体 天体間の万有引力作用 惑星軌道の計算","path":["第 9 章   グラフ","9.1   グラフ"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   グラフの基本操作","text":"

グラフの基本操作は、「辺」に対する操作と「頂点」に対する操作に分けられます。「隣接行列」と「隣接リスト」の 2 つの表現方法では、実装方法が異なります。

","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#921","level":2,"title":"9.2.1   隣接行列に基づく実装","text":"

頂点数が \\(n\\) の無向グラフを与えると、各種操作の実装方法は次図のとおりです。

  • 辺の追加または削除:隣接行列で指定した辺を直接変更すればよく、\\(O(1)\\) 時間です。無向グラフであるため、2 方向の辺を同時に更新する必要があります。
  • 頂点の追加:隣接行列の末尾に 1 行 1 列を追加し、すべてを \\(0\\) で埋めればよく、\\(O(n)\\) 時間です。
  • 頂点の削除:隣接行列から 1 行 1 列を削除します。先頭行と先頭列を削除する場合が最悪で、\\((n-1)^2\\) 個の要素を「左上へ移動」させる必要があるため、\\(O(n^2)\\) 時間です。
  • 初期化:\\(n\\) 個の頂点を受け取り、長さ \\(n\\) の頂点リスト vertices を初期化するのに \\(O(n)\\) 時間、サイズ \\(n \\times n\\) の隣接行列 adjMat を初期化するのに \\(O(n^2)\\) 時間かかります。
隣接行列の初期化辺の追加辺の削除頂点の追加頂点の削除

図 9-7   隣接行列の初期化、辺の追加と削除、頂点の追加と削除

以下は、隣接行列でグラフを表した実装コードです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_matrix.py
class GraphAdjMat:\n    \"\"\"隣接行列に基づく無向グラフクラス\"\"\"\n\n    def __init__(self, vertices: list[int], edges: list[list[int]]):\n        \"\"\"コンストラクタ\"\"\"\n        # 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n        self.vertices: list[int] = []\n        # 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n        self.adj_mat: list[list[int]] = []\n        # 頂点を追加\n        for val in vertices:\n            self.add_vertex(val)\n        # 辺を追加\n        # 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for e in edges:\n            self.add_edge(e[0], e[1])\n\n    def size(self) -> int:\n        \"\"\"頂点数を取得\"\"\"\n        return len(self.vertices)\n\n    def add_vertex(self, val: int):\n        \"\"\"頂点を追加\"\"\"\n        n = self.size()\n        # 頂点リストに新しい頂点の値を追加\n        self.vertices.append(val)\n        # 隣接行列に 1 行追加\n        new_row = [0] * n\n        self.adj_mat.append(new_row)\n        # 隣接行列に 1 列追加\n        for row in self.adj_mat:\n            row.append(0)\n\n    def remove_vertex(self, index: int):\n        \"\"\"頂点を削除\"\"\"\n        if index >= self.size():\n            raise IndexError()\n        # 頂点リストから index の頂点を削除する\n        self.vertices.pop(index)\n        # 隣接行列で index 行を削除する\n        self.adj_mat.pop(index)\n        # 隣接行列で index 列を削除する\n        for row in self.adj_mat:\n            row.pop(index)\n\n    def add_edge(self, i: int, j: int):\n        \"\"\"辺を追加\"\"\"\n        # パラメータ i, j は vertices の要素インデックスに対応する\n        # 範囲外と同値の場合の処理\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        # 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        self.adj_mat[i][j] = 1\n        self.adj_mat[j][i] = 1\n\n    def remove_edge(self, i: int, j: int):\n        \"\"\"辺を削除\"\"\"\n        # パラメータ i, j は vertices の要素インデックスに対応する\n        # 範囲外と同値の場合の処理\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        self.adj_mat[i][j] = 0\n        self.adj_mat[j][i] = 0\n\n    def print(self):\n        \"\"\"隣接行列を出力\"\"\"\n        print(\"頂点リスト =\", self.vertices)\n        print(\"隣接行列 =\")\n        print_matrix(self.adj_mat)\n
graph_adjacency_matrix.cpp
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    vector<int> vertices;       // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    vector<vector<int>> adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n  public:\n    /* コンストラクタ */\n    GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {\n        // 頂点を追加\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for (const vector<int> &edge : edges) {\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    int size() const {\n        return vertices.size();\n    }\n\n    /* 頂点を追加 */\n    void addVertex(int val) {\n        int n = size();\n        // 頂点リストに新しい頂点の値を追加\n        vertices.push_back(val);\n        // 隣接行列に 1 行追加\n        adjMat.emplace_back(vector<int>(n, 0));\n        // 隣接行列に 1 列追加\n        for (vector<int> &row : adjMat) {\n            row.push_back(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    void removeVertex(int index) {\n        if (index >= size()) {\n            throw out_of_range(\"頂点が存在しません\");\n        }\n        // 頂点リストから index の頂点を削除する\n        vertices.erase(vertices.begin() + index);\n        // 隣接行列で index 行を削除する\n        adjMat.erase(adjMat.begin() + index);\n        // 隣接行列で index 列を削除する\n        for (vector<int> &row : adjMat) {\n            row.erase(row.begin() + index);\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    void addEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"頂点が存在しません\");\n        }\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    void removeEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"頂点が存在しません\");\n        }\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* 隣接行列を出力 */\n    void print() {\n        cout << \"頂点リスト = \";\n        printVector(vertices);\n        cout << \"隣接行列 =\" << endl;\n        printVectorMatrix(adjMat);\n    }\n};\n
graph_adjacency_matrix.java
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    List<Integer> vertices; // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    List<List<Integer>> adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = new ArrayList<>();\n        this.adjMat = new ArrayList<>();\n        // 頂点を追加\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for (int[] e : edges) {\n            addEdge(e[0], e[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    public int size() {\n        return vertices.size();\n    }\n\n    /* 頂点を追加 */\n    public void addVertex(int val) {\n        int n = size();\n        // 頂点リストに新しい頂点の値を追加\n        vertices.add(val);\n        // 隣接行列に 1 行追加\n        List<Integer> newRow = new ArrayList<>(n);\n        for (int j = 0; j < n; j++) {\n            newRow.add(0);\n        }\n        adjMat.add(newRow);\n        // 隣接行列に 1 列追加\n        for (List<Integer> row : adjMat) {\n            row.add(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    public void removeVertex(int index) {\n        if (index >= size())\n            throw new IndexOutOfBoundsException();\n        // 頂点リストから index の頂点を削除する\n        vertices.remove(index);\n        // 隣接行列で index 行を削除する\n        adjMat.remove(index);\n        // 隣接行列で index 列を削除する\n        for (List<Integer> row : adjMat) {\n            row.remove(index);\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    public void addEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        adjMat.get(i).set(j, 1);\n        adjMat.get(j).set(i, 1);\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    public void removeEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        adjMat.get(i).set(j, 0);\n        adjMat.get(j).set(i, 0);\n    }\n\n    /* 隣接行列を出力 */\n    public void print() {\n        System.out.print(\"頂点リスト = \");\n        System.out.println(vertices);\n        System.out.println(\"隣接行列 =\");\n        PrintUtil.printMatrix(adjMat);\n    }\n}\n
graph_adjacency_matrix.cs
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    List<int> vertices;     // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    List<List<int>> adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // 頂点を追加\n        foreach (int val in vertices) {\n            AddVertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        foreach (int[] e in edges) {\n            AddEdge(e[0], e[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    int Size() {\n        return vertices.Count;\n    }\n\n    /* 頂点を追加 */\n    public void AddVertex(int val) {\n        int n = Size();\n        // 頂点リストに新しい頂点の値を追加\n        vertices.Add(val);\n        // 隣接行列に 1 行追加\n        List<int> newRow = new(n);\n        for (int j = 0; j < n; j++) {\n            newRow.Add(0);\n        }\n        adjMat.Add(newRow);\n        // 隣接行列に 1 列追加\n        foreach (List<int> row in adjMat) {\n            row.Add(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    public void RemoveVertex(int index) {\n        if (index >= Size())\n            throw new IndexOutOfRangeException();\n        // 頂点リストから index の頂点を削除する\n        vertices.RemoveAt(index);\n        // 隣接行列で index 行を削除する\n        adjMat.RemoveAt(index);\n        // 隣接行列で index 列を削除する\n        foreach (List<int> row in adjMat) {\n            row.RemoveAt(index);\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    public void AddEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    public void RemoveEdge(int i, int j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* 隣接行列を出力 */\n    public void Print() {\n        Console.Write(\"頂点リスト = \");\n        PrintUtil.PrintList(vertices);\n        Console.WriteLine(\"隣接行列 =\");\n        PrintUtil.PrintMatrix(adjMat);\n    }\n}\n
graph_adjacency_matrix.go
/* 隣接行列に基づく無向グラフクラス */\ntype graphAdjMat struct {\n    // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    vertices []int\n    // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n    adjMat [][]int\n}\n\n/* コンストラクタ */\nfunc newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat {\n    // 頂点を追加\n    n := len(vertices)\n    adjMat := make([][]int, n)\n    for i := range adjMat {\n        adjMat[i] = make([]int, n)\n    }\n    // グラフを初期化する\n    g := &graphAdjMat{\n        vertices: vertices,\n        adjMat:   adjMat,\n    }\n    // 辺を追加\n    // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n    for i := range edges {\n        g.addEdge(edges[i][0], edges[i][1])\n    }\n    return g\n}\n\n/* 頂点数を取得 */\nfunc (g *graphAdjMat) size() int {\n    return len(g.vertices)\n}\n\n/* 頂点を追加 */\nfunc (g *graphAdjMat) addVertex(val int) {\n    n := g.size()\n    // 頂点リストに新しい頂点の値を追加\n    g.vertices = append(g.vertices, val)\n    // 隣接行列に 1 行追加\n    newRow := make([]int, n)\n    g.adjMat = append(g.adjMat, newRow)\n    // 隣接行列に 1 列追加\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i], 0)\n    }\n}\n\n/* 頂点を削除 */\nfunc (g *graphAdjMat) removeVertex(index int) {\n    if index >= g.size() {\n        return\n    }\n    // 頂点リストから index の頂点を削除する\n    g.vertices = append(g.vertices[:index], g.vertices[index+1:]...)\n    // 隣接行列で index 行を削除する\n    g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...)\n    // 隣接行列で index 列を削除する\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...)\n    }\n}\n\n/* 辺を追加 */\n// 引数 i, j は vertices の要素インデックスに対応する\nfunc (g *graphAdjMat) addEdge(i, j int) {\n    // インデックスの範囲外と等値の処理\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n    g.adjMat[i][j] = 1\n    g.adjMat[j][i] = 1\n}\n\n/* 辺を削除 */\n// 引数 i, j は vertices の要素インデックスに対応する\nfunc (g *graphAdjMat) removeEdge(i, j int) {\n    // インデックスの範囲外と等値の処理\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    g.adjMat[i][j] = 0\n    g.adjMat[j][i] = 0\n}\n\n/* 隣接行列を出力 */\nfunc (g *graphAdjMat) print() {\n    fmt.Printf(\"\\t頂点リスト = %v\\n\", g.vertices)\n    fmt.Printf(\"\\t隣接行列 = \\n\")\n    for i := range g.adjMat {\n        fmt.Printf(\"\\t\\t\\t%v\\n\", g.adjMat[i])\n    }\n}\n
graph_adjacency_matrix.swift
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    private var vertices: [Int] // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    private var adjMat: [[Int]] // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    init(vertices: [Int], edges: [[Int]]) {\n        self.vertices = []\n        adjMat = []\n        // 頂点を追加\n        for val in vertices {\n            addVertex(val: val)\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for e in edges {\n            addEdge(i: e[0], j: e[1])\n        }\n    }\n\n    /* 頂点数を取得 */\n    func size() -> Int {\n        vertices.count\n    }\n\n    /* 頂点を追加 */\n    func addVertex(val: Int) {\n        let n = size()\n        // 頂点リストに新しい頂点の値を追加\n        vertices.append(val)\n        // 隣接行列に 1 行追加\n        let newRow = Array(repeating: 0, count: n)\n        adjMat.append(newRow)\n        // 隣接行列に 1 列追加\n        for i in adjMat.indices {\n            adjMat[i].append(0)\n        }\n    }\n\n    /* 頂点を削除 */\n    func removeVertex(index: Int) {\n        if index >= size() {\n            fatalError(\"範囲外\")\n        }\n        // 頂点リストから index の頂点を削除する\n        vertices.remove(at: index)\n        // 隣接行列で index 行を削除する\n        adjMat.remove(at: index)\n        // 隣接行列で index 列を削除する\n        for i in adjMat.indices {\n            adjMat[i].remove(at: index)\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    func addEdge(i: Int, j: Int) {\n        // インデックスの範囲外と等値の処理\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"範囲外\")\n        }\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    func removeEdge(i: Int, j: Int) {\n        // インデックスの範囲外と等値の処理\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"範囲外\")\n        }\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* 隣接行列を出力 */\n    func print() {\n        Swift.print(\"頂点リスト = \", terminator: \"\")\n        Swift.print(vertices)\n        Swift.print(\"隣接行列 =\")\n        PrintUtil.printMatrix(matrix: adjMat)\n    }\n}\n
graph_adjacency_matrix.js
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    vertices; // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    adjMat; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    constructor(vertices, edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // 頂点を追加\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    size() {\n        return this.vertices.length;\n    }\n\n    /* 頂点を追加 */\n    addVertex(val) {\n        const n = this.size();\n        // 頂点リストに新しい頂点の値を追加\n        this.vertices.push(val);\n        // 隣接行列に 1 行追加\n        const newRow = [];\n        for (let j = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // 隣接行列に 1 列追加\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    removeVertex(index) {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 頂点リストから index の頂点を削除する\n        this.vertices.splice(index, 1);\n\n        // 隣接行列で index 行を削除する\n        this.adjMat.splice(index, 1);\n        // 隣接行列で index 列を削除する\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    addEdge(i, j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 無向グラフでは、隣接行列は主対角線に関して対称であり、(i, j) === (j, i) を満たす\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    removeEdge(i, j) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* 隣接行列を出力 */\n    print() {\n        console.log('頂点リスト = ', this.vertices);\n        console.log('隣接行列 =', this.adjMat);\n    }\n}\n
graph_adjacency_matrix.ts
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n    vertices: number[]; // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    adjMat: number[][]; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    constructor(vertices: number[], edges: number[][]) {\n        this.vertices = [];\n        this.adjMat = [];\n        // 頂点を追加\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    size(): number {\n        return this.vertices.length;\n    }\n\n    /* 頂点を追加 */\n    addVertex(val: number): void {\n        const n: number = this.size();\n        // 頂点リストに新しい頂点の値を追加\n        this.vertices.push(val);\n        // 隣接行列に 1 行追加\n        const newRow: number[] = [];\n        for (let j: number = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // 隣接行列に 1 列追加\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    removeVertex(index: number): void {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 頂点リストから index の頂点を削除する\n        this.vertices.splice(index, 1);\n\n        // 隣接行列で index 行を削除する\n        this.adjMat.splice(index, 1);\n        // 隣接行列で index 列を削除する\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    addEdge(i: number, j: number): void {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 無向グラフでは、隣接行列は主対角線に関して対称であり、(i, j) === (j, i) を満たす\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    removeEdge(i: number, j: number): void {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* 隣接行列を出力 */\n    print(): void {\n        console.log('頂点リスト = ', this.vertices);\n        console.log('隣接行列 =', this.adjMat);\n    }\n}\n
graph_adjacency_matrix.dart
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat {\n  List<int> vertices = []; // 頂点要素。要素は「頂点値」を表し、インデックスは「頂点インデックス」を表す\n  List<List<int>> adjMat = []; // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n  /* コンストラクタ */\n  GraphAdjMat(List<int> vertices, List<List<int>> edges) {\n    this.vertices = [];\n    this.adjMat = [];\n    // 頂点を追加\n    for (int val in vertices) {\n      addVertex(val);\n    }\n    // 辺を追加\n    // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n    for (List<int> e in edges) {\n      addEdge(e[0], e[1]);\n    }\n  }\n\n  /* 頂点数を取得 */\n  int size() {\n    return vertices.length;\n  }\n\n  /* 頂点を追加 */\n  void addVertex(int val) {\n    int n = size();\n    // 頂点リストに新しい頂点の値を追加\n    vertices.add(val);\n    // 隣接行列に 1 行追加\n    List<int> newRow = List.filled(n, 0, growable: true);\n    adjMat.add(newRow);\n    // 隣接行列に 1 列追加\n    for (List<int> row in adjMat) {\n      row.add(0);\n    }\n  }\n\n  /* 頂点を削除 */\n  void removeVertex(int index) {\n    if (index >= size()) {\n      throw IndexError;\n    }\n    // 頂点リストから index の頂点を削除する\n    vertices.removeAt(index);\n    // 隣接行列で index 行を削除する\n    adjMat.removeAt(index);\n    // 隣接行列で index 列を削除する\n    for (List<int> row in adjMat) {\n      row.removeAt(index);\n    }\n  }\n\n  /* 辺を追加 */\n  // 引数 i, j は vertices の要素インデックスに対応する\n  void addEdge(int i, int j) {\n    // インデックスの範囲外と等値の処理\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n    adjMat[i][j] = 1;\n    adjMat[j][i] = 1;\n  }\n\n  /* 辺を削除 */\n  // 引数 i, j は vertices の要素インデックスに対応する\n  void removeEdge(int i, int j) {\n    // インデックスの範囲外と等値の処理\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    adjMat[i][j] = 0;\n    adjMat[j][i] = 0;\n  }\n\n  /* 隣接行列を出力 */\n  void printAdjMat() {\n    print(\"頂点リスト = $vertices\");\n    print(\"隣接行列 = \");\n    printMatrix(adjMat);\n  }\n}\n
graph_adjacency_matrix.rs
/* 隣接行列に基づく無向グラフ型 */\npub struct GraphAdjMat {\n    // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    pub vertices: Vec<i32>,\n    // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n    pub adj_mat: Vec<Vec<i32>>,\n}\n\nimpl GraphAdjMat {\n    /* コンストラクタ */\n    pub fn new(vertices: Vec<i32>, edges: Vec<[usize; 2]>) -> Self {\n        let mut graph = GraphAdjMat {\n            vertices: vec![],\n            adj_mat: vec![],\n        };\n        // 頂点を追加\n        for val in vertices {\n            graph.add_vertex(val);\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for edge in edges {\n            graph.add_edge(edge[0], edge[1])\n        }\n\n        graph\n    }\n\n    /* 頂点数を取得 */\n    pub fn size(&self) -> usize {\n        self.vertices.len()\n    }\n\n    /* 頂点を追加 */\n    pub fn add_vertex(&mut self, val: i32) {\n        let n = self.size();\n        // 頂点リストに新しい頂点の値を追加\n        self.vertices.push(val);\n        // 隣接行列に 1 行追加\n        self.adj_mat.push(vec![0; n]);\n        // 隣接行列に 1 列追加\n        for row in self.adj_mat.iter_mut() {\n            row.push(0);\n        }\n    }\n\n    /* 頂点を削除 */\n    pub fn remove_vertex(&mut self, index: usize) {\n        if index >= self.size() {\n            panic!(\"index error\")\n        }\n        // 頂点リストから index の頂点を削除する\n        self.vertices.remove(index);\n        // 隣接行列で index 行を削除する\n        self.adj_mat.remove(index);\n        // 隣接行列で index 列を削除する\n        for row in self.adj_mat.iter_mut() {\n            row.remove(index);\n        }\n    }\n\n    /* 辺を追加 */\n    pub fn add_edge(&mut self, i: usize, j: usize) {\n        // パラメータ i, j は vertices の要素インデックスに対応する\n        // 範囲外と同値の場合の処理\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        self.adj_mat[i][j] = 1;\n        self.adj_mat[j][i] = 1;\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    pub fn remove_edge(&mut self, i: usize, j: usize) {\n        // パラメータ i, j は vertices の要素インデックスに対応する\n        // 範囲外と同値の場合の処理\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        self.adj_mat[i][j] = 0;\n        self.adj_mat[j][i] = 0;\n    }\n\n    /* 隣接行列を出力 */\n    pub fn print(&self) {\n        println!(\"頂点リスト = {:?}\", self.vertices);\n        println!(\"隣接行列 =\");\n        println!(\"[\");\n        for row in &self.adj_mat {\n            println!(\"  {:?},\", row);\n        }\n        println!(\"]\")\n    }\n}\n
graph_adjacency_matrix.c
/* 隣接行列に基づく無向グラフ構造体 */\ntypedef struct {\n    int vertices[MAX_SIZE];\n    int adjMat[MAX_SIZE][MAX_SIZE];\n    int size;\n} GraphAdjMat;\n\n/* コンストラクタ */\nGraphAdjMat *newGraphAdjMat() {\n    GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat));\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        for (int j = 0; j < MAX_SIZE; j++) {\n            graph->adjMat[i][j] = 0;\n        }\n    }\n    return graph;\n}\n\n/* デストラクタ */\nvoid delGraphAdjMat(GraphAdjMat *graph) {\n    free(graph);\n}\n\n/* 頂点を追加 */\nvoid addVertex(GraphAdjMat *graph, int val) {\n    if (graph->size == MAX_SIZE) {\n        fprintf(stderr, \"グラフの頂点数が最大値に達しました\\n\");\n        return;\n    }\n    // n 番目の頂点を追加し、n 行目と n 列目を 0 にする\n    int n = graph->size;\n    graph->vertices[n] = val;\n    for (int i = 0; i <= n; i++) {\n        graph->adjMat[n][i] = graph->adjMat[i][n] = 0;\n    }\n    graph->size++;\n}\n\n/* 頂点を削除 */\nvoid removeVertex(GraphAdjMat *graph, int index) {\n    if (index < 0 || index >= graph->size) {\n        fprintf(stderr, \"頂点インデックスが範囲外です\\n\");\n        return;\n    }\n    // 頂点リストから index の頂点を削除する\n    for (int i = index; i < graph->size - 1; i++) {\n        graph->vertices[i] = graph->vertices[i + 1];\n    }\n    // 隣接行列で index 行を削除する\n    for (int i = index; i < graph->size - 1; i++) {\n        for (int j = 0; j < graph->size; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i + 1][j];\n        }\n    }\n    // 隣接行列で index 列を削除する\n    for (int i = 0; i < graph->size; i++) {\n        for (int j = index; j < graph->size - 1; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i][j + 1];\n        }\n    }\n    graph->size--;\n}\n\n/* 辺を追加 */\n// 引数 i, j は vertices の要素インデックスに対応する\nvoid addEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"辺インデックスが範囲外であるか、同一です\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 1;\n    graph->adjMat[j][i] = 1;\n}\n\n/* 辺を削除 */\n// 引数 i, j は vertices の要素インデックスに対応する\nvoid removeEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"辺インデックスが範囲外であるか、同一です\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 0;\n    graph->adjMat[j][i] = 0;\n}\n\n/* 隣接行列を出力 */\nvoid printGraphAdjMat(GraphAdjMat *graph) {\n    printf(\"頂点リスト = \");\n    printArray(graph->vertices, graph->size);\n    printf(\"隣接行列 =\\n\");\n    for (int i = 0; i < graph->size; i++) {\n        printArray(graph->adjMat[i], graph->size);\n    }\n}\n
graph_adjacency_matrix.kt
/* 隣接行列に基づく無向グラフクラス */\nclass GraphAdjMat(vertices: IntArray, edges: Array<IntArray>) {\n    val vertices = mutableListOf<Int>() // 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    val adjMat = mutableListOf<MutableList<Int>>() // 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n\n    /* コンストラクタ */\n    init {\n        // 頂点を追加\n        for (vertex in vertices) {\n            addVertex(vertex)\n        }\n        // 辺を追加\n        // 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n        for (edge in edges) {\n            addEdge(edge[0], edge[1])\n        }\n    }\n\n    /* 頂点数を取得 */\n    fun size(): Int {\n        return vertices.size\n    }\n\n    /* 頂点を追加 */\n    fun addVertex(_val: Int) {\n        val n = size()\n        // 頂点リストに新しい頂点の値を追加\n        vertices.add(_val)\n        // 隣接行列に 1 行追加\n        val newRow = mutableListOf<Int>()\n        for (j in 0..<n) {\n            newRow.add(0)\n        }\n        adjMat.add(newRow)\n        // 隣接行列に 1 列追加\n        for (row in adjMat) {\n            row.add(0)\n        }\n    }\n\n    /* 頂点を削除 */\n    fun removeVertex(index: Int) {\n        if (index >= size())\n            throw IndexOutOfBoundsException()\n        // 頂点リストから index の頂点を削除する\n        vertices.removeAt(index)\n        // 隣接行列で index 行を削除する\n        adjMat.removeAt(index)\n        // 隣接行列で index 列を削除する\n        for (row in adjMat) {\n            row.removeAt(index)\n        }\n    }\n\n    /* 辺を追加 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    fun addEdge(i: Int, j: Int) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        // 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* 辺を削除 */\n    // 引数 i, j は vertices の要素インデックスに対応する\n    fun removeEdge(i: Int, j: Int) {\n        // インデックスの範囲外と等値の処理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* 隣接行列を出力 */\n    fun print() {\n        print(\"頂点リスト = \")\n        println(vertices)\n        println(\"隣接行列 =\")\n        printMatrix(adjMat)\n    }\n}\n
graph_adjacency_matrix.rb
### 隣接行列で実装した無向グラフクラス ###\nclass GraphAdjMat\n  def initialize(vertices, edges)\n    ### コンストラクタ ###\n    # 頂点リスト。要素は「頂点値」、インデックスは「頂点インデックス」を表す\n    @vertices = []\n    # 隣接行列。行・列のインデックスは「頂点インデックス」に対応\n    @adj_mat = []\n    # 頂点を追加\n    vertices.each { |val| add_vertex(val) }\n    # 辺を追加\n    # 注意:edges の各要素は頂点インデックスを表し、vertices の要素インデックスに対応する\n    edges.each { |e| add_edge(e[0], e[1]) }\n  end\n\n  ### 頂点数を取得 ###\n  def size\n    @vertices.length\n  end\n\n  ### 頂点を追加 ###\n  def add_vertex(val)\n    n = size\n    # 頂点リストに新しい頂点の値を追加\n    @vertices << val\n    # 隣接行列に 1 行追加\n    new_row = Array.new(n, 0)\n    @adj_mat << new_row\n    # 隣接行列に 1 列追加\n    @adj_mat.each { |row| row << 0 }\n  end\n\n  ### 頂点を削除 ###\n  def remove_vertex(index)\n    raise IndexError if index >= size\n\n    # 頂点リストから index の頂点を削除する\n    @vertices.delete_at(index)\n    # 隣接行列で index 行を削除する\n    @adj_mat.delete_at(index)\n    # 隣接行列で index 列を削除する\n    @adj_mat.each { |row| row.delete_at(index) }\n  end\n\n  ### 辺を追加 ###\n  def add_edge(i, j)\n    # パラメータ i, j は vertices の要素インデックスに対応する\n    # 範囲外と同値の場合の処理\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    # 無向グラフでは、隣接行列は主対角線に関して対称、すなわち (i, j) == (j, i) を満たす\n    @adj_mat[i][j] = 1\n    @adj_mat[j][i] = 1\n  end\n\n  ### 辺を削除 ###\n  def remove_edge(i, j)\n    # パラメータ i, j は vertices の要素インデックスに対応する\n    # 範囲外と同値の場合の処理\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    @adj_mat[i][j] = 0\n    @adj_mat[j][i] = 0\n  end\n\n  ### 隣接行列を出力 ###\n  def __print__\n    puts \"頂点リスト = #{@vertices}\"\n    puts '隣接行列 ='\n    print_matrix(@adj_mat)\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#922","level":2,"title":"9.2.2   隣接リストに基づく実装","text":"

無向グラフの頂点総数を \\(n\\)、辺総数を \\(m\\) とすると、各種操作は次図の方法で実装できます。

  • 辺の追加:頂点に対応する連結リストの末尾に辺を追加すればよく、\\(O(1)\\) 時間です。無向グラフなので、2 方向の辺を同時に追加する必要があります。
  • 辺の削除:頂点に対応する連結リストから指定した辺を探して削除するため、\\(O(m)\\) 時間です。無向グラフでは、2 方向の辺を同時に削除する必要があります。
  • 頂点の追加:隣接リストに 1 つの連結リストを追加し、新しい頂点をその連結リストの先頭ノードとするため、\\(O(1)\\) 時間です。
  • 頂点の削除:隣接リスト全体を走査し、指定した頂点を含むすべての辺を削除する必要があるため、\\(O(n + m)\\) 時間です。
  • 初期化:隣接リストに \\(n\\) 個の頂点と \\(2m\\) 本の辺を作成するため、\\(O(n + m)\\) 時間です。
隣接リストの初期化辺の追加辺の削除頂点の追加頂点の削除

図 9-8   隣接リストの初期化、辺の追加と削除、頂点の追加と削除

以下は隣接リストのコード実装です。上図と比べると、実際のコードには次の違いがあります。

  • 頂点の追加と削除を容易にし、コードを簡潔にするため、連結リストの代わりにリスト(動的配列)を使用しています。
  • ハッシュテーブルを用いて隣接リストを格納しており、key は頂点インスタンス、value はその頂点に隣接する頂点のリスト(連結リスト)です。

また、隣接リストでは頂点を表すために Vertex クラスを使用しています。その理由は、もし隣接行列と同様にリストのインデックスで異なる頂点を区別すると、インデックス \\(i\\) の頂点を削除する場合、隣接リスト全体を走査して、\\(i\\) より大きいすべてのインデックスを \\(1\\) 減らす必要があり、効率が非常に低いためです。これに対して、各頂点が一意な Vertex インスタンスであれば、ある頂点を削除しても他の頂点を変更する必要はありません。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_list.py
class GraphAdjList:\n    \"\"\"隣接リストに基づく無向グラフクラス\"\"\"\n\n    def __init__(self, edges: list[list[Vertex]]):\n        \"\"\"コンストラクタ\"\"\"\n        # 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n        self.adj_list = dict[Vertex, list[Vertex]]()\n        # すべての頂点と辺を追加\n        for edge in edges:\n            self.add_vertex(edge[0])\n            self.add_vertex(edge[1])\n            self.add_edge(edge[0], edge[1])\n\n    def size(self) -> int:\n        \"\"\"頂点数を取得\"\"\"\n        return len(self.adj_list)\n\n    def add_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"辺を追加\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # 辺 vet1 - vet2 を追加\n        self.adj_list[vet1].append(vet2)\n        self.adj_list[vet2].append(vet1)\n\n    def remove_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"辺を削除\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # 辺 vet1 - vet2 を削除\n        self.adj_list[vet1].remove(vet2)\n        self.adj_list[vet2].remove(vet1)\n\n    def add_vertex(self, vet: Vertex):\n        \"\"\"頂点を追加\"\"\"\n        if vet in self.adj_list:\n            return\n        # 隣接リストに新しいリストを追加\n        self.adj_list[vet] = []\n\n    def remove_vertex(self, vet: Vertex):\n        \"\"\"頂点を削除\"\"\"\n        if vet not in self.adj_list:\n            raise ValueError()\n        # 隣接リストから頂点 vet に対応するリストを削除\n        self.adj_list.pop(vet)\n        # 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for vertex in self.adj_list:\n            if vet in self.adj_list[vertex]:\n                self.adj_list[vertex].remove(vet)\n\n    def print(self):\n        \"\"\"隣接リストを出力\"\"\"\n        print(\"隣接リスト =\")\n        for vertex in self.adj_list:\n            tmp = [v.val for v in self.adj_list[vertex]]\n            print(f\"{vertex.val}: {tmp},\")\n
graph_adjacency_list.cpp
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n  public:\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    unordered_map<Vertex *, vector<Vertex *>> adjList;\n\n    /* vector 内の指定ノードを削除 */\n    void remove(vector<Vertex *> &vec, Vertex *vet) {\n        for (int i = 0; i < vec.size(); i++) {\n            if (vec[i] == vet) {\n                vec.erase(vec.begin() + i);\n                break;\n            }\n        }\n    }\n\n    /* コンストラクタ */\n    GraphAdjList(const vector<vector<Vertex *>> &edges) {\n        // すべての頂点と辺を追加\n        for (const vector<Vertex *> &edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    int size() {\n        return adjList.size();\n    }\n\n    /* 辺を追加 */\n    void addEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"頂点が存在しません\");\n        // 辺 vet1 - vet2 を追加\n        adjList[vet1].push_back(vet2);\n        adjList[vet2].push_back(vet1);\n    }\n\n    /* 辺を削除 */\n    void removeEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"頂点が存在しません\");\n        // 辺 vet1 - vet2 を削除\n        remove(adjList[vet1], vet2);\n        remove(adjList[vet2], vet1);\n    }\n\n    /* 頂点を追加 */\n    void addVertex(Vertex *vet) {\n        if (adjList.count(vet))\n            return;\n        // 隣接リストに新しいリストを追加\n        adjList[vet] = vector<Vertex *>();\n    }\n\n    /* 頂点を削除 */\n    void removeVertex(Vertex *vet) {\n        if (!adjList.count(vet))\n            throw invalid_argument(\"頂点が存在しません\");\n        // 隣接リストから頂点 vet に対応するリストを削除\n        adjList.erase(vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for (auto &adj : adjList) {\n            remove(adj.second, vet);\n        }\n    }\n\n    /* 隣接リストを出力 */\n    void print() {\n        cout << \"隣接リスト =\" << endl;\n        for (auto &adj : adjList) {\n            const auto &key = adj.first;\n            const auto &vec = adj.second;\n            cout << key->val << \": \";\n            printVector(vetsToVals(vec));\n        }\n    }\n};\n
graph_adjacency_list.java
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    Map<Vertex, List<Vertex>> adjList;\n\n    /* コンストラクタ */\n    public GraphAdjList(Vertex[][] edges) {\n        this.adjList = new HashMap<>();\n        // すべての頂点と辺を追加\n        for (Vertex[] edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    public int size() {\n        return adjList.size();\n    }\n\n    /* 辺を追加 */\n    public void addEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // 辺 vet1 - vet2 を追加\n        adjList.get(vet1).add(vet2);\n        adjList.get(vet2).add(vet1);\n    }\n\n    /* 辺を削除 */\n    public void removeEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // 辺 vet1 - vet2 を削除\n        adjList.get(vet1).remove(vet2);\n        adjList.get(vet2).remove(vet1);\n    }\n\n    /* 頂点を追加 */\n    public void addVertex(Vertex vet) {\n        if (adjList.containsKey(vet))\n            return;\n        // 隣接リストに新しいリストを追加\n        adjList.put(vet, new ArrayList<>());\n    }\n\n    /* 頂点を削除 */\n    public void removeVertex(Vertex vet) {\n        if (!adjList.containsKey(vet))\n            throw new IllegalArgumentException();\n        // 隣接リストから頂点 vet に対応するリストを削除\n        adjList.remove(vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for (List<Vertex> list : adjList.values()) {\n            list.remove(vet);\n        }\n    }\n\n    /* 隣接リストを出力 */\n    public void print() {\n        System.out.println(\"隣接リスト =\");\n        for (Map.Entry<Vertex, List<Vertex>> pair : adjList.entrySet()) {\n            List<Integer> tmp = new ArrayList<>();\n            for (Vertex vertex : pair.getValue())\n                tmp.add(vertex.val);\n            System.out.println(pair.getKey().val + \": \" + tmp + \",\");\n        }\n    }\n}\n
graph_adjacency_list.cs
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    public Dictionary<Vertex, List<Vertex>> adjList;\n\n    /* コンストラクタ */\n    public GraphAdjList(Vertex[][] edges) {\n        adjList = [];\n        // すべての頂点と辺を追加\n        foreach (Vertex[] edge in edges) {\n            AddVertex(edge[0]);\n            AddVertex(edge[1]);\n            AddEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    int Size() {\n        return adjList.Count;\n    }\n\n    /* 辺を追加 */\n    public void AddEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // 辺 vet1 - vet2 を追加\n        adjList[vet1].Add(vet2);\n        adjList[vet2].Add(vet1);\n    }\n\n    /* 辺を削除 */\n    public void RemoveEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // 辺 vet1 - vet2 を削除\n        adjList[vet1].Remove(vet2);\n        adjList[vet2].Remove(vet1);\n    }\n\n    /* 頂点を追加 */\n    public void AddVertex(Vertex vet) {\n        if (adjList.ContainsKey(vet))\n            return;\n        // 隣接リストに新しいリストを追加\n        adjList.Add(vet, []);\n    }\n\n    /* 頂点を削除 */\n    public void RemoveVertex(Vertex vet) {\n        if (!adjList.ContainsKey(vet))\n            throw new InvalidOperationException();\n        // 隣接リストから頂点 vet に対応するリストを削除\n        adjList.Remove(vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        foreach (List<Vertex> list in adjList.Values) {\n            list.Remove(vet);\n        }\n    }\n\n    /* 隣接リストを出力 */\n    public void Print() {\n        Console.WriteLine(\"隣接リスト =\");\n        foreach (KeyValuePair<Vertex, List<Vertex>> pair in adjList) {\n            List<int> tmp = [];\n            foreach (Vertex vertex in pair.Value)\n                tmp.Add(vertex.val);\n            Console.WriteLine(pair.Key.val + \": [\" + string.Join(\", \", tmp) + \"],\");\n        }\n    }\n}\n
graph_adjacency_list.go
/* 隣接リストに基づく無向グラフクラス */\ntype graphAdjList struct {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    adjList map[Vertex][]Vertex\n}\n\n/* コンストラクタ */\nfunc newGraphAdjList(edges [][]Vertex) *graphAdjList {\n    g := &graphAdjList{\n        adjList: make(map[Vertex][]Vertex),\n    }\n    // すべての頂点と辺を追加\n    for _, edge := range edges {\n        g.addVertex(edge[0])\n        g.addVertex(edge[1])\n        g.addEdge(edge[0], edge[1])\n    }\n    return g\n}\n\n/* 頂点数を取得 */\nfunc (g *graphAdjList) size() int {\n    return len(g.adjList)\n}\n\n/* 辺を追加 */\nfunc (g *graphAdjList) addEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // 辺 `vet1 - vet2` を追加し、無名 `struct{}` を追加する\n    g.adjList[vet1] = append(g.adjList[vet1], vet2)\n    g.adjList[vet2] = append(g.adjList[vet2], vet1)\n}\n\n/* 辺を削除 */\nfunc (g *graphAdjList) removeEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // 辺 vet1 - vet2 を削除\n    g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2)\n    g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1)\n}\n\n/* 頂点を追加 */\nfunc (g *graphAdjList) addVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if ok {\n        return\n    }\n    // 隣接リストに新しいリストを追加\n    g.adjList[vet] = make([]Vertex, 0)\n}\n\n/* 頂点を削除 */\nfunc (g *graphAdjList) removeVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if !ok {\n        panic(\"error\")\n    }\n    // 隣接リストから頂点 vet に対応するリストを削除\n    delete(g.adjList, vet)\n    // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n    for v, list := range g.adjList {\n        g.adjList[v] = DeleteSliceElms(list, vet)\n    }\n}\n\n/* 隣接リストを出力 */\nfunc (g *graphAdjList) print() {\n    var builder strings.Builder\n    fmt.Printf(\"隣接リスト = \\n\")\n    for k, v := range g.adjList {\n        builder.WriteString(\"\\t\\t\" + strconv.Itoa(k.Val) + \": \")\n        for _, vet := range v {\n            builder.WriteString(strconv.Itoa(vet.Val) + \" \")\n        }\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
graph_adjacency_list.swift
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    public private(set) var adjList: [Vertex: [Vertex]]\n\n    /* コンストラクタ */\n    public init(edges: [[Vertex]]) {\n        adjList = [:]\n        // すべての頂点と辺を追加\n        for edge in edges {\n            addVertex(vet: edge[0])\n            addVertex(vet: edge[1])\n            addEdge(vet1: edge[0], vet2: edge[1])\n        }\n    }\n\n    /* 頂点数を取得 */\n    public func size() -> Int {\n        adjList.count\n    }\n\n    /* 辺を追加 */\n    public func addEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"引数エラー\")\n        }\n        // 辺 vet1 - vet2 を追加\n        adjList[vet1]?.append(vet2)\n        adjList[vet2]?.append(vet1)\n    }\n\n    /* 辺を削除 */\n    public func removeEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"引数エラー\")\n        }\n        // 辺 vet1 - vet2 を削除\n        adjList[vet1]?.removeAll { $0 == vet2 }\n        adjList[vet2]?.removeAll { $0 == vet1 }\n    }\n\n    /* 頂点を追加 */\n    public func addVertex(vet: Vertex) {\n        if adjList[vet] != nil {\n            return\n        }\n        // 隣接リストに新しいリストを追加\n        adjList[vet] = []\n    }\n\n    /* 頂点を削除 */\n    public func removeVertex(vet: Vertex) {\n        if adjList[vet] == nil {\n            fatalError(\"引数エラー\")\n        }\n        // 隣接リストから頂点 vet に対応するリストを削除\n        adjList.removeValue(forKey: vet)\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for key in adjList.keys {\n            adjList[key]?.removeAll { $0 == vet }\n        }\n    }\n\n    /* 隣接リストを出力 */\n    public func print() {\n        Swift.print(\"隣接リスト =\")\n        for (vertex, list) in adjList {\n            let list = list.map { $0.val }\n            Swift.print(\"\\(vertex.val): \\(list),\")\n        }\n    }\n}\n
graph_adjacency_list.js
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    adjList;\n\n    /* コンストラクタ */\n    constructor(edges) {\n        this.adjList = new Map();\n        // すべての頂点と辺を追加\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    size() {\n        return this.adjList.size;\n    }\n\n    /* 辺を追加 */\n    addEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 辺 vet1 - vet2 を追加\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* 辺を削除 */\n    removeEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 辺 vet1 - vet2 を削除\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* 頂点を追加 */\n    addVertex(vet) {\n        if (this.adjList.has(vet)) return;\n        // 隣接リストに新しいリストを追加\n        this.adjList.set(vet, []);\n    }\n\n    /* 頂点を削除 */\n    removeVertex(vet) {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 隣接リストから頂点 vet に対応するリストを削除\n        this.adjList.delete(vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for (const set of this.adjList.values()) {\n            const index = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* 隣接リストを出力 */\n    print() {\n        console.log('隣接リスト =');\n        for (const [key, value] of this.adjList) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
graph_adjacency_list.ts
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    adjList: Map<Vertex, Vertex[]>;\n\n    /* コンストラクタ */\n    constructor(edges: Vertex[][]) {\n        this.adjList = new Map();\n        // すべての頂点と辺を追加\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 頂点数を取得 */\n    size(): number {\n        return this.adjList.size;\n    }\n\n    /* 辺を追加 */\n    addEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 辺 vet1 - vet2 を追加\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* 辺を削除 */\n    removeEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 辺 vet1 - vet2 を削除\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* 頂点を追加 */\n    addVertex(vet: Vertex): void {\n        if (this.adjList.has(vet)) return;\n        // 隣接リストに新しいリストを追加\n        this.adjList.set(vet, []);\n    }\n\n    /* 頂点を削除 */\n    removeVertex(vet: Vertex): void {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 隣接リストから頂点 vet に対応するリストを削除\n        this.adjList.delete(vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for (const set of this.adjList.values()) {\n            const index: number = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* 隣接リストを出力 */\n    print(): void {\n        console.log('隣接リスト =');\n        for (const [key, value] of this.adjList.entries()) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
graph_adjacency_list.dart
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList {\n  // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n  Map<Vertex, List<Vertex>> adjList = {};\n\n  /* コンストラクタ */\n  GraphAdjList(List<List<Vertex>> edges) {\n    for (List<Vertex> edge in edges) {\n      addVertex(edge[0]);\n      addVertex(edge[1]);\n      addEdge(edge[0], edge[1]);\n    }\n  }\n\n  /* 頂点数を取得 */\n  int size() {\n    return adjList.length;\n  }\n\n  /* 辺を追加 */\n  void addEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // 辺 vet1 - vet2 を追加\n    adjList[vet1]!.add(vet2);\n    adjList[vet2]!.add(vet1);\n  }\n\n  /* 辺を削除 */\n  void removeEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // 辺 vet1 - vet2 を削除\n    adjList[vet1]!.remove(vet2);\n    adjList[vet2]!.remove(vet1);\n  }\n\n  /* 頂点を追加 */\n  void addVertex(Vertex vet) {\n    if (adjList.containsKey(vet)) return;\n    // 隣接リストに新しいリストを追加\n    adjList[vet] = [];\n  }\n\n  /* 頂点を削除 */\n  void removeVertex(Vertex vet) {\n    if (!adjList.containsKey(vet)) {\n      throw ArgumentError;\n    }\n    // 隣接リストから頂点 vet に対応するリストを削除\n    adjList.remove(vet);\n    // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n    adjList.forEach((key, value) {\n      value.remove(vet);\n    });\n  }\n\n  /* 隣接リストを出力 */\n  void printAdjList() {\n    print(\"隣接リスト =\");\n    adjList.forEach((key, value) {\n      List<int> tmp = [];\n      for (Vertex vertex in value) {\n        tmp.add(vertex.val);\n      }\n      print(\"${key.val}: $tmp,\");\n    });\n  }\n}\n
graph_adjacency_list.rs
/* 隣接リストに基づく無向グラフ型 */\npub struct GraphAdjList {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    pub adj_list: HashMap<Vertex, Vec<Vertex>>, // maybe HashSet<Vertex> for value part is better?\n}\n\nimpl GraphAdjList {\n    /* コンストラクタ */\n    pub fn new(edges: Vec<[Vertex; 2]>) -> Self {\n        let mut graph = GraphAdjList {\n            adj_list: HashMap::new(),\n        };\n        // すべての頂点と辺を追加\n        for edge in edges {\n            graph.add_vertex(edge[0]);\n            graph.add_vertex(edge[1]);\n            graph.add_edge(edge[0], edge[1]);\n        }\n\n        graph\n    }\n\n    /* 頂点数を取得 */\n    #[allow(unused)]\n    pub fn size(&self) -> usize {\n        self.adj_list.len()\n    }\n\n    /* 辺を追加 */\n    pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // 辺 vet1 - vet2 を追加\n        self.adj_list.entry(vet1).or_default().push(vet2);\n        self.adj_list.entry(vet2).or_default().push(vet1);\n    }\n\n    /* 辺を削除 */\n    #[allow(unused)]\n    pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // 辺 vet1 - vet2 を削除\n        self.adj_list\n            .entry(vet1)\n            .and_modify(|v| v.retain(|&e| e != vet2));\n        self.adj_list\n            .entry(vet2)\n            .and_modify(|v| v.retain(|&e| e != vet1));\n    }\n\n    /* 頂点を追加 */\n    pub fn add_vertex(&mut self, vet: Vertex) {\n        if self.adj_list.contains_key(&vet) {\n            return;\n        }\n        // 隣接リストに新しいリストを追加\n        self.adj_list.insert(vet, vec![]);\n    }\n\n    /* 頂点を削除 */\n    #[allow(unused)]\n    pub fn remove_vertex(&mut self, vet: Vertex) {\n        // 隣接リストから頂点 vet に対応するリストを削除\n        self.adj_list.remove(&vet);\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for list in self.adj_list.values_mut() {\n            list.retain(|&v| v != vet);\n        }\n    }\n\n    /* 隣接リストを出力 */\n    pub fn print(&self) {\n        println!(\"隣接リスト =\");\n        for (vertex, list) in &self.adj_list {\n            let list = list.iter().map(|vertex| vertex.val).collect::<Vec<i32>>();\n            println!(\"{}: {:?},\", vertex.val, list);\n        }\n    }\n}\n
graph_adjacency_list.c
/* ノード構造体 */\ntypedef struct AdjListNode {\n    Vertex *vertex;           // 頂点\n    struct AdjListNode *next; // 後続ノード\n} AdjListNode;\n\n/* 頂点に対応するノードを検索 */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* 辺を追加する補助関数 */\nvoid addEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode));\n    node->vertex = vet;\n    // 先頭挿入法\n    node->next = head->next;\n    head->next = node;\n}\n\n/* 辺削除の補助関数 */\nvoid removeEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *pre = head;\n    AdjListNode *cur = head->next;\n    // 連結リスト内で vet に対応するノードを探索\n    while (cur != NULL && cur->vertex != vet) {\n        pre = cur;\n        cur = cur->next;\n    }\n    if (cur == NULL)\n        return;\n    // vet に対応するノードを連結リストから削除\n    pre->next = cur->next;\n    // メモリを解放する\n    free(cur);\n}\n\n/* 隣接リストに基づく無向グラフクラス */\ntypedef struct {\n    AdjListNode *heads[MAX_SIZE]; // ノード配列\n    int size;                     // ノード数\n} GraphAdjList;\n\n/* コンストラクタ */\nGraphAdjList *newGraphAdjList() {\n    GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList));\n    if (!graph) {\n        return NULL;\n    }\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        graph->heads[i] = NULL;\n    }\n    return graph;\n}\n\n/* デストラクタ */\nvoid delGraphAdjList(GraphAdjList *graph) {\n    for (int i = 0; i < graph->size; i++) {\n        AdjListNode *cur = graph->heads[i];\n        while (cur != NULL) {\n            AdjListNode *next = cur->next;\n            if (cur != graph->heads[i]) {\n                free(cur);\n            }\n            cur = next;\n        }\n        free(graph->heads[i]->vertex);\n        free(graph->heads[i]);\n    }\n    free(graph);\n}\n\n/* 頂点に対応するノードを検索 */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* 辺を追加 */\nvoid addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL && head1 != head2);\n    // 辺 vet1 - vet2 を追加\n    addEdgeHelper(head1, vet2);\n    addEdgeHelper(head2, vet1);\n}\n\n/* 辺を削除 */\nvoid removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL);\n    // 辺 vet1 - vet2 を削除\n    removeEdgeHelper(head1, head2->vertex);\n    removeEdgeHelper(head2, head1->vertex);\n}\n\n/* 頂点を追加 */\nvoid addVertex(GraphAdjList *graph, Vertex *vet) {\n    assert(graph != NULL && graph->size < MAX_SIZE);\n    AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode));\n    head->vertex = vet;\n    head->next = NULL;\n    // 隣接リストに新しいリストを追加\n    graph->heads[graph->size++] = head;\n}\n\n/* 頂点を削除 */\nvoid removeVertex(GraphAdjList *graph, Vertex *vet) {\n    AdjListNode *node = findNode(graph, vet);\n    assert(node != NULL);\n    // 隣接リストから頂点 vet に対応するリストを削除\n    AdjListNode *cur = node, *pre = NULL;\n    while (cur) {\n        pre = cur;\n        cur = cur->next;\n        free(pre);\n    }\n    // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n    for (int i = 0; i < graph->size; i++) {\n        cur = graph->heads[i];\n        pre = NULL;\n        while (cur) {\n            pre = cur;\n            cur = cur->next;\n            if (cur && cur->vertex == vet) {\n                pre->next = cur->next;\n                free(cur);\n                break;\n            }\n        }\n    }\n    // この頂点より後ろの頂点を前に詰めて欠損を埋める\n    int i;\n    for (i = 0; i < graph->size; i++) {\n        if (graph->heads[i] == node)\n            break;\n    }\n    for (int j = i; j < graph->size - 1; j++) {\n        graph->heads[j] = graph->heads[j + 1];\n    }\n    graph->size--;\n    free(vet);\n}\n
graph_adjacency_list.kt
/* 隣接リストに基づく無向グラフクラス */\nclass GraphAdjList(edges: Array<Array<Vertex?>>) {\n    // 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    val adjList = HashMap<Vertex, MutableList<Vertex>>()\n\n    /* コンストラクタ */\n    init {\n        // すべての頂点と辺を追加\n        for (edge in edges) {\n            addVertex(edge[0]!!)\n            addVertex(edge[1]!!)\n            addEdge(edge[0]!!, edge[1]!!)\n        }\n    }\n\n    /* 頂点数を取得 */\n    fun size(): Int {\n        return adjList.size\n    }\n\n    /* 辺を追加 */\n    fun addEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // 辺 vet1 - vet2 を追加\n        adjList[vet1]?.add(vet2)\n        adjList[vet2]?.add(vet1)\n    }\n\n    /* 辺を削除 */\n    fun removeEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // 辺 vet1 - vet2 を削除\n        adjList[vet1]?.remove(vet2)\n        adjList[vet2]?.remove(vet1)\n    }\n\n    /* 頂点を追加 */\n    fun addVertex(vet: Vertex) {\n        if (adjList.containsKey(vet))\n            return\n        // 隣接リストに新しいリストを追加\n        adjList[vet] = mutableListOf()\n    }\n\n    /* 頂点を削除 */\n    fun removeVertex(vet: Vertex) {\n        if (!adjList.containsKey(vet))\n            throw IllegalArgumentException()\n        // 隣接リストから頂点 vet に対応するリストを削除\n        adjList.remove(vet)\n        // 他の頂点のリストを走査し、vet を含むすべての辺を削除\n        for (list in adjList.values) {\n            list.remove(vet)\n        }\n    }\n\n    /* 隣接リストを出力 */\n    fun print() {\n        println(\"隣接リスト =\")\n        for (pair in adjList.entries) {\n            val tmp = mutableListOf<Int>()\n            for (vertex in pair.value) {\n                tmp.add(vertex._val)\n            }\n            println(\"${pair.key._val}: $tmp,\")\n        }\n    }\n}\n
graph_adjacency_list.rb
### 隣接リストで実装した無向グラフクラス ###\nclass GraphAdjList\n  attr_reader :adj_list\n\n  ### コンストラクタ ###\n  def initialize(edges)\n    # 隣接リスト。key は頂点、value はその頂点に隣接する全頂点\n    @adj_list = {}\n    # すべての頂点と辺を追加\n    for edge in edges\n      add_vertex(edge[0])\n      add_vertex(edge[1])\n      add_edge(edge[0], edge[1])\n    end\n  end\n\n  ### 頂点数を取得 ###\n  def size\n    @adj_list.length\n  end\n\n  ### 辺を追加 ###\n  def add_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    @adj_list[vet1] << vet2\n    @adj_list[vet2] << vet1\n  end\n\n  ### 辺を削除 ###\n  def remove_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    # 辺 vet1 - vet2 を削除\n    @adj_list[vet1].delete(vet2)\n    @adj_list[vet2].delete(vet1)\n  end\n\n  ### 頂点を追加 ###\n  def add_vertex(vet)\n    return if @adj_list.include?(vet)\n\n    # 隣接リストに新しいリストを追加\n    @adj_list[vet] = []\n  end\n\n  ### 頂点を削除 ###\n  def remove_vertex(vet)\n    raise ArgumentError unless @adj_list.include?(vet)\n\n    # 隣接リストから頂点 vet に対応するリストを削除\n    @adj_list.delete(vet)\n    # 他の頂点のリストを走査し、vet を含むすべての辺を削除\n    for vertex in @adj_list\n      @adj_list[vertex.first].delete(vet) if @adj_list[vertex.first].include?(vet)\n    end\n  end\n\n  ### 隣接リストを出力 ###\n  def __print__\n    puts '隣接リスト ='\n    for vertex in @adj_list\n      tmp = @adj_list[vertex.first].map { |v| v.val }\n      puts \"#{vertex.first.val}: #{tmp},\"\n    end\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#923","level":2,"title":"9.2.3   効率の比較","text":"

グラフに \\(n\\) 個の頂点と \\(m\\) 本の辺があるとすると、次の表は隣接行列と隣接リストの時間効率および空間効率を比較したものです。なお、隣接リスト(連結リスト)は本記事の実装に対応し、隣接リスト(ハッシュテーブル)はすべての連結リストをハッシュテーブルに置き換えた実装を指します。

表 9-2   隣接行列と隣接リストの比較

隣接行列 隣接リスト(連結リスト) 隣接リスト(ハッシュテーブル) 隣接判定 \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) 辺の追加 \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) 辺の削除 \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) 頂点の追加 \\(O(n)\\) \\(O(1)\\) \\(O(1)\\) 頂点の削除 \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n)\\) メモリ使用量 \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n + m)\\)

上表を見ると、隣接リスト(ハッシュテーブル)の時間効率と空間効率が最も優れているように見えます。しかし実際には、隣接行列のほうが辺の操作効率は高く、必要なのは 1 回の配列アクセスまたは代入だけです。総合的に見ると、隣接行列は「空間を時間と引き換えにする」原則を体現し、隣接リストは「時間を空間と引き換えにする」原則を体現しています。

","path":["第 9 章   グラフ","9.2   グラフの基本操作"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   グラフの走査","text":"

木は「一対多」の関係を表すのに対し、グラフはより高い自由度を持ち、任意の「多対多」の関係を表現できます。したがって、木はグラフの一種の特殊な場合とみなせます。明らかに、木の走査操作もグラフの走査操作の一種の特殊な場合です。

グラフと木はいずれも、走査操作を実現するために探索アルゴリズムを用いる必要があります。グラフの走査方法も、幅優先走査と深さ優先走査の 2 種類に分けられます。

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931","level":2,"title":"9.3.1   幅優先走査","text":"

幅優先走査は、近いところから遠いところへ向かう走査方法であり、ある頂点から出発して、常に最も近い頂点を優先して訪問し、層ごとに外側へ広がっていきます。以下の図に示すように、左上の頂点から出発し、まずその頂点のすべての隣接頂点を走査し、次に次の頂点のすべての隣接頂点を走査し、これを繰り返して、すべての頂点を訪問するまで続けます。

図 9-9   グラフの幅優先走査

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1","level":3,"title":"1.   アルゴリズムの実装","text":"

BFS は通常キューを用いて実装され、コードは以下のとおりです。キューは「先入れ先出し」という性質を持ち、これは BFS の「近いところから遠いところへ」という考え方と本質的に一致しています。

  1. 走査の開始頂点 startVet をキューに追加し、ループを開始します。
  2. ループの各反復で、キュー先頭の頂点を取り出して訪問を記録し、その後その頂点のすべての隣接頂点をキューの末尾に追加します。
  3. 手順 2. を繰り返し、すべての頂点が訪問されると終了します。

頂点の重複走査を防ぐために、どの頂点が訪問済みかを記録するハッシュ集合 visited を用います。

Tip

ハッシュ集合は、value を持たず key だけを格納するハッシュテーブルとみなせます。これは \\(O(1)\\) の時間計算量で key の追加・削除・検索・更新を行えます。key の一意性にもとづき、ハッシュ集合は通常、データの重複排除などの場面で用いられます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_bfs.py
def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"幅優先探索\"\"\"\n    # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する\n    # 頂点の走査順序\n    res = []\n    # 訪問済み頂点を記録するためのハッシュ集合\n    visited = set[Vertex]([start_vet])\n    # BFS の実装にキューを用いる\n    que = deque[Vertex]([start_vet])\n    # 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while len(que) > 0:\n        vet = que.popleft()  # 先頭の頂点をデキュー\n        res.append(vet)  # 訪問した頂点を記録\n        # この頂点のすべての隣接頂点を走査\n        for adj_vet in graph.adj_list[vet]:\n            if adj_vet in visited:\n                continue  # 訪問済みの頂点をスキップ\n            que.append(adj_vet)  # 未訪問の頂点のみをキューに追加\n            visited.add(adj_vet)  # この頂点を訪問済みにする\n    # 頂点の走査順を返す\n    return res\n
graph_bfs.cpp
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nvector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {\n    // 頂点の走査順序\n    vector<Vertex *> res;\n    // 訪問済み頂点を記録するためのハッシュ集合\n    unordered_set<Vertex *> visited = {startVet};\n    // BFS の実装にキューを用いる\n    queue<Vertex *> que;\n    que.push(startVet);\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (!que.empty()) {\n        Vertex *vet = que.front();\n        que.pop();          // 先頭の頂点をデキュー\n        res.push_back(vet); // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for (auto adjVet : graph.adjList[vet]) {\n            if (visited.count(adjVet))\n                continue;            // 訪問済みの頂点をスキップ\n            que.push(adjVet);        // 未訪問の頂点のみをキューに追加\n            visited.emplace(adjVet); // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res;\n}\n
graph_bfs.java
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n    // 頂点の走査順序\n    List<Vertex> res = new ArrayList<>();\n    // 訪問済み頂点を記録するためのハッシュ集合\n    Set<Vertex> visited = new HashSet<>();\n    visited.add(startVet);\n    // BFS の実装にキューを用いる\n    Queue<Vertex> que = new LinkedList<>();\n    que.offer(startVet);\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (!que.isEmpty()) {\n        Vertex vet = que.poll(); // 先頭の頂点をデキュー\n        res.add(vet);            // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for (Vertex adjVet : graph.adjList.get(vet)) {\n            if (visited.contains(adjVet))\n                continue;        // 訪問済みの頂点をスキップ\n            que.offer(adjVet);   // 未訪問の頂点のみをキューに追加\n            visited.add(adjVet); // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res;\n}\n
graph_bfs.cs
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nList<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {\n    // 頂点の走査順序\n    List<Vertex> res = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    HashSet<Vertex> visited = [startVet];\n    // BFS の実装にキューを用いる\n    Queue<Vertex> que = new();\n    que.Enqueue(startVet);\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (que.Count > 0) {\n        Vertex vet = que.Dequeue(); // 先頭の頂点をデキュー\n        res.Add(vet);               // 訪問した頂点を記録\n        foreach (Vertex adjVet in graph.adjList[vet]) {\n            if (visited.Contains(adjVet)) {\n                continue;          // 訪問済みの頂点をスキップ\n            }\n            que.Enqueue(adjVet);   // 未訪問の頂点のみをキューに追加\n            visited.Add(adjVet);   // この頂点を訪問済みにする\n        }\n    }\n\n    // 頂点の走査順を返す\n    return res;\n}\n
graph_bfs.go
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunc graphBFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // 頂点の走査順序\n    res := make([]Vertex, 0)\n    // 訪問済み頂点を記録するためのハッシュ集合\n    visited := make(map[Vertex]struct{})\n    visited[startVet] = struct{}{}\n    // キューは BFS の実装に用い、スライスでキューをシミュレートする\n    queue := make([]Vertex, 0)\n    queue = append(queue, startVet)\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    for len(queue) > 0 {\n        // 先頭の頂点をデキュー\n        vet := queue[0]\n        queue = queue[1:]\n        // 訪問した頂点を記録\n        res = append(res, vet)\n        // この頂点のすべての隣接頂点を走査\n        for _, adjVet := range g.adjList[vet] {\n            _, isExist := visited[adjVet]\n            // 未訪問の頂点のみをキューに追加\n            if !isExist {\n                queue = append(queue, adjVet)\n                visited[adjVet] = struct{}{}\n            }\n        }\n    }\n    // 頂点の走査順を返す\n    return res\n}\n
graph_bfs.swift
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunc graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // 頂点の走査順序\n    var res: [Vertex] = []\n    // 訪問済み頂点を記録するためのハッシュ集合\n    var visited: Set<Vertex> = [startVet]\n    // BFS の実装にキューを用いる\n    var que: [Vertex] = [startVet]\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while !que.isEmpty {\n        let vet = que.removeFirst() // 先頭の頂点をデキュー\n        res.append(vet) // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for adjVet in graph.adjList[vet] ?? [] {\n            if visited.contains(adjVet) {\n                continue // 訪問済みの頂点をスキップ\n            }\n            que.append(adjVet) // 未訪問の頂点のみをキューに追加\n            visited.insert(adjVet) // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res\n}\n
graph_bfs.js
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunction graphBFS(graph, startVet) {\n    // 頂点の走査順序\n    const res = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    const visited = new Set();\n    visited.add(startVet);\n    // BFS の実装にキューを用いる\n    const que = [startVet];\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (que.length) {\n        const vet = que.shift(); // 先頭の頂点をデキュー\n        res.push(vet); // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // 訪問済みの頂点をスキップ\n            }\n            que.push(adjVet); // 未訪問の頂点のみをキューに追加\n            visited.add(adjVet); // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res;\n}\n
graph_bfs.ts
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunction graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // 頂点の走査順序\n    const res: Vertex[] = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    const visited: Set<Vertex> = new Set();\n    visited.add(startVet);\n    // BFS の実装にキューを用いる\n    const que = [startVet];\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (que.length) {\n        const vet = que.shift(); // 先頭の頂点をデキュー\n        res.push(vet); // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // 訪問済みの頂点をスキップ\n            }\n            que.push(adjVet); // 未訪問のものだけをキューに入れる\n            visited.add(adjVet); // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res;\n}\n
graph_bfs.dart
/* 幅優先探索 */\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n  // 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する\n  // 頂点の走査順序\n  List<Vertex> res = [];\n  // 訪問済み頂点を記録するためのハッシュ集合\n  Set<Vertex> visited = {};\n  visited.add(startVet);\n  // BFS の実装にキューを用いる\n  Queue<Vertex> que = Queue();\n  que.add(startVet);\n  // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n  while (que.isNotEmpty) {\n    Vertex vet = que.removeFirst(); // 先頭の頂点をデキュー\n    res.add(vet); // 訪問した頂点を記録\n    // この頂点のすべての隣接頂点を走査\n    for (Vertex adjVet in graph.adjList[vet]!) {\n      if (visited.contains(adjVet)) {\n        continue; // 訪問済みの頂点をスキップ\n      }\n      que.add(adjVet); // 未訪問の頂点のみをキューに追加\n      visited.add(adjVet); // この頂点を訪問済みにする\n    }\n  }\n  // 頂点の走査順を返す\n  return res;\n}\n
graph_bfs.rs
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // 頂点の走査順序\n    let mut res = vec![];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    let mut visited = HashSet::new();\n    visited.insert(start_vet);\n    // BFS の実装にキューを用いる\n    let mut que = VecDeque::new();\n    que.push_back(start_vet);\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while let Some(vet) = que.pop_front() {\n        res.push(vet); // 訪問した頂点を記録\n\n        // この頂点のすべての隣接頂点を走査\n        if let Some(adj_vets) = graph.adj_list.get(&vet) {\n            for &adj_vet in adj_vets {\n                if visited.contains(&adj_vet) {\n                    continue; // 訪問済みの頂点をスキップ\n                }\n                que.push_back(adj_vet); // 未訪問の頂点のみをキューに追加\n                visited.insert(adj_vet); // この頂点を訪問済みにする\n            }\n        }\n    }\n    // 頂点の走査順を返す\n    res\n}\n
graph_bfs.c
/* ノードキュー構造体 */\ntypedef struct {\n    Vertex *vertices[MAX_SIZE];\n    int front, rear, size;\n} Queue;\n\n/* コンストラクタ */\nQueue *newQueue() {\n    Queue *q = (Queue *)malloc(sizeof(Queue));\n    q->front = q->rear = q->size = 0;\n    return q;\n}\n\n/* キューが空かどうかを判定 */\nint isEmpty(Queue *q) {\n    return q->size == 0;\n}\n\n/* エンキュー操作 */\nvoid enqueue(Queue *q, Vertex *vet) {\n    q->vertices[q->rear] = vet;\n    q->rear = (q->rear + 1) % MAX_SIZE;\n    q->size++;\n}\n\n/* デキュー操作 */\nVertex *dequeue(Queue *q) {\n    Vertex *vet = q->vertices[q->front];\n    q->front = (q->front + 1) % MAX_SIZE;\n    q->size--;\n    return vet;\n}\n\n/* 頂点が訪問済みかを確認 */\nint isVisited(Vertex **visited, int size, Vertex *vet) {\n    // 走査してノードを探すため、O(n) 時間を要する\n    for (int i = 0; i < size; i++) {\n        if (visited[i] == vet)\n            return 1;\n    }\n    return 0;\n}\n\n/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nvoid graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) {\n    // BFS の実装にキューを用いる\n    Queue *queue = newQueue();\n    enqueue(queue, startVet);\n    visited[(*visitedSize)++] = startVet;\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (!isEmpty(queue)) {\n        Vertex *vet = dequeue(queue); // 先頭の頂点をデキュー\n        res[(*resSize)++] = vet;      // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        AdjListNode *node = findNode(graph, vet);\n        while (node != NULL) {\n            // 訪問済みの頂点をスキップ\n            if (!isVisited(visited, *visitedSize, node->vertex)) {\n                enqueue(queue, node->vertex);             // 未訪問の頂点のみをキューに追加\n                visited[(*visitedSize)++] = node->vertex; // この頂点を訪問済みにする\n            }\n            node = node->next;\n        }\n    }\n    // メモリを解放する\n    free(queue);\n}\n
graph_bfs.kt
/* 幅優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {\n    // 頂点の走査順序\n    val res = mutableListOf<Vertex?>()\n    // 訪問済み頂点を記録するためのハッシュ集合\n    val visited = HashSet<Vertex>()\n    visited.add(startVet)\n    // BFS の実装にキューを用いる\n    val que = LinkedList<Vertex>()\n    que.offer(startVet)\n    // 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n    while (!que.isEmpty()) {\n        val vet = que.poll() // 先頭の頂点をデキュー\n        res.add(vet)         // 訪問した頂点を記録\n        // この頂点のすべての隣接頂点を走査\n        for (adjVet in graph.adjList[vet]!!) {\n            if (visited.contains(adjVet))\n                continue        // 訪問済みの頂点をスキップ\n            que.offer(adjVet)   // 未訪問の頂点のみをキューに追加\n            visited.add(adjVet) // この頂点を訪問済みにする\n        }\n    }\n    // 頂点の走査順を返す\n    return res\n}\n
graph_bfs.rb
### 幅優先探索 ###\ndef graph_bfs(graph, start_vet)\n  # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する\n  # 頂点の走査順序\n  res = []\n  # 訪問済み頂点を記録するためのハッシュ集合\n  visited = Set.new([start_vet])\n  # BFS の実装にキューを用いる\n  que = [start_vet]\n  # 頂点 vet を起点に、すべての頂点を訪問し終えるまで繰り返す\n  while que.length > 0\n    vet = que.shift # 先頭の頂点をデキュー\n    res << vet # 訪問した頂点を記録\n    # この頂点のすべての隣接頂点を走査\n    for adj_vet in graph.adj_list[vet]\n      next if visited.include?(adj_vet) # 訪問済みの頂点をスキップ\n      que << adj_vet # 未訪問の頂点のみをキューに追加\n      visited.add(adj_vet) # この頂点を訪問済みにする\n    end\n  end\n  # 頂点の走査順を返す\n  res\nend\n
コードの可視化

全画面で見る >

コードはやや抽象的なので、以下の図と照らし合わせて理解を深めることを勧めます。

<1><2><3><4><5><6><7><8><9><10><11>

図 9-10   グラフの幅優先走査の手順

幅優先走査の順序列は一意ですか?

一意ではありません。幅優先走査は「近いところから遠いところへ」の順で走査することだけを要求し、同じ距離にある複数の頂点の走査順は任意に入れ替えて構いません。上図を例にすると、頂点 \\(1\\) と \\(3\\) の訪問順は交換でき、頂点 \\(2\\)、\\(4\\)、\\(6\\) の訪問順も任意に入れ替えられます。

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2","level":3,"title":"2.   計算量の分析","text":"

時間計算量:すべての頂点は 1 回ずつキューに入り、1 回ずつキューから出るため、\\(O(|V|)\\) 時間です。隣接頂点を走査する過程では、無向グラフであるため、すべての辺が \\(2\\) 回訪問され、\\(O(2|E|)\\) 時間です。したがって全体では \\(O(|V| + |E|)\\) 時間です。

空間計算量:リスト res、ハッシュ集合 visited、キュー que に含まれる頂点数は最大で \\(|V|\\) であるため、\\(O(|V|)\\) 空間です。

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#932","level":2,"title":"9.3.2   深さ優先走査","text":"

深さ優先走査は、まず行けるところまで進み、進めなくなったら戻る走査方法です。以下の図に示すように、左上の頂点から出発し、現在の頂点のある隣接頂点を訪問して、行き止まりに達するまで進んだら戻り、再び別の方向へ進んで行き止まりまで進んで戻る、ということを繰り返し、すべての頂点の走査が完了するまで続けます。

図 9-11   グラフの深さ優先走査

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1_1","level":3,"title":"1.   アルゴリズムの実装","text":"

この「行き止まりまで進んでから戻る」アルゴリズムのパターンは、通常再帰にもとづいて実装されます。幅優先走査と同様に、深さ優先走査でも、頂点の重複訪問を避けるために、訪問済みの頂点を記録するハッシュ集合 visited を用います。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_dfs.py
def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):\n    \"\"\"深さ優先走査の補助関数\"\"\"\n    res.append(vet)  # 訪問した頂点を記録\n    visited.add(vet)  # この頂点を訪問済みにする\n    # この頂点のすべての隣接頂点を走査\n    for adjVet in graph.adj_list[vet]:\n        if adjVet in visited:\n            continue  # 訪問済みの頂点をスキップ\n        # 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet)\n\ndef graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"深さ優先探索\"\"\"\n    # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する\n    # 頂点の走査順序\n    res = []\n    # 訪問済み頂点を記録するためのハッシュ集合\n    visited = set[Vertex]()\n    dfs(graph, visited, res, start_vet)\n    return res\n
graph_dfs.cpp
/* 深さ優先走査の補助関数 */\nvoid dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {\n    res.push_back(vet);   // 訪問した頂点を記録\n    visited.emplace(vet); // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for (Vertex *adjVet : graph.adjList[vet]) {\n        if (visited.count(adjVet))\n            continue; // 訪問済みの頂点をスキップ\n        // 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nvector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {\n    // 頂点の走査順序\n    vector<Vertex *> res;\n    // 訪問済み頂点を記録するためのハッシュ集合\n    unordered_set<Vertex *> visited;\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.java
/* 深さ優先走査の補助関数 */\nvoid dfs(GraphAdjList graph, Set<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.add(vet);     // 訪問した頂点を記録\n    visited.add(vet); // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for (Vertex adjVet : graph.adjList.get(vet)) {\n        if (visited.contains(adjVet))\n            continue; // 訪問済みの頂点をスキップ\n        // 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n    // 頂点の走査順序\n    List<Vertex> res = new ArrayList<>();\n    // 訪問済み頂点を記録するためのハッシュ集合\n    Set<Vertex> visited = new HashSet<>();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.cs
/* 深さ優先走査の補助関数 */\nvoid DFS(GraphAdjList graph, HashSet<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.Add(vet);     // 訪問した頂点を記録\n    visited.Add(vet); // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    foreach (Vertex adjVet in graph.adjList[vet]) {\n        if (visited.Contains(adjVet)) {\n            continue; // 訪問済みの頂点をスキップ\n        }\n        // 隣接頂点を再帰的に訪問\n        DFS(graph, visited, res, adjVet);\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nList<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {\n    // 頂点の走査順序\n    List<Vertex> res = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    HashSet<Vertex> visited = [];\n    DFS(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.go
/* 深さ優先走査の補助関数 */\nfunc dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) {\n    // append 操作は新しい参照を返すため、元の参照を新しい slice の参照で再代入する必要がある\n    *res = append(*res, vet)\n    visited[vet] = struct{}{}\n    // この頂点のすべての隣接頂点を走査\n    for _, adjVet := range g.adjList[vet] {\n        _, isExist := visited[adjVet]\n        // 隣接頂点を再帰的に訪問\n        if !isExist {\n            dfs(g, visited, res, adjVet)\n        }\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunc graphDFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // 頂点の走査順序\n    res := make([]Vertex, 0)\n    // 訪問済み頂点を記録するためのハッシュ集合\n    visited := make(map[Vertex]struct{})\n    dfs(g, visited, &res, startVet)\n    // 頂点の走査順を返す\n    return res\n}\n
graph_dfs.swift
/* 深さ優先走査の補助関数 */\nfunc dfs(graph: GraphAdjList, visited: inout Set<Vertex>, res: inout [Vertex], vet: Vertex) {\n    res.append(vet) // 訪問した頂点を記録\n    visited.insert(vet) // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for adjVet in graph.adjList[vet] ?? [] {\n        if visited.contains(adjVet) {\n            continue // 訪問済みの頂点をスキップ\n        }\n        // 隣接頂点を再帰的に訪問\n        dfs(graph: graph, visited: &visited, res: &res, vet: adjVet)\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunc graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // 頂点の走査順序\n    var res: [Vertex] = []\n    // 訪問済み頂点を記録するためのハッシュ集合\n    var visited: Set<Vertex> = []\n    dfs(graph: graph, visited: &visited, res: &res, vet: startVet)\n    return res\n}\n
graph_dfs.js
/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunction dfs(graph, visited, res, vet) {\n    res.push(vet); // 訪問した頂点を記録\n    visited.add(vet); // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // 訪問済みの頂点をスキップ\n        }\n        // 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunction graphDFS(graph, startVet) {\n    // 頂点の走査順序\n    const res = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    const visited = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.ts
/* 深さ優先走査の補助関数 */\nfunction dfs(\n    graph: GraphAdjList,\n    visited: Set<Vertex>,\n    res: Vertex[],\n    vet: Vertex\n): void {\n    res.push(vet); // 訪問した頂点を記録\n    visited.add(vet); // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // 訪問済みの頂点をスキップ\n        }\n        // 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfunction graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // 頂点の走査順序\n    const res: Vertex[] = [];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    const visited: Set<Vertex> = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.dart
/* 深さ優先走査の補助関数 */\nvoid dfs(\n  GraphAdjList graph,\n  Set<Vertex> visited,\n  List<Vertex> res,\n  Vertex vet,\n) {\n  res.add(vet); // 訪問した頂点を記録\n  visited.add(vet); // この頂点を訪問済みにする\n  // この頂点のすべての隣接頂点を走査\n  for (Vertex adjVet in graph.adjList[vet]!) {\n    if (visited.contains(adjVet)) {\n      continue; // 訪問済みの頂点をスキップ\n    }\n    // 隣接頂点を再帰的に訪問\n    dfs(graph, visited, res, adjVet);\n  }\n}\n\n/* 深さ優先探索 */\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n  // 頂点の走査順序\n  List<Vertex> res = [];\n  // 訪問済み頂点を記録するためのハッシュ集合\n  Set<Vertex> visited = {};\n  dfs(graph, visited, res, startVet);\n  return res;\n}\n
graph_dfs.rs
/* 深さ優先走査の補助関数 */\nfn dfs(graph: &GraphAdjList, visited: &mut HashSet<Vertex>, res: &mut Vec<Vertex>, vet: Vertex) {\n    res.push(vet); // 訪問した頂点を記録\n    visited.insert(vet); // この頂点を訪問済みにする\n                         // この頂点のすべての隣接頂点を走査\n    if let Some(adj_vets) = graph.adj_list.get(&vet) {\n        for &adj_vet in adj_vets {\n            if visited.contains(&adj_vet) {\n                continue; // 訪問済みの頂点をスキップ\n            }\n            // 隣接頂点を再帰的に訪問\n            dfs(graph, visited, res, adj_vet);\n        }\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // 頂点の走査順序\n    let mut res = vec![];\n    // 訪問済み頂点を記録するためのハッシュ集合\n    let mut visited = HashSet::new();\n    dfs(&graph, &mut visited, &mut res, start_vet);\n\n    res\n}\n
graph_dfs.c
/* 頂点が訪問済みかを確認 */\nint isVisited(Vertex **res, int size, Vertex *vet) {\n    // 走査してノードを探すため、O(n) 時間を要する\n    for (int i = 0; i < size; i++) {\n        if (res[i] == vet) {\n            return 1;\n        }\n    }\n    return 0;\n}\n\n/* 深さ優先走査の補助関数 */\nvoid dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) {\n    // 訪問した頂点を記録\n    res[(*resSize)++] = vet;\n    // この頂点のすべての隣接頂点を走査\n    AdjListNode *node = findNode(graph, vet);\n    while (node != NULL) {\n        // 訪問済みの頂点をスキップ\n        if (!isVisited(res, *resSize, node->vertex)) {\n            // 隣接頂点を再帰的に訪問\n            dfs(graph, res, resSize, node->vertex);\n        }\n        node = node->next;\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nvoid graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) {\n    dfs(graph, res, resSize, startVet);\n}\n
graph_dfs.kt
/* 深さ優先走査の補助関数 */\nfun dfs(\n    graph: GraphAdjList,\n    visited: MutableSet<Vertex?>,\n    res: MutableList<Vertex?>,\n    vet: Vertex?\n) {\n    res.add(vet)     // 訪問した頂点を記録\n    visited.add(vet) // この頂点を訪問済みにする\n    // この頂点のすべての隣接頂点を走査\n    for (adjVet in graph.adjList[vet]!!) {\n        if (visited.contains(adjVet))\n            continue  // 訪問済みの頂点をスキップ\n        // 隣接頂点を再帰的に訪問\n        dfs(graph, visited, res, adjVet)\n    }\n}\n\n/* 深さ優先探索 */\n// グラフを隣接リストで表し、指定した頂点の隣接頂点をすべて取得できるようにする\nfun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {\n    // 頂点の走査順序\n    val res = mutableListOf<Vertex?>()\n    // 訪問済み頂点を記録するためのハッシュ集合\n    val visited = HashSet<Vertex?>()\n    dfs(graph, visited, res, startVet)\n    return res\n}\n
graph_dfs.rb
### 深さ優先探索の補助関数 ###\ndef dfs(graph, visited, res, vet)\n  res << vet # 訪問した頂点を記録\n  visited.add(vet) # この頂点を訪問済みにする\n  # この頂点のすべての隣接頂点を走査\n  for adj_vet in graph.adj_list[vet]\n    next if visited.include?(adj_vet) # 訪問済みの頂点をスキップ\n    # 隣接頂点を再帰的に訪問\n    dfs(graph, visited, res, adj_vet)\n  end\nend\n\n### 深さ優先探索 ###\ndef graph_dfs(graph, start_vet)\n  # 指定した頂点の隣接頂点をすべて取得できるよう、隣接リストでグラフを表現する\n  # 頂点の走査順序\n  res = []\n  # 訪問済み頂点を記録するためのハッシュ集合\n  visited = Set.new\n  dfs(graph, visited, res, start_vet)\n  res\nend\n
コードの可視化

全画面で見る >

深さ優先走査のアルゴリズムの流れは以下の図のとおりです。

  • **直線の破線は下向きの再帰呼び出し**を表し、新しい頂点を訪問するために新たな再帰メソッドが開始されたことを意味します。
  • **曲線の破線は上向きのバックトラック**を表し、この再帰メソッドがすでに戻って、呼び出し元の位置までたどり着いたことを意味します。

理解を深めるために、以下の図とコードを結びつけて、DFS 全体の過程を頭の中でシミュレーションする(あるいは紙に書き出す)ことを勧めます。各再帰メソッドがいつ開始し、いつ戻るかも含めて追ってみてください。

<1><2><3><4><5><6><7><8><9><10><11>

図 9-12   グラフの深さ優先走査の手順

深さ優先走査の順序列は一意ですか?

幅優先走査と同様に、深さ優先走査の順序列も一意ではありません。ある頂点が与えられたとき、どの方向を先に探索してもよく、つまり隣接頂点の順序は任意に入れ替えられ、それでも深さ優先走査になります。

木の走査を例にすると、「根 \\(\\rightarrow\\) 左 \\(\\rightarrow\\) 右」「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」「左 \\(\\rightarrow\\) 右 \\(\\rightarrow\\) 根」は、それぞれ先行順、中間順、後行順走査に対応します。これらは 3 種類の走査優先順位を示していますが、いずれも深さ優先走査に属します。

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2_1","level":3,"title":"2.   計算量の分析","text":"

時間計算量:すべての頂点は \\(1\\) 回ずつ訪問されるため、\\(O(|V|)\\) 時間です。すべての辺は \\(2\\) 回ずつ訪問されるため、\\(O(2|E|)\\) 時間です。したがって全体では \\(O(|V| + |E|)\\) 時間です。

空間計算量:リスト res とハッシュ集合 visited に含まれる頂点数は最大で \\(|V|\\) であり、再帰の深さも最大で \\(|V|\\) であるため、\\(O(|V|)\\) 空間です。

","path":["第 9 章   グラフ","9.3   グラフの走査"],"tags":[]},{"location":"chapter_graph/summary/","level":1,"title":"9.4   まとめ","text":"","path":["第 9 章   グラフ","9.4   まとめ"],"tags":[]},{"location":"chapter_graph/summary/#1","level":3,"title":"1.   重要なポイントの振り返り","text":"
  • グラフは頂点と辺から構成され、一組の頂点と一組の辺からなる集合として表せます。
  • 線形関係(連結リスト)や分治関係(木)と比べて、ネットワーク関係(グラフ)は自由度が高く、そのぶん複雑です。
  • 有向グラフの辺は方向性を持ち、連結グラフでは任意の頂点に到達でき、重み付きグラフの各辺は重み変数を含みます。
  • 隣接行列は行列を用いてグラフを表し、各行(列)が 1 つの頂点を表し、行列要素が辺を表します。\\(1\\) または \\(0\\) を用いて、2 つの頂点の間に辺があるかないかを示します。隣接行列は追加・削除・検索・更新の操作効率が高い一方で、より多くの空間を消費します。
  • 隣接リストは複数の連結リストを使ってグラフを表し、第 \\(i\\) 個の連結リストが頂点 \\(i\\) に対応し、その頂点に隣接するすべての頂点を格納します。隣接リストは隣接行列よりも省スペースですが、辺を探すために連結リストを走査する必要があるため、時間効率は低くなります。
  • 隣接リスト内の連結リストが長くなりすぎた場合は、赤黒木やハッシュテーブルに変換することで、検索効率を高められます。
  • アルゴリズムの考え方という観点では、隣接行列は「空間を時間と引き換えにする」ことを体現し、隣接リストは「時間を空間と引き換えにする」ことを体現します。
  • グラフは、ソーシャルネットワークや地下鉄路線など、さまざまな現実のシステムをモデル化するために使えます。
  • 木はグラフの特殊な一例であり、木の走査もグラフ走査の特殊な一例です。
  • グラフの幅優先探索は、近いところから遠いところへ、層ごとに広がっていく探索方法であり、通常はキューを使って実装します。
  • グラフの深さ優先探索は、まず行けるところまで進み、進めなくなったらバックトラックする探索方法であり、通常は再帰に基づいて実装します。
","path":["第 9 章   グラフ","9.4   まとめ"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:経路の定義は頂点列ですか、それとも辺列ですか?

Wikipedia では言語版ごとに定義が一致していません。英語版では「経路は辺の列」であり、中国語版では「経路は頂点の列」です。以下は英語版の原文です:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

本書では、経路を頂点列ではなく辺列とみなします。これは、2 つの頂点の間に複数の辺が存在する可能性があり、その場合は各辺がそれぞれ 1 本の経路に対応するためです。

Q:非連結グラフには到達できない頂点がありますか?

非連結グラフでは、ある頂点から出発すると、少なくとも 1 つの頂点には到達できません。非連結グラフ全体を走査するには、グラフ内のすべての連結成分をたどれるように複数の始点を設定する必要があります。

Q:隣接リストにおいて、「その頂点に接続されたすべての頂点」の順序に決まりはありますか?

順序は任意でかまいません。ただし実際の応用では、頂点を追加した順序や頂点値の大小順など、特定の規則に従って並べ替える必要がある場合があります。そうすることで、「ある種の極値を持つ」頂点をすばやく見つけやすくなります。

","path":["第 9 章   グラフ","9.4   まとめ"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"第 15 章   貪欲法","text":"

Abstract

ヒマワリは太陽に向かって回り、自らが最も大きく成長できる可能性を常に追い求める。

貪欲戦略は、一回ごとの単純な選択を通じて、徐々に最適な答えへと導く。

","path":["第 15 章   貪欲法"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"章の内容","text":"
  • 15.1   貪欲法
  • 15.2   分数ナップサック問題
  • 15.3   最大容量問題
  • 15.4   最大積分割問題
  • 15.5   まとめ
","path":["第 15 章   貪欲法"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   分数ナップサック問題","text":"

Question

\\(n\\) 個の品物が与えられ、第 \\(i\\) 個の品物の重さは \\(wgt[i-1]\\)、価値は \\(val[i-1]\\) であり、容量が \\(cap\\) のナップサックがある。各品物は 1 回だけ選択できるが、品物の一部を選ぶこともでき、価値は選択した重量の割合に応じて計算される。容量制限の下でナップサック内の品物の最大価値を求めよ。例を以下に示す。

図 15-3   分数ナップサック問題の例データ

分数ナップサック問題は 0-1 ナップサック問題と全体として非常によく似ており、状態には現在の品物 \\(i\\) と容量 \\(c\\) が含まれ、目標は容量制限下での最大価値を求めることである。

異なる点は、本問では品物の一部だけを選べることである。以下に示すように、品物は任意に分割でき、対応する価値は重量の割合に応じて計算される。

  1. 品物 \\(i\\) について、単位重量あたりの価値は \\(val[i-1] / wgt[i-1]\\) であり、これを単位価値と呼ぶ。
  2. 品物 \\(i\\) の一部を重さ \\(w\\) だけ入れると、ナップサックに増える価値は \\(w \\times val[i-1] / wgt[i-1]\\) となる。

図 15-4   品物の単位重量あたりの価値

","path":["第 15 章   貪欲法","15.2   分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#1","level":3,"title":"1.   貪欲戦略の決定","text":"

ナップサック内の品物の総価値を最大化することは、**本質的には単位重量あたりの品物価値を最大化すること**である。そこから、以下に示す貪欲戦略を導ける。

  1. 品物を単位価値の高い順にソートする。
  2. すべての品物を走査し、各回で単位価値が最も高い品物を貪欲に選択する。
  3. 残りのナップサック容量が足りない場合は、現在の品物の一部を使ってナップサックを満たす。

図 15-5   分数ナップサック問題の貪欲戦略

","path":["第 15 章   貪欲法","15.2   分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#2","level":3,"title":"2.   コード実装","text":"

品物を単位価値でソートできるように、Item クラスを定義する。貪欲選択を繰り返し、ナップサックが満杯になったら終了して解を返す。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fractional_knapsack.py
class Item:\n    \"\"\"品物\"\"\"\n\n    def __init__(self, w: int, v: int):\n        self.w = w  # 品物の重さ\n        self.v = v  # 品物の価値\n\ndef fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"分数ナップサック:貪欲法\"\"\"\n    # 重さと価値の 2 属性を持つ品物リストを作成\n    items = [Item(w, v) for w, v in zip(wgt, val)]\n    # 単位価値 item.v / item.w の高い順にソートする\n    items.sort(key=lambda item: item.v / item.w, reverse=True)\n    # 貪欲選択を繰り返す\n    res = 0\n    for item in items:\n        if item.w <= cap:\n            # 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v\n            cap -= item.w\n        else:\n            # 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (item.v / item.w) * cap\n            # 残り容量がないため、ループを抜ける\n            break\n    return res\n
fractional_knapsack.cpp
/* 品物 */\nclass Item {\n  public:\n    int w; // 品物の重さ\n    int v; // 品物の価値\n\n    Item(int w, int v) : w(w), v(v) {\n    }\n};\n\n/* 分数ナップサック:貪欲法 */\ndouble fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    vector<Item> items;\n    for (int i = 0; i < wgt.size(); i++) {\n        items.push_back(Item(wgt[i], val[i]));\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });\n    // 貪欲選択を繰り返す\n    double res = 0;\n    for (auto &item : items) {\n        if (item.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (double)item.v / item.w * cap;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.java
/* 品物 */\nclass Item {\n    int w; // 品物の重さ\n    int v; // 品物の価値\n\n    public Item(int w, int v) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* 分数ナップサック:貪欲法 */\ndouble fractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    Item[] items = new Item[wgt.length];\n    for (int i = 0; i < wgt.length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));\n    // 貪欲選択を繰り返す\n    double res = 0;\n    for (Item item : items) {\n        if (item.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (double) item.v / item.w * cap;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.cs
/* 品物 */\nclass Item(int w, int v) {\n    public int w = w; // 品物の重さ\n    public int v = v; // 品物の価値\n}\n\n/* 分数ナップサック:貪欲法 */\ndouble FractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    Item[] items = new Item[wgt.Length];\n    for (int i = 0; i < wgt.Length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));\n    // 貪欲選択を繰り返す\n    double res = 0;\n    foreach (Item item in items) {\n        if (item.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (double)item.v / item.w * cap;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.go
/* 品物 */\ntype Item struct {\n    w int // 品物の重さ\n    v int // 品物の価値\n}\n\n/* 分数ナップサック:貪欲法 */\nfunc fractionalKnapsack(wgt []int, val []int, cap int) float64 {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    items := make([]Item, len(wgt))\n    for i := 0; i < len(wgt); i++ {\n        items[i] = Item{wgt[i], val[i]}\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    sort.Slice(items, func(i, j int) bool {\n        return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)\n    })\n    // 貪欲選択を繰り返す\n    res := 0.0\n    for _, item := range items {\n        if item.w <= cap {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += float64(item.v)\n            cap -= item.w\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += float64(item.v) / float64(item.w) * float64(cap)\n            // 残り容量がないため、ループを抜ける\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.swift
/* 品物 */\nclass Item {\n    var w: Int // 品物の重さ\n    var v: Int // 品物の価値\n\n    init(w: Int, v: Int) {\n        self.w = w\n        self.v = v\n    }\n}\n\n/* 分数ナップサック:貪欲法 */\nfunc fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    var items = zip(wgt, val).map { Item(w: $0, v: $1) }\n    // 単位価値 item.v / item.w の高い順にソートする\n    items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }\n    // 貪欲選択を繰り返す\n    var res = 0.0\n    var cap = cap\n    for item in items {\n        if item.w <= cap {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += Double(item.v)\n            cap -= item.w\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += Double(item.v) / Double(item.w) * Double(cap)\n            // 残り容量がないため、ループを抜ける\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.js
/* 品物 */\nclass Item {\n    constructor(w, v) {\n        this.w = w; // 品物の重さ\n        this.v = v; // 品物の価値\n    }\n}\n\n/* 分数ナップサック:貪欲法 */\nfunction fractionalKnapsack(wgt, val, cap) {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    const items = wgt.map((w, i) => new Item(w, val[i]));\n    // 単位価値 item.v / item.w の高い順にソートする\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // 貪欲選択を繰り返す\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (item.v / item.w) * cap;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.ts
/* 品物 */\nclass Item {\n    w: number; // 品物の重さ\n    v: number; // 品物の価値\n\n    constructor(w: number, v: number) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* 分数ナップサック:貪欲法 */\nfunction fractionalKnapsack(wgt: number[], val: number[], cap: number): number {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));\n    // 単位価値 item.v / item.w の高い順にソートする\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // 貪欲選択を繰り返す\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (item.v / item.w) * cap;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.dart
/* 品物 */\nclass Item {\n  int w; // 品物の重さ\n  int v; // 品物の価値\n\n  Item(this.w, this.v);\n}\n\n/* 分数ナップサック:貪欲法 */\ndouble fractionalKnapsack(List<int> wgt, List<int> val, int cap) {\n  // 重さと価値の 2 属性を持つ品物リストを作成\n  List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));\n  // 単位価値 item.v / item.w の高い順にソートする\n  items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));\n  // 貪欲選択を繰り返す\n  double res = 0;\n  for (Item item in items) {\n    if (item.w <= cap) {\n      // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n      res += item.v;\n      cap -= item.w;\n    } else {\n      // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n      res += item.v / item.w * cap;\n      // 残り容量がないため、ループを抜ける\n      break;\n    }\n  }\n  return res;\n}\n
fractional_knapsack.rs
/* 品物 */\nstruct Item {\n    w: i32, // 品物の重さ\n    v: i32, // 品物の価値\n}\n\nimpl Item {\n    fn new(w: i32, v: i32) -> Self {\n        Self { w, v }\n    }\n}\n\n/* 分数ナップサック:貪欲法 */\nfn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    let mut items = wgt\n        .iter()\n        .zip(val.iter())\n        .map(|(&w, &v)| Item::new(w, v))\n        .collect::<Vec<Item>>();\n    // 単位価値 item.v / item.w の高い順にソートする\n    items.sort_by(|a, b| {\n        (b.v as f64 / b.w as f64)\n            .partial_cmp(&(a.v as f64 / a.w as f64))\n            .unwrap()\n    });\n    // 貪欲選択を繰り返す\n    let mut res = 0.0;\n    for item in &items {\n        if item.w <= cap {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v as f64;\n            cap -= item.w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += item.v as f64 / item.w as f64 * cap as f64;\n            // 残り容量がないため、ループを抜ける\n            break;\n        }\n    }\n    res\n}\n
fractional_knapsack.c
/* 品物 */\ntypedef struct {\n    int w; // 品物の重さ\n    int v; // 品物の価値\n} Item;\n\n/* 分数ナップサック:貪欲法 */\nfloat fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    Item *items = malloc(sizeof(Item) * itemCount);\n    for (int i = 0; i < itemCount; i++) {\n        items[i] = (Item){.w = wgt[i], .v = val[i]};\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);\n    // 貪欲選択を繰り返す\n    float res = 0.0;\n    for (int i = 0; i < itemCount; i++) {\n        if (items[i].w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += items[i].v;\n            cap -= items[i].w;\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += (float)cap / items[i].w * items[i].v;\n            cap = 0;\n            break;\n        }\n    }\n    free(items);\n    return res;\n}\n
fractional_knapsack.kt
/* 品物 */\nclass Item(\n    val w: Int, // 品物\n    val v: Int  // 品物の価値\n)\n\n/* 分数ナップサック:貪欲法 */\nfun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {\n    // 重さと価値の 2 属性を持つ品物リストを作成\n    var cap = c\n    val items = arrayOfNulls<Item>(wgt.size)\n    for (i in wgt.indices) {\n        items[i] = Item(wgt[i], _val[i])\n    }\n    // 単位価値 item.v / item.w の高い順にソートする\n    items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }\n    // 貪欲選択を繰り返す\n    var res = 0.0\n    for (item in items) {\n        if (item!!.w <= cap) {\n            // 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n            res += item.v\n            cap -= item.w\n        } else {\n            // 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n            res += item.v.toDouble() / item.w * cap\n            // 残り容量がないため、ループを抜ける\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.rb
### アイテム ###\nclass Item\n  attr_accessor :w # 品物の重さ\n  attr_accessor :v # 品物の価値\n\n  def initialize(w, v)\n    @w = w\n    @v = v\n  end\nend\n\n### 分数ナップサック:貪欲法 ###\ndef fractional_knapsack(wgt, val, cap)\n  # 重さと価値の 2 属性を持つ品物リストを作成する\n  items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }\n  # 単位価値 item.v / item.w の高い順にソートする\n  items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }\n  # 貪欲選択を繰り返す\n  res = 0\n  for item in items\n    if item.w <= cap\n      # 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる\n      res += item.v\n      cap -= item.w\n    else\n      # 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる\n      res += (item.v.to_f / item.w) * cap\n      # 残り容量がないため、ループを抜ける\n      break\n    end\n  end\n  res\nend\n
コードの可視化

全画面で見る >

組み込みのソートアルゴリズムの時間計算量は通常 \\(O(\\log n)\\)、空間計算量は通常 \\(O(\\log n)\\) または \\(O(n)\\) であり、具体的な値はプログラミング言語の実装に依存する。

ソートを除けば、最悪の場合は品物リスト全体を走査する必要があるため、時間計算量は \\(O(n)\\) であり、ここで \\(n\\) は品物数である。

Item オブジェクトのリストを初期化しているため、空間計算量は \\(O(n)\\) である。

","path":["第 15 章   貪欲法","15.2   分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#3","level":3,"title":"3.   正しさの証明","text":"

背理法を用いる。品物 \\(x\\) が単位価値最大の品物であり、あるアルゴリズムで得られた最大価値を res とするが、その解には品物 \\(x\\) が含まれていないと仮定する。

ここでナップサックから単位重量の任意の品物を取り出し、単位重量の品物 \\(x\\) に置き換える。品物 \\(x\\) の単位価値が最大であるため、置き換え後の総価値は必ず res より大きくなる。これは res が最適解であることに矛盾し、最適解には必ず品物 \\(x\\) が含まれなければならないことを示す。

この解に含まれる他の品物についても、同様の矛盾を構成できる。要するに、単位価値がより大きい品物は常により良い選択である。これは貪欲戦略が有効であることを示している。

以下に示すように、品物の重さと品物の単位価値をそれぞれ二次元グラフの横軸と縦軸とみなすと、分数ナップサック問題は「有限な横軸区間で囲まれる最大面積を求める問題」に変換できる。この類比は、幾何学的な観点から貪欲戦略の有効性を理解する助けになる。

図 15-6   分数ナップサック問題の幾何学的表現

","path":["第 15 章   貪欲法","15.2   分数ナップサック問題"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/","level":1,"title":"15.1   貪欲法","text":"

貪欲法(greedy algorithm)は、最適化問題を解くための一般的なアルゴリズムです。その基本的な考え方は、問題の各意思決定段階において、その時点で最善に見える選択を行い、すなわち貪欲に局所最適な決定を下すことで、大域最適解を得ようとするものです。貪欲法は簡潔で効率的であり、多くの実際の問題で広く用いられています。

貪欲法と動的計画法は、どちらも最適化問題を解く際によく用いられます。両者には、最適部分構造に依存するなどの共通点がありますが、その動作原理は異なります。

  • 動的計画法は、前の段階までのすべての決定に基づいて現在の決定を考え、過去の部分問題の解を用いて現在の部分問題の解を構築します。
  • 貪欲法は過去の決定を考慮せず、ひたすら前に進みながら貪欲な選択を行い、問題の範囲を縮小し続けて、最終的に問題を解決します。

まずは例題「コイン両替」を通して、貪欲法の仕組みを理解しましょう。この問題はすでに「完全ナップサック問題」の節で紹介しているので、見覚えがあるはずです。

Question

\\(n\\) 種類の硬貨が与えられ、\\(i\\) 番目の硬貨の額面は \\(coins[i - 1]\\) 、目標金額は \\(amt\\) です。各硬貨は何度でも選べるとき、目標金額を作るために必要な最小の硬貨枚数を求めてください。目標金額を作れない場合は \\(-1\\) を返します。

この問題で採用する貪欲戦略は下図のとおりです。目標金額が与えられたら、それを超えず、かつ最も近い硬貨を貪欲に選択し、この手順を目標金額を作り切るまで繰り返します。

図 15-1   コイン両替の貪欲戦略

実装コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_greedy.py
def coin_change_greedy(coins: list[int], amt: int) -> int:\n    \"\"\"コイン交換:貪欲法\"\"\"\n    # coins リストはソート済みと仮定する\n    i = len(coins) - 1\n    count = 0\n    # 残額がなくなるまで貪欲選択を繰り返す\n    while amt > 0:\n        # 残額以下で最も近い硬貨を見つける\n        while i > 0 and coins[i] > amt:\n            i -= 1\n        # coins[i] を選択する\n        amt -= coins[i]\n        count += 1\n    # 実行可能な解が見つからなければ -1 を返す\n    return count if amt == 0 else -1\n
coin_change_greedy.cpp
/* コイン交換:貪欲法 */\nint coinChangeGreedy(vector<int> &coins, int amt) {\n    // coins リストはソート済みと仮定する\n    int i = coins.size() - 1;\n    int count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.java
/* コイン交換:貪欲法 */\nint coinChangeGreedy(int[] coins, int amt) {\n    // coins リストはソート済みと仮定する\n    int i = coins.length - 1;\n    int count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.cs
/* コイン交換:貪欲法 */\nint CoinChangeGreedy(int[] coins, int amt) {\n    // coins リストはソート済みと仮定する\n    int i = coins.Length - 1;\n    int count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.go
/* コイン交換:貪欲法 */\nfunc coinChangeGreedy(coins []int, amt int) int {\n    // coins リストはソート済みと仮定する\n    i := len(coins) - 1\n    count := 0\n    // 残額がなくなるまで貪欲選択を繰り返す\n    for amt > 0 {\n        // 残額以下で最も近い硬貨を見つける\n        for i > 0 && coins[i] > amt {\n            i--\n        }\n        // coins[i] を選択する\n        amt -= coins[i]\n        count++\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    if amt != 0 {\n        return -1\n    }\n    return count\n}\n
coin_change_greedy.swift
/* コイン交換:貪欲法 */\nfunc coinChangeGreedy(coins: [Int], amt: Int) -> Int {\n    // coins リストはソート済みと仮定する\n    var i = coins.count - 1\n    var count = 0\n    var amt = amt\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while amt > 0 {\n        // 残額以下で最も近い硬貨を見つける\n        while i > 0 && coins[i] > amt {\n            i -= 1\n        }\n        // coins[i] を選択する\n        amt -= coins[i]\n        count += 1\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt == 0 ? count : -1\n}\n
coin_change_greedy.js
/* コイン交換:貪欲法 */\nfunction coinChangeGreedy(coins, amt) {\n    // coins 配列はソート済みと仮定する\n    let i = coins.length - 1;\n    let count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt === 0 ? count : -1;\n}\n
coin_change_greedy.ts
/* コイン交換:貪欲法 */\nfunction coinChangeGreedy(coins: number[], amt: number): number {\n    // coins 配列はソート済みと仮定する\n    let i = coins.length - 1;\n    let count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt === 0 ? count : -1;\n}\n
coin_change_greedy.dart
/* コイン交換:貪欲法 */\nint coinChangeGreedy(List<int> coins, int amt) {\n  // coins リストはソート済みと仮定する\n  int i = coins.length - 1;\n  int count = 0;\n  // 残額がなくなるまで貪欲選択を繰り返す\n  while (amt > 0) {\n    // 残額以下で最も近い硬貨を見つける\n    while (i > 0 && coins[i] > amt) {\n      i--;\n    }\n    // coins[i] を選択する\n    amt -= coins[i];\n    count++;\n  }\n  // 実行可能な解が見つからなければ -1 を返す\n  return amt == 0 ? count : -1;\n}\n
coin_change_greedy.rs
/* コイン交換:貪欲法 */\nfn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {\n    // coins リストはソート済みと仮定する\n    let mut i = coins.len() - 1;\n    let mut count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while amt > 0 {\n        // 残額以下で最も近い硬貨を見つける\n        while i > 0 && coins[i] > amt {\n            i -= 1;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count += 1;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    if amt == 0 {\n        count\n    } else {\n        -1\n    }\n}\n
coin_change_greedy.c
/* コイン交換:貪欲法 */\nint coinChangeGreedy(int *coins, int size, int amt) {\n    // coins リストはソート済みと仮定する\n    int i = size - 1;\n    int count = 0;\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (amt > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // coins[i] を選択する\n        amt -= coins[i];\n        count++;\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.kt
/* コイン交換:貪欲法 */\nfun coinChangeGreedy(coins: IntArray, amt: Int): Int {\n    // coins リストはソート済みと仮定する\n    var am = amt\n    var i = coins.size - 1\n    var count = 0\n    // 残額がなくなるまで貪欲選択を繰り返す\n    while (am > 0) {\n        // 残額以下で最も近い硬貨を見つける\n        while (i > 0 && coins[i] > am) {\n            i--\n        }\n        // coins[i] を選択する\n        am -= coins[i]\n        count++\n    }\n    // 実行可能な解が見つからなければ -1 を返す\n    return if (am == 0) count else -1\n}\n
coin_change_greedy.rb
### コイン両替:貪欲法 ###\ndef coin_change_greedy(coins, amt)\n  # coins リストはソート済みと仮定する\n  i = coins.length - 1\n  count = 0\n  # 残額がなくなるまで貪欲選択を繰り返す\n  while amt > 0\n    # 残額以下で最も近い硬貨を見つける\n    while i > 0 && coins[i] > amt\n      i -= 1\n    end\n    # coins[i] を選択する\n    amt -= coins[i]\n    count += 1\n  end\n  # 実行可能な解が見つからなければ `-1` を返す\n  amt == 0 ? count : -1\nend\n
コードの可視化

全画面で見る >

思わずこう言いたくなるかもしれません。So clean!貪欲法はわずか十行ほどのコードでコイン両替問題を解いてしまいます。

","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1511","level":2,"title":"15.1.1   貪欲法の利点と限界","text":"

**貪欲法は操作が直接的で実装が簡単なだけでなく、通常は効率も高い**です。上のコードでは、硬貨の最小額面を \\(\\min(coins)\\) とすると、貪欲選択のループ回数は高々 \\(amt / \\min(coins)\\) 回であり、時間計算量は \\(O(amt / \\min(coins))\\) です。これは動的計画法による解法の時間計算量 \\(O(n \\times amt)\\) より 1 桁小さいオーダーです。

しかし、硬貨の額面の組み合わせによっては、貪欲法では最適解を見つけられません。下図に 2 つの例を示します。

  • 正例 \\(coins = [1, 5, 10, 20, 50, 100]\\):この硬貨の組み合わせでは、任意の \\(amt\\) に対して貪欲法で最適解を見つけられます。
  • 反例 \\(coins = [1, 20, 50]\\):\\(amt = 60\\) とすると、貪欲法では \\(50 + 1 \\times 10\\) という両替しか見つからず、硬貨は合計 \\(11\\) 枚になります。しかし動的計画法なら最適解 \\(20 + 20 + 20\\) を見つけられ、必要なのはわずか \\(3\\) 枚です。
  • 反例 \\(coins = [1, 49, 50]\\):\\(amt = 98\\) とすると、貪欲法では \\(50 + 1 \\times 48\\) という両替しか見つからず、硬貨は合計 \\(49\\) 枚になります。しかし動的計画法なら最適解 \\(49 + 49\\) を見つけられ、必要なのはわずか \\(2\\) 枚です。

図 15-2   貪欲法では最適解を見つけられない例

つまり、コイン両替問題に対して、貪欲法は大域最適解を保証できず、非常に悪い解を見つけてしまうこともあります。この問題は動的計画法で解くほうが適しています。

一般に、貪欲法が適用できる状況は次の 2 つに分けられます。

  1. 最適解を保証できる場合:この場合、貪欲法はしばしば最良の選択です。多くの場合、バックトラッキングや動的計画法より効率的だからです。
  2. 近似最適解を見つけられる場合:この場合も貪欲法は有効です。多くの複雑な問題では、大域最適解を求めること自体が非常に難しく、より高い効率で準最適解を得られるだけでも十分価値があります。
","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512","level":2,"title":"15.1.2   貪欲法の特性","text":"

では、どのような問題が貪欲法に適しているのでしょうか。言い換えると、貪欲法はどのような場合に最適解を保証できるのでしょうか。

動的計画法と比べると、貪欲法の適用条件はより厳しく、主に次の 2 つの性質に注目します。

  • 貪欲選択性:局所最適な選択が常に大域最適解につながる場合にのみ、貪欲法は最適解を保証できます。
  • 最適部分構造:元の問題の最適解が、部分問題の最適解を含むことです。

最適部分構造については「動的計画法」の節ですでに紹介したので、ここでは繰り返しません。なお、問題によっては最適部分構造が明確でなくても、貪欲法で解ける場合があります。

ここでは主に、貪欲選択性をどのように判定するかを考えます。説明だけを見ると単純そうですが、実際には多くの問題で、貪欲選択性を証明するのは容易ではありません。

たとえばコイン両替問題では、反例を挙げて貪欲選択性が成り立たないことを示すのは簡単ですが、成り立つことを証明するのは難しいです。もし、**どのような条件を満たす硬貨の組み合わせなら貪欲法で解けるのか**と問われると、直感や例示に頼った曖昧な答えしか出せず、厳密な数学的証明を与えるのは困難です。

Quote

ある論文では、ある硬貨の組み合わせについて、任意の金額に対する最適解を貪欲法で求められるかどうかを判定する、時間計算量 \\(O(n^3)\\) のアルゴリズムが示されています。

Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.

","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1513","level":2,"title":"15.1.3   貪欲法の問題解決手順","text":"

貪欲法による問題解決の流れは、おおむね次の 3 段階に分けられます。

  1. 問題分析:状態の定義、最適化目標、制約条件などを整理し、問題の性質を理解します。この段階はバックトラッキングや動的計画法でも共通して現れます。
  2. 貪欲戦略の決定:各ステップでどのように貪欲選択を行うかを定めます。この戦略により各ステップで問題規模を縮小し、最終的に問題全体を解決します。
  3. 正しさの証明:通常は、その問題が貪欲選択性と最適部分構造を持つことを示す必要があります。この段階では、帰納法や背理法などの数学的証明が必要になることがあります。

貪欲戦略を定めることは問題解決の核心ですが、実際には簡単ではないことも多く、主な理由は次のとおりです。

  • 問題ごとに貪欲戦略の差が大きい。多くの問題では貪欲戦略は比較的わかりやすく、おおまかな考察や試行だけで見つけられます。しかし複雑な問題では、貪欲戦略が非常に見えにくいことがあり、その場合は解法経験やアルゴリズム力が大きく問われます。
  • 一見もっともらしい貪欲戦略もある。自信を持って貪欲戦略を設計し、コードを書いて提出しても、一部のテストケースを通過できないことがあります。これは、その貪欲戦略が「部分的にしか正しくない」ためであり、先ほどのコイン両替は典型例です。

正しさを保証するためには、貪欲戦略に対して厳密な数学的証明を行うべきであり、通常は背理法や数学的帰納法が必要になります。

しかし、正しさの証明もまた簡単とは限りません。手がかりがない場合には、テストケースを使ってコードをデバッグしながら、貪欲戦略を少しずつ修正して検証していくことがよくあります。

","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514","level":2,"title":"15.1.4   貪欲法の典型問題","text":"

貪欲法は、貪欲選択性と最適部分構造を満たす最適化問題によく用いられます。以下に典型的な貪欲法の問題をいくつか挙げます。

  • 硬貨のお釣り問題:ある種の硬貨の組み合わせでは、貪欲法で常に最適解が得られます。
  • 区間スケジューリング問題:いくつかのタスクがあり、それぞれがある時間区間で実行されるとします。できるだけ多くのタスクを完了することが目標で、毎回終了時刻が最も早いタスクを選ぶなら、貪欲法で最適解を得られます。
  • 分数ナップサック問題:一群の品物と積載容量が与えられたとき、総重量が容量を超えず、かつ総価値が最大になるように品物を選ぶ問題です。毎回、価値対重量比(価値 / 重量)が最も高い品物を選ぶなら、ある条件下で貪欲法は最適解を得られます。
  • 株式売買問題:株価の履歴が与えられ、複数回の売買が可能ですが、すでに株を保有している場合は売却前に再度購入することはできません。目標は最大利益を得ることです。
  • ハフマン符号化:ハフマン符号化は、可逆データ圧縮に用いられる貪欲法です。ハフマン木を構築する際、毎回出現頻度が最も低い 2 つのノードを選んで併合すると、最終的に得られるハフマン木の重み付きパス長(符号長)は最小になります。
  • Dijkstra アルゴリズム:与えられた始点から他の各頂点への最短経路問題を解く貪欲法です。
","path":["第 15 章   貪欲法","15.1   貪欲法"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   最大容量問題","text":"

Question

配列 \\(ht\\) が与えられ、各要素は垂直な仕切り板の高さを表します。配列内の任意の 2 枚の仕切り板と、その間の空間で容器を構成できます。

容器の容量は高さと幅の積(面積)に等しく、高さは短い方の仕切り板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。

配列から 2 枚の仕切り板を選び、構成される容器の容量が最大となるようにしてください。最大容量を返します。例を以下の図に示します。

図 15-7   最大容量問題のサンプルデータ

容器は任意の 2 枚の仕切り板で囲まれるため、本問の状態は 2 枚の仕切り板のインデックスで表され、\\([i, j]\\) と記します。

問題の条件より、容量は高さと幅の積に等しく、高さは短い板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。容量を \\(cap[i, j]\\) とすると、計算式は次のようになります。

\\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\]

配列の長さを \\(n\\) とすると、2 枚の仕切り板の組合せ数(状態総数)は \\(C_n^2 = \\frac{n(n - 1)}{2}\\) 個です。最も直接的には、すべての状態を総当たりできます。これにより最大容量を求められ、時間計算量は \\(O(n^2)\\) です。

","path":["第 15 章   貪欲法","15.3   最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#1","level":3,"title":"1.   貪欲戦略の決定","text":"

この問題にはさらに効率的な解法があります。以下の図のように、状態 \\([i, j]\\) を 1 つ選び、インデックスが \\(i < j\\) かつ高さが \\(ht[i] < ht[j]\\) を満たすとします。つまり、\\(i\\) が短い板、\\(j\\) が長い板です。

図 15-8   初期状態

以下の図のように、このとき長い板 \\(j\\) を短い板 \\(i\\) に近づけると、容量は必ず小さくなります。

これは、長い板 \\(j\\) を動かした後は幅 \\(j-i\\) が必ず小さくなるためです。また、高さは短い板で決まるので、高さは変わらない( \\(i\\) が依然として短い板)か、小さくなる(移動後の \\(j\\) が短い板になる)ことしかありません。

図 15-9   長い板を内側へ動かした後の状態

逆に考えると、短い板 \\(i\\) を内側へ縮めた場合にのみ、容量が大きくなる可能性があります。幅は必ず小さくなりますが、**高さは大きくなる可能性がある**からです(移動後の短い板 \\(i\\) がより長くなる可能性があります)。たとえば次の図では、短い板を動かした後に面積が大きくなっています。

図 15-10   短い板を内側へ動かした後の状態

以上から、本問の貪欲戦略を導けます。2 本のポインタを初期化して容器の両端に置き、各ラウンドで短い板に対応するポインタを内側へ縮め、2 本のポインタが出会うまで続けます。

以下の図は、貪欲戦略の実行過程を示しています。

  1. 初期状態では、ポインタ \\(i\\) と \\(j\\) は配列の両端にあります。
  2. 現在の状態の容量 \\(cap[i, j]\\) を計算し、最大容量を更新します。
  3. 板 \\(i\\) と板 \\(j\\) の高さを比較し、短い板を内側へ 1 マス移動します。
  4. 2.3. を繰り返し実行し、\\(i\\) と \\(j\\) が出会ったら終了します。
<1><2><3><4><5><6><7><8><9>

図 15-11   最大容量問題の貪欲な過程

","path":["第 15 章   貪欲法","15.3   最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#2","level":3,"title":"2.   コード実装","text":"

コードのループ回数は最大でも \\(n\\) 回であるため、時間計算量は \\(O(n)\\) です。

変数 \\(i\\)、\\(j\\)、\\(res\\) が使う追加領域は定数サイズなので、空間計算量は \\(O(1)\\) です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_capacity.py
def max_capacity(ht: list[int]) -> int:\n    \"\"\"最大容量:貪欲法\"\"\"\n    # i, j を初期化し、それぞれ配列の両端に置く\n    i, j = 0, len(ht) - 1\n    # 初期の最大容量は 0\n    res = 0\n    # 2 枚の板が出会うまで貪欲選択を繰り返す\n    while i < j:\n        # 最大容量を更新する\n        cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        # 短い方を内側へ動かす\n        if ht[i] < ht[j]:\n            i += 1\n        else:\n            j -= 1\n    return res\n
max_capacity.cpp
/* 最大容量:貪欲法 */\nint maxCapacity(vector<int> &ht) {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    int i = 0, j = ht.size() - 1;\n    // 初期の最大容量は 0\n    int res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        int cap = min(ht[i], ht[j]) * (j - i);\n        res = max(res, cap);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.java
/* 最大容量:貪欲法 */\nint maxCapacity(int[] ht) {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    int i = 0, j = ht.length - 1;\n    // 初期の最大容量は 0\n    int res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        int cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.cs
/* 最大容量:貪欲法 */\nint MaxCapacity(int[] ht) {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    int i = 0, j = ht.Length - 1;\n    // 初期の最大容量は 0\n    int res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        int cap = Math.Min(ht[i], ht[j]) * (j - i);\n        res = Math.Max(res, cap);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.go
/* 最大容量:貪欲法 */\nfunc maxCapacity(ht []int) int {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    i, j := 0, len(ht)-1\n    // 初期の最大容量は 0\n    res := 0\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    for i < j {\n        // 最大容量を更新する\n        capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)\n        res = int(math.Max(float64(res), float64(capacity)))\n        // 短い方を内側へ動かす\n        if ht[i] < ht[j] {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
max_capacity.swift
/* 最大容量:貪欲法 */\nfunc maxCapacity(ht: [Int]) -> Int {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    var i = ht.startIndex, j = ht.endIndex - 1\n    // 初期の最大容量は 0\n    var res = 0\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while i < j {\n        // 最大容量を更新する\n        let cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // 短い方を内側へ動かす\n        if ht[i] < ht[j] {\n            i += 1\n        } else {\n            j -= 1\n        }\n    }\n    return res\n}\n
max_capacity.js
/* 最大容量:貪欲法 */\nfunction maxCapacity(ht) {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    let i = 0,\n        j = ht.length - 1;\n    // 初期の最大容量は 0\n    let res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        const cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
max_capacity.ts
/* 最大容量:貪欲法 */\nfunction maxCapacity(ht: number[]): number {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    let i = 0,\n        j = ht.length - 1;\n    // 初期の最大容量は 0\n    let res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        const cap: number = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
max_capacity.dart
/* 最大容量:貪欲法 */\nint maxCapacity(List<int> ht) {\n  // i, j を初期化し、それぞれ配列の両端に置く\n  int i = 0, j = ht.length - 1;\n  // 初期の最大容量は 0\n  int res = 0;\n  // 2 枚の板が出会うまで貪欲選択を繰り返す\n  while (i < j) {\n    // 最大容量を更新する\n    int cap = min(ht[i], ht[j]) * (j - i);\n    res = max(res, cap);\n    // 短い方を内側へ動かす\n    if (ht[i] < ht[j]) {\n      i++;\n    } else {\n      j--;\n    }\n  }\n  return res;\n}\n
max_capacity.rs
/* 最大容量:貪欲法 */\nfn max_capacity(ht: &[i32]) -> i32 {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    let mut i = 0;\n    let mut j = ht.len() - 1;\n    // 初期の最大容量は 0\n    let mut res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while i < j {\n        // 最大容量を更新する\n        let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;\n        res = std::cmp::max(res, cap);\n        // 短い方を内側へ動かす\n        if ht[i] < ht[j] {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    res\n}\n
max_capacity.c
/* 最大容量:貪欲法 */\nint maxCapacity(int ht[], int htLength) {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    int i = 0;\n    int j = htLength - 1;\n    // 初期の最大容量は 0\n    int res = 0;\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        int capacity = myMin(ht[i], ht[j]) * (j - i);\n        res = myMax(res, capacity);\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.kt
/* 最大容量:貪欲法 */\nfun maxCapacity(ht: IntArray): Int {\n    // i, j を初期化し、それぞれ配列の両端に置く\n    var i = 0\n    var j = ht.size - 1\n    // 初期の最大容量は 0\n    var res = 0\n    // 2 枚の板が出会うまで貪欲選択を繰り返す\n    while (i < j) {\n        // 最大容量を更新する\n        val cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // 短い方を内側へ動かす\n        if (ht[i] < ht[j]) {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
max_capacity.rb
### 最大容量:貪欲法 ###\ndef max_capacity(ht)\n  # i, j を初期化し、それぞれ配列の両端に置く\n  i, j = 0, ht.length - 1\n  # 初期の最大容量は 0\n  res = 0\n\n  # 2 枚の板が出会うまで貪欲選択を繰り返す\n  while i < j\n    # 最大容量を更新する\n    cap = [ht[i], ht[j]].min * (j - i)\n    res = [res, cap].max\n    # 短い方を内側へ動かす\n    if ht[i] < ht[j]\n      i += 1\n    else\n      j -= 1\n    end\n  end\n\n  res\nend\n
コードの可視化

全画面で見る >

","path":["第 15 章   貪欲法","15.3   最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#3","level":3,"title":"3.   正しさの証明","text":"

貪欲法が総当たりより速いのは、各ラウンドの貪欲な選択がいくつかの状態を「スキップ」するためです。

たとえば状態 \\(cap[i, j]\\) において、\\(i\\) が短い板、\\(j\\) が長い板だとします。貪欲に短い板 \\(i\\) を内側へ 1 マス動かすと、次の図に示す状態が「スキップ」されます。これは、その後それらの状態の容量を検証できないことを意味します。

\\[ cap[i, i+1], cap[i, i+2], \\dots, cap[i, j-2], cap[i, j-1] \\]

図 15-12   短い板の移動によってスキップされる状態

観察すると、これらのスキップされた状態は、実際には長い板 \\(j\\) を内側へ動かしたすべての状態そのものです。前述のとおり、長い板を内側へ動かすと容量は必ず小さくなります。つまり、スキップされた状態はいずれも最適解にはなりえず、それらを飛ばしても最適解を逃すことはありません。

以上の分析から、短い板を動かす操作は「安全」であり、貪欲戦略は有効であると分かります。

","path":["第 15 章   貪欲法","15.3   最大容量問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/","level":1,"title":"15.4   最大積分割問題","text":"

Question

正整数 \\(n\\) が与えられたとき、それを少なくとも 2 つの正整数の和に分割し、分割後のすべての整数の積の最大値を求めよ。下図に示す。

図 15-13   最大積分割問題の定義

仮に \\(n\\) を \\(m\\) 個の整数因子に分割し、そのうち第 \\(i\\) 個の因子を \\(n_i\\) と記すと、

\\[ n = \\sum_{i=1}^{m}n_i \\]

本問題の目的は、すべての整数因子の積の最大値を求めることであり、すなわち

\\[ \\max(\\prod_{i=1}^{m}n_i) \\]

考えるべきことは、分割数 \\(m\\) をいくつにすべきか、各 \\(n_i\\) をいくつにすべきかである。

","path":["第 15 章   貪欲法","15.4   最大積分割問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#1","level":3,"title":"1.   貪欲戦略の決定","text":"

経験的に、2 つの整数の積はその和より大きくなることが多い。\\(n\\) から因子 \\(2\\) を 1 つ切り出すと、それらの積は \\(2(n-2)\\) となる。この積を \\(n\\) と比較すると、

\\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\]

下図のように、\\(n \\geq 4\\) のとき、\\(2\\) を 1 つ切り出すと積は大きくなる。これは、\\(4\\) 以上の整数はすべて分割すべきことを意味する。

貪欲戦略一:分割方法に \\(\\geq 4\\) の因子が含まれるなら、それはさらに分割すべきである。最終的な分割方法に現れる因子は \\(1\\)、\\(2\\)、\\(3\\) の 3 種類だけである。

図 15-14   分割により積が大きくなる

次に、どの因子が最適かを考える。\\(1\\)、\\(2\\)、\\(3\\) の 3 つの因子のうち、明らかに \\(1\\) が最も悪い。なぜなら \\(1 \\times (n-1) < n\\) は常に成り立ち、\\(1\\) を切り出すとかえって積が小さくなるからである。

下図のように、\\(n = 6\\) のとき、\\(3 \\times 3 > 2 \\times 2 \\times 2\\) が成り立つ。これは、\\(2\\) を切り出すより \\(3\\) を切り出すほうが有利であることを意味する。

貪欲戦略二:分割方法の中に存在してよい \\(2\\) は高々 2 つである。なぜなら、3 つの \\(2\\) は常に 2 つの \\(3\\) に置き換えられ、より大きな積を得られるからである。

図 15-15   最適な分割因子

以上より、次の貪欲戦略が導かれる。

  1. 整数 \\(n\\) を入力し、余りが \\(0\\)、\\(1\\)、\\(2\\) になるまで、そこから因子 \\(3\\) を繰り返し切り出す。
  2. 余りが \\(0\\) のとき、\\(n\\) は \\(3\\) の倍数であることを表すため、何も処理しない。
  3. 余りが \\(2\\) のときは、それ以上分割せず、そのまま残す。
  4. 余りが \\(1\\) のとき、\\(2 \\times 2 > 1 \\times 3\\) であるため、最後の \\(3\\) を \\(2\\) に置き換えるべきである。
","path":["第 15 章   貪欲法","15.4   最大積分割問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#2","level":3,"title":"2.   コード実装","text":"

下図のように、ループで整数を分割する必要はなく、切り捨て除算によって \\(3\\) の個数 \\(a\\) を、剰余演算によって余り \\(b\\) を得られる。このとき、

\\[ n = 3 a + b \\]

なお、\\(n \\leq 3\\) の境界ケースでは、必ず \\(1\\) を 1 つ分割する必要があり、積は \\(1 \\times (n - 1)\\) となる。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_product_cutting.py
def max_product_cutting(n: int) -> int:\n    \"\"\"最大切断積:貪欲法\"\"\"\n    # n <= 3 のときは、必ず 1 を切り出す\n    if n <= 3:\n        return 1 * (n - 1)\n    # 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    a, b = n // 3, n % 3\n    if b == 1:\n        # 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return int(math.pow(3, a - 1)) * 2 * 2\n    if b == 2:\n        # 余りが 2 のときは、そのままにする\n        return int(math.pow(3, a)) * 2\n    # 余りが 0 のときは、そのままにする\n    return int(math.pow(3, a))\n
max_product_cutting.cpp
/* 最大切断積:貪欲法 */\nint maxProductCutting(int n) {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return (int)pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 余りが 2 のときは、そのままにする\n        return (int)pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return (int)pow(3, a);\n}\n
max_product_cutting.java
/* 最大切断積:貪欲法 */\nint maxProductCutting(int n) {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return (int) Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 余りが 2 のときは、そのままにする\n        return (int) Math.pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return (int) Math.pow(3, a);\n}\n
max_product_cutting.cs
/* 最大切断積:貪欲法 */\nint MaxProductCutting(int n) {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return (int)Math.Pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 余りが 2 のときは、そのままにする\n        return (int)Math.Pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return (int)Math.Pow(3, a);\n}\n
max_product_cutting.go
/* 最大切断積:貪欲法 */\nfunc maxProductCutting(n int) int {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    a := n / 3\n    b := n % 3\n    if b == 1 {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return int(math.Pow(3, float64(a-1))) * 2 * 2\n    }\n    if b == 2 {\n        // 余りが 2 のときは、そのままにする\n        return int(math.Pow(3, float64(a))) * 2\n    }\n    // 余りが 0 のときは、そのままにする\n    return int(math.Pow(3, float64(a)))\n}\n
max_product_cutting.swift
/* 最大切断積:貪欲法 */\nfunc maxProductCutting(n: Int) -> Int {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    let a = n / 3\n    let b = n % 3\n    if b == 1 {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return pow(3, a - 1) * 2 * 2\n    }\n    if b == 2 {\n        // 余りが 2 のときは、そのままにする\n        return pow(3, a) * 2\n    }\n    // 余りが 0 のときは、そのままにする\n    return pow(3, a)\n}\n
max_product_cutting.js
/* 最大切断積:貪欲法 */\nfunction maxProductCutting(n) {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    let a = Math.floor(n / 3);\n    let b = n % 3;\n    if (b === 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // 余りが 2 のときは、そのままにする\n        return Math.pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return Math.pow(3, a);\n}\n
max_product_cutting.ts
/* 最大切断積:貪欲法 */\nfunction maxProductCutting(n: number): number {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    let a: number = Math.floor(n / 3);\n    let b: number = n % 3;\n    if (b === 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // 余りが 2 のときは、そのままにする\n        return Math.pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return Math.pow(3, a);\n}\n
max_product_cutting.dart
/* 最大切断積:貪欲法 */\nint maxProductCutting(int n) {\n  // n <= 3 のときは、必ず 1 を切り出す\n  if (n <= 3) {\n    return 1 * (n - 1);\n  }\n  // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n  int a = n ~/ 3;\n  int b = n % 3;\n  if (b == 1) {\n    // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n    return (pow(3, a - 1) * 2 * 2).toInt();\n  }\n  if (b == 2) {\n    // 余りが 2 のときは、そのままにする\n    return (pow(3, a) * 2).toInt();\n  }\n  // 余りが 0 のときは、そのままにする\n  return pow(3, a).toInt();\n}\n
max_product_cutting.rs
/* 最大切断積:貪欲法 */\nfn max_product_cutting(n: i32) -> i32 {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if n <= 3 {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    let a = n / 3;\n    let b = n % 3;\n    if b == 1 {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        3_i32.pow(a as u32 - 1) * 2 * 2\n    } else if b == 2 {\n        // 余りが 2 のときは、そのままにする\n        3_i32.pow(a as u32) * 2\n    } else {\n        // 余りが 0 のときは、そのままにする\n        3_i32.pow(a as u32)\n    }\n}\n
max_product_cutting.c
/* 最大切断積:貪欲法 */\nint maxProductCutting(int n) {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 余りが 2 のときは、そのままにする\n        return pow(3, a) * 2;\n    }\n    // 余りが 0 のときは、そのままにする\n    return pow(3, a);\n}\n
max_product_cutting.kt
/* 最大切断積:貪欲法 */\nfun maxProductCutting(n: Int): Int {\n    // n <= 3 のときは、必ず 1 を切り出す\n    if (n <= 3) {\n        return 1 * (n - 1)\n    }\n    // 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n    val a = n / 3\n    val b = n % 3\n    if (b == 1) {\n        // 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n        return 3.0.pow((a - 1)).toInt() * 2 * 2\n    }\n    if (b == 2) {\n        // 余りが 2 のときは、そのままにする\n        return 3.0.pow(a).toInt() * 2 * 2\n    }\n    // 余りが 0 のときは、そのままにする\n    return 3.0.pow(a).toInt()\n}\n
max_product_cutting.rb
### 最大分割積:貪欲法 ###\ndef max_product_cutting(n)\n  # n <= 3 のときは、必ず 1 を切り出す\n  return 1 * (n - 1) if n <= 3\n  # 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする\n  a, b = n / 3, n % 3\n  # 余りが 1 のときは、1 * 3 を 2 * 2 に変える\n  return (3.pow(a - 1) * 2 * 2).to_i if b == 1\n  # 余りが 2 のときは、そのままにする\n  return (3.pow(a) * 2).to_i if b == 2\n  # 余りが 0 のときは、そのままにする\n  3.pow(a).to_i\nend\n
コードの可視化

全画面で見る >

図 15-16   最大積分割の計算方法

時間計算量は、プログラミング言語におけるべき乗演算の実装方法に依存する。Python を例に取ると、よく使われるべき乗計算関数は 3 種類ある。

  • 演算子 ** と関数 pow() の時間計算量はいずれも \\(O(\\log⁡ a)\\) である。
  • 関数 math.pow() は内部で C 言語ライブラリの pow() 関数を呼び出し、浮動小数点のべき乗を実行するため、時間計算量は \\(O(1)\\) である。

変数 \\(a\\) と \\(b\\) が使う追加領域は定数サイズであり、したがって空間計算量は \\(O(1)\\) である。

","path":["第 15 章   貪欲法","15.4   最大積分割問題"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#3","level":3,"title":"3.   正しさの証明","text":"

背理法を用い、\\(n \\geq 4\\) の場合のみを考える。

  1. すべての因子は \\(\\leq 3\\) :最適な分割方法に \\(\\geq 4\\) の因子 \\(x\\) が存在すると仮定すると、それは必ずさらに \\(2(x-2)\\) に分割でき、より大きい(または等しい)積が得られる。これは仮定に矛盾する。
  2. 分割方法に \\(1\\) は含まれない :最適な分割方法に因子 \\(1\\) が 1 つ存在すると仮定すると、それは必ず別の因子に併合でき、より大きい積を得られる。これは仮定に矛盾する。
  3. 分割方法に含まれる \\(2\\) は高々 2 つ :最適な分割方法に 3 つの \\(2\\) が含まれると仮定すると、それは必ず 2 つの \\(3\\) に置き換えられ、積はより大きくなる。これは仮定に矛盾する。
","path":["第 15 章   貪欲法","15.4   最大積分割問題"],"tags":[]},{"location":"chapter_greedy/summary/","level":1,"title":"15.5   まとめ","text":"","path":["第 15 章   貪欲法","15.5   まとめ"],"tags":[]},{"location":"chapter_greedy/summary/#1","level":3,"title":"1.   重要な振り返り","text":"
  • 貪欲法は通常、最適化問題を解くために用いられ、その原理は各意思決定段階で局所最適な決定を行い、全体最適解を得ることを目指すというものである。
  • 貪欲法は反復的に次々と貪欲な選択を行い、各ラウンドで問題をより小さな部分問題へと変換し、最終的に問題を解決する。
  • 貪欲法は実装が簡単であるだけでなく、問題を解く効率も高い。動的計画法と比べると、貪欲法の時間計算量は通常より低い。
  • 硬貨両替問題では、ある種の硬貨の組み合わせに対しては貪欲法で最適解を保証できるが、別の組み合わせではそうではなく、非常に悪い解を見つけてしまう可能性がある。
  • 貪欲法による解法に適した問題は、貪欲選択性と最適部分構造という 2 つの性質を備えている。貪欲選択性は、貪欲戦略の有効性を表している。
  • 一部の複雑な問題では、貪欲選択性を証明するのは容易ではない。相対的には、反例による否定のほうが簡単であり、硬貨両替問題がその一例である。
  • 貪欲法の問題を解く流れは主に 3 段階に分かれる。すなわち、問題分析、貪欲戦略の決定、正しさの証明である。このうち、貪欲戦略の決定が中核であり、正しさの証明はしばしば難所となる。
  • 分数ナップサック問題は 0-1 ナップサックを基に、品物の一部を選ぶことを許しているため、貪欲法で解くことができる。貪欲戦略の正しさは背理法で証明できる。
  • 最大容量問題は全探索で解くことができ、時間計算量は \\(O(n^2)\\) である。貪欲戦略を設計し、各ラウンドで短い板を内側へ動かすことで、時間計算量を \\(O(n)\\) に最適化できる。
  • 最大分割積問題では、2 つの貪欲戦略を順に導いた。すなわち、\\(\\geq 4\\) の整数はすべてさらに分割すべきであり、最適な分割因子は \\(3\\) である。コードにはべき乗演算が含まれており、時間計算量はその実装方法に依存し、通常は \\(O(1)\\) または \\(O(\\log n)\\) である。
","path":["第 15 章   貪欲法","15.5   まとめ"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"第 6 章   ハッシュテーブル","text":"

Abstract

コンピュータの世界では、ハッシュテーブルは聡明な図書館員のような存在です。

彼は請求記号の計算方法を知っており、そのため目的の本を素早く見つけられます。

","path":["第 6 章   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"章の内容","text":"
  • 6.1   ハッシュテーブル
  • 6.2   ハッシュ衝突
  • 6.3   ハッシュアルゴリズム
  • 6.4   まとめ
","path":["第 6 章   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   ハッシュアルゴリズム","text":"

前の 2 節では、ハッシュテーブルの動作原理とハッシュ衝突の処理方法を紹介しました。しかし、オープンアドレス法であれ連鎖方式であれ、それらが保証できるのは衝突発生時でもハッシュテーブルが正常に動作することだけであり、ハッシュ衝突そのものを減らすことはできません。

ハッシュ衝突があまりにも頻繁に発生すると、ハッシュテーブルの性能は急激に劣化します。下図のように、連鎖方式のハッシュテーブルでは、理想的な場合にはキーと値のペアが各バケットに均等に分布し、最良の検索効率を達成します。最悪の場合には、すべてのキーと値のペアが同じバケットに格納され、時間計算量は \\(O(n)\\) に劣化します。

図 6-8   ハッシュ衝突の最良ケースと最悪ケース

キーと値のペアの分布はハッシュ関数によって決まります。ハッシュ関数の計算手順を思い出すと、まずハッシュ値を計算し、その後で配列長に対して剰余を取ります。

index = hash(key) % capacity\n

上の式から分かるように、ハッシュテーブルの容量 capacity が固定されているとき、出力値を決めるのはハッシュアルゴリズム hash() です。したがって、それがキーと値のペアのハッシュテーブル内での分布も決定します。

これは、ハッシュ衝突の発生確率を下げるには、ハッシュアルゴリズム hash() の設計に注目すべきだということを意味します。

","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#631","level":2,"title":"6.3.1   ハッシュアルゴリズムの目標","text":"

「高速かつ安定した」ハッシュテーブルというデータ構造を実現するために、ハッシュアルゴリズムは次の特徴を備える必要があります。

  • 決定性:同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成しなければなりません。そうして初めて、ハッシュテーブルの信頼性が保たれます。
  • 高効率:ハッシュ値の計算過程は十分に高速であるべきです。計算コストが小さいほど、ハッシュテーブルの実用性は高くなります。
  • 均一分布:ハッシュアルゴリズムは、キーと値のペアがハッシュテーブル内に均等に分布するようにすべきです。分布が均一であるほど、ハッシュ衝突の確率は低くなります。

実際には、ハッシュアルゴリズムはハッシュテーブルの実装だけでなく、ほかの多くの分野でも広く利用されています。

  • パスワード保存:ユーザーのパスワードを保護するために、システムは通常、平文パスワードを直接保存せず、そのハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力内容のハッシュ値を計算し、保存済みのハッシュ値と比較します。一致すれば、そのパスワードは正しいと見なされます。
  • データ完全性検査:送信側はデータのハッシュ値を計算してデータと一緒に送信できます。受信側は受け取ったデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。両者が一致すれば、そのデータは完全だと見なされます。

暗号分野の応用では、ハッシュ値から元のパスワードを推測するといった逆解析を防ぐために、ハッシュアルゴリズムにはさらに高いレベルの安全性が求められます。

  • 一方向性:ハッシュ値から入力データに関するいかなる情報も逆算できないこと。
  • 耐衝突性:異なる 2 つの入力で同じハッシュ値になるものを見つけることが、極めて困難であること。
  • アバランシェ効果:入力のわずかな変化が、出力の大きく予測不能な変化を引き起こすこと。

注意してほしいのは、**「均一分布」と「耐衝突性」は独立した 2 つの概念である**という点です。均一分布を満たしていても、耐衝突性を満たすとは限りません。たとえば、入力 key がランダムである場合、ハッシュ関数 key % 100 は均一に分布した出力を生成できます。しかし、このハッシュアルゴリズムはあまりにも単純で、下 2 桁が同じ key はすべて同じ出力になります。そのため、ハッシュ値から利用可能な key を容易に逆算でき、結果としてパスワードが破られてしまいます。

","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632","level":2,"title":"6.3.2   ハッシュアルゴリズムの設計","text":"

ハッシュアルゴリズムの設計は、多くの要素を考慮しなければならない複雑な問題です。しかし、要求の高くない場面であれば、いくつかの単純なハッシュアルゴリズムを設計することもできます。

  • 加算ハッシュ:入力の各文字の ASCII コードを足し合わせ、その合計をハッシュ値とします。
  • 乗算ハッシュ:乗算の非相関性を利用し、各ラウンドで定数を掛けながら、各文字の ASCII コードをハッシュ値に累積します。
  • XOR ハッシュ:入力データの各要素を XOR 演算で 1 つのハッシュ値に累積します。
  • 回転ハッシュ:各文字の ASCII コードを 1 つのハッシュ値に累積し、各回の累積前にハッシュ値を回転させます。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby simple_hash.py
def add_hash(key: str) -> int:\n    \"\"\"加算ハッシュ\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash += ord(c)\n    return hash % modulus\n\ndef mul_hash(key: str) -> int:\n    \"\"\"乗算ハッシュ\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = 31 * hash + ord(c)\n    return hash % modulus\n\ndef xor_hash(key: str) -> int:\n    \"\"\"XOR ハッシュ\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash ^= ord(c)\n    return hash % modulus\n\ndef rot_hash(key: str) -> int:\n    \"\"\"回転ハッシュ\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = (hash << 4) ^ (hash >> 28) ^ ord(c)\n    return hash % modulus\n
simple_hash.cpp
/* 加算ハッシュ */\nint addHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 乗算ハッシュ */\nint mulHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (31 * hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR ハッシュ */\nint xorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash ^= (int)c;\n    }\n    return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint rotHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n
simple_hash.java
/* 加算ハッシュ */\nint addHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* 乗算ハッシュ */\nint mulHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (31 * hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* XOR ハッシュ */\nint xorHash(String key) {\n    int hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash ^= (int) c;\n    }\n    return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint rotHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n
simple_hash.cs
/* 加算ハッシュ */\nint AddHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 乗算ハッシュ */\nint MulHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (31 * hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR ハッシュ */\nint XorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash ^= c;\n    }\n    return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint RotHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS;\n    }\n    return (int)hash;\n}\n
simple_hash.go
/* 加算ハッシュ */\nfunc addHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* 乗算ハッシュ */\nfunc mulHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (31*hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* XOR ハッシュ */\nfunc xorHash(key string) int {\n    hash := 0\n    modulus := 1000000007\n    for _, b := range []byte(key) {\n        fmt.Println(int(b))\n        hash ^= int(b)\n        hash = (31*hash + int(b)) % modulus\n    }\n    return hash & modulus\n}\n\n/* 回転ハッシュ */\nfunc rotHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus\n    }\n    return int(hash)\n}\n
simple_hash.swift
/* 加算ハッシュ */\nfunc addHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* 乗算ハッシュ */\nfunc mulHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (31 * hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* XOR ハッシュ */\nfunc xorHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash ^= Int(scalar.value)\n        }\n    }\n    return hash & MODULUS\n}\n\n/* 回転ハッシュ */\nfunc rotHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n
simple_hash.js
/* 加算ハッシュ */\nfunction addHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* 乗算ハッシュ */\nfunction mulHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR ハッシュ */\nfunction xorHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* 回転ハッシュ */\nfunction rotHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
simple_hash.ts
/* 加算ハッシュ */\nfunction addHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* 乗算ハッシュ */\nfunction mulHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* XOR ハッシュ */\nfunction xorHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* 回転ハッシュ */\nfunction rotHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
simple_hash.dart
/* 加算ハッシュ */\nint addHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* 乗算ハッシュ */\nint mulHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (31 * hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* XOR ハッシュ */\nint xorHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash ^= key.codeUnitAt(i);\n  }\n  return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint rotHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n
simple_hash.rs
/* 加算ハッシュ */\nfn add_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* 乗算ハッシュ */\nfn mul_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (31 * hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* XOR ハッシュ */\nfn xor_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash ^= c as i64;\n    }\n\n    (hash & MODULUS) as i32\n}\n\n/* 回転ハッシュ */\nfn rot_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n
simple_hash.c
/* 加算ハッシュ */\nint addHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 乗算ハッシュ */\nint mulHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (31 * hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* XOR ハッシュ */\nint xorHash(char *key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n\n    for (int i = 0; i < strlen(key); i++) {\n        hash ^= (unsigned char)key[i];\n    }\n    return hash & MODULUS;\n}\n\n/* 回転ハッシュ */\nint rotHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS;\n    }\n\n    return (int)hash;\n}\n
simple_hash.kt
/* 加算ハッシュ */\nfun addHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* 乗算ハッシュ */\nfun mulHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (31 * hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* XOR ハッシュ */\nfun xorHash(key: String): Int {\n    var hash = 0\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = hash xor c.code\n    }\n    return hash and MODULUS\n}\n\n/* 回転ハッシュ */\nfun rotHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS\n    }\n    return hash.toInt()\n}\n
simple_hash.rb
### 加算ハッシュ ###\ndef add_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash += c.ord }\n\n  hash % modulus\nend\n\n### 乗算ハッシュ ###\ndef mul_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = 31 * hash + c.ord }\n\n  hash % modulus\nend\n\n### XOR ハッシュ ###\ndef xor_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash ^= c.ord }\n\n  hash % modulus\nend\n\n### 回転ハッシュ ###\ndef rot_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord }\n\n  hash % modulus\nend\n
コードの可視化

全画面で見る >

見て分かるように、各ハッシュアルゴリズムの最後のステップでは、大きな素数 \\(1000000007\\) で剰余を取り、ハッシュ値が適切な範囲に収まるようにしています。ここで考えてみる価値があるのは、なぜ素数での剰余を強調するのか、あるいは合成数で剰余を取ることにどんな欠点があるのか、という点です。これは興味深い問題です。

先に結論を述べると、法として大きな素数を使うと、ハッシュ値が均一に分布することを最大限に保証できます。素数はほかの数と公約数を持たないため、剰余演算によって生じる周期的なパターンを減らし、ハッシュ衝突を避けやすくなります。

たとえば、法として合成数 \\(9\\) を選ぶとします。これは \\(3\\) で割り切れるため、\\(3\\) で割り切れるすべての key は、\\(0\\)、\\(3\\)、\\(6\\) の 3 つのハッシュ値に写像されます。

\\[ \\begin{aligned} \\text{modulus} & = 9 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\\dots \\} \\end{aligned} \\]

入力 key がたまたまこのような等差数列の分布をしていると、ハッシュ値に偏りが生じ、ハッシュ衝突がさらに深刻になります。そこで modulus を素数 \\(13\\) に置き換えると仮定すると、keymodulus の間に公約数が存在しないため、出力されるハッシュ値の均一性は明らかに向上します。

\\[ \\begin{aligned} \\text{modulus} & = 13 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \\dots \\} \\end{aligned} \\]

補足すると、key がランダムかつ均一に分布していると保証できるなら、法に素数を選んでも合成数を選んでも構いません。どちらでも均一に分布したハッシュ値を出力できます。しかし、key の分布に何らかの周期性がある場合、合成数で剰余を取るほうが偏りが生じやすくなります。

要するに、通常は法として素数を選び、その素数はできるだけ大きいほうが望ましいです。そうすることで周期的なパターンをできる限り取り除き、ハッシュアルゴリズムの堅牢性を高められます。

","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#633","level":2,"title":"6.3.3   一般的なハッシュアルゴリズム","text":"

上で紹介した単純なハッシュアルゴリズムは、どれも比較的「脆弱」であり、ハッシュアルゴリズムの設計目標にはほど遠いことが分かります。たとえば、加算と XOR は交換法則を満たすため、加算ハッシュと XOR ハッシュでは、内容が同じで順序だけ異なる文字列を区別できません。これはハッシュ衝突を悪化させ、一部の安全上の問題を引き起こす可能性があります。

実際には、MD5、SHA-1、SHA-2、SHA-3 などの標準的なハッシュアルゴリズムを用いることが一般的です。これらは任意長の入力データを、固定長のハッシュ値へ写像できます。

ここ 1 世紀近くの間、ハッシュアルゴリズムは継続的に改良と最適化が進められてきました。ある研究者たちは性能向上に取り組み、別の研究者やハッカーたちは安全性の弱点を探し続けてきました。次の表は、実際の応用でよく使われるハッシュアルゴリズムを示したものです。

  • MD5 と SHA-1 は何度も攻撃に成功されているため、各種のセキュリティ用途では廃止されています。
  • SHA-2 系列の SHA-256 は最も安全なハッシュアルゴリズムの 1 つであり、いまだに成功した攻撃例がないため、多くのセキュリティ用途やプロトコルで広く使われています。
  • SHA-3 は SHA-2 と比べて実装コストが低く、計算効率も高い一方で、現時点での普及度は SHA-2 系列に及びません。

表 6-2   一般的なハッシュアルゴリズム

MD5 SHA-1 SHA-2 SHA-3 発表年 1992 1995 2002 2008 出力長 128 bit 160 bit 256/512 bit 224/256/384/512 bit ハッシュ衝突 多い 多い 非常に少ない 非常に少ない セキュリティレベル 低く、攻撃に成功されている 低く、攻撃に成功されている 高い 高い 用途 廃止済みだが、データ完全性検査には使われる 廃止済み 暗号資産の取引検証、デジタル署名など SHA-2 の代替に使える","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#634","level":2,"title":"6.3.4   データ構造のハッシュ値","text":"

ご存じのように、ハッシュテーブルの key には整数、小数、文字列などのデータ型を使えます。プログラミング言語は通常、これらのデータ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュテーブル内のバケットインデックス計算に利用します。Python を例にすると、hash() 関数を呼び出して各種データ型のハッシュ値を計算できます。

  • 整数と真理値のハッシュ値は、その値自身です。
  • 浮動小数点数と文字列のハッシュ値の計算はやや複雑なので、興味がある読者は自分で調べてみてください。
  • タプルのハッシュ値は、各要素のハッシュ値を求めてから、それらを組み合わせて 1 つのハッシュ値にしたものです。
  • オブジェクトのハッシュ値は、そのメモリアドレスに基づいて生成されます。オブジェクトのハッシュメソッドをオーバーライドすれば、内容に基づくハッシュ値を実装できます。

Tip

注意してください。組み込みのハッシュ値計算関数の定義や方法は、プログラミング言語ごとに異なります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby built_in_hash.py
num = 3\nhash_num = hash(num)\n# 整数 3 のハッシュ値は 3\n\nbol = True\nhash_bol = hash(bol)\n# 真理値 True のハッシュ値は 1\n\ndec = 3.14159\nhash_dec = hash(dec)\n# 小数 3.14159 のハッシュ値は 326484311674566659\n\nstr = \"Hello アルゴリズム\"\nhash_str = hash(str)\n# 文字列「Hello アルゴリズム」のハッシュ値は 4617003410720528961\n\ntup = (12836, \"シャオハ\")\nhash_tup = hash(tup)\n# タプル (12836, 'シャオハ') のハッシュ値は 1029005403108185979\n\nobj = ListNode(0)\nhash_obj = hash(obj)\n# ノードオブジェクト <ListNode object at 0x1058fd810> のハッシュ値は 274267521\n
built_in_hash.cpp
int num = 3;\nsize_t hashNum = hash<int>()(num);\n// 整数 3 のハッシュ値は 3\n\nbool bol = true;\nsize_t hashBol = hash<bool>()(bol);\n// 真理値 1 のハッシュ値は 1\n\ndouble dec = 3.14159;\nsize_t hashDec = hash<double>()(dec);\n// 小数 3.14159 のハッシュ値は 4614256650576692846\n\nstring str = \"Hello アルゴリズム\";\nsize_t hashStr = hash<string>()(str);\n// 文字列「Hello アルゴリズム」のハッシュ値は 15466937326284535026\n\n// C++ では、組み込みの std:hash() は基本データ型のハッシュ値計算のみを提供する\n// 配列やオブジェクトのハッシュ値計算は自分で実装する必要がある\n
built_in_hash.java
int num = 3;\nint hashNum = Integer.hashCode(num);\n// 整数 3 のハッシュ値は 3\n\nboolean bol = true;\nint hashBol = Boolean.hashCode(bol);\n// 真理値 true のハッシュ値は 1231\n\ndouble dec = 3.14159;\nint hashDec = Double.hashCode(dec);\n// 小数 3.14159 のハッシュ値は -1340954729\n\nString str = \"Hello アルゴリズム\";\nint hashStr = str.hashCode();\n// 文字列「Hello アルゴリズム」のハッシュ値は -727081396\n\nObject[] arr = { 12836, \"シャオハ\" };\nint hashTup = Arrays.hashCode(arr);\n// 配列 [12836, シャオハ] のハッシュ値は 1151158\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode();\n// ノードオブジェクト utils.ListNode@7dc5e7b4 のハッシュ値は 2110121908\n
built_in_hash.cs
int num = 3;\nint hashNum = num.GetHashCode();\n// 整数 3 のハッシュ値は 3;\n\nbool bol = true;\nint hashBol = bol.GetHashCode();\n// 真理値 true のハッシュ値は 1;\n\ndouble dec = 3.14159;\nint hashDec = dec.GetHashCode();\n// 小数 3.14159 のハッシュ値は -1340954729;\n\nstring str = \"Hello アルゴリズム\";\nint hashStr = str.GetHashCode();\n// 文字列「Hello アルゴリズム」のハッシュ値は -586107568;\n\nobject[] arr = [12836, \"シャオハ\"];\nint hashTup = arr.GetHashCode();\n// 配列 [12836, シャオハ] のハッシュ値は 42931033;\n\nListNode obj = new(0);\nint hashObj = obj.GetHashCode();\n// ノードオブジェクト 0 のハッシュ値は 39053774;\n
built_in_hash.go
// Go は組み込みの hash code 関数を提供していない\n
built_in_hash.swift
let num = 3\nlet hashNum = num.hashValue\n// 整数 3 のハッシュ値は 9047044699613009734\n\nlet bol = true\nlet hashBol = bol.hashValue\n// 真理値 true のハッシュ値は -4431640247352757451\n\nlet dec = 3.14159\nlet hashDec = dec.hashValue\n// 小数 3.14159 のハッシュ値は -2465384235396674631\n\nlet str = \"Hello アルゴリズム\"\nlet hashStr = str.hashValue\n// 文字列「Hello アルゴリズム」のハッシュ値は -7850626797806988787\n\nlet arr = [AnyHashable(12836), AnyHashable(\"シャオハ\")]\nlet hashTup = arr.hashValue\n// 配列 [AnyHashable(12836), AnyHashable(\"シャオハ\")] のハッシュ値は -2308633508154532996\n\nlet obj = ListNode(x: 0)\nlet hashObj = obj.hashValue\n// ノードオブジェクト utils.ListNode のハッシュ値は -2434780518035996159\n
built_in_hash.js
// JavaScript は組み込みの hash code 関数を提供していない\n
built_in_hash.ts
// TypeScript は組み込みの hash code 関数を提供していない\n
built_in_hash.dart
int num = 3;\nint hashNum = num.hashCode;\n// 整数 3 のハッシュ値は 34803\n\nbool bol = true;\nint hashBol = bol.hashCode;\n// 真理値 true のハッシュ値は 1231\n\ndouble dec = 3.14159;\nint hashDec = dec.hashCode;\n// 小数 3.14159 のハッシュ値は 2570631074981783\n\nString str = \"Hello アルゴリズム\";\nint hashStr = str.hashCode;\n// 文字列「Hello アルゴリズム」のハッシュ値は 468167534\n\nList arr = [12836, \"シャオハ\"];\nint hashArr = arr.hashCode;\n// 配列 [12836, シャオハ] のハッシュ値は 976512528\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode;\n// ノードオブジェクト Instance of 'ListNode' のハッシュ値は 1033450432\n
built_in_hash.rs
use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\nlet num = 3;\nlet mut num_hasher = DefaultHasher::new();\nnum.hash(&mut num_hasher);\nlet hash_num = num_hasher.finish();\n// 整数 3 のハッシュ値は 568126464209439262\n\nlet bol = true;\nlet mut bol_hasher = DefaultHasher::new();\nbol.hash(&mut bol_hasher);\nlet hash_bol = bol_hasher.finish();\n// 真理値 true のハッシュ値は 4952851536318644461\n\nlet dec: f32 = 3.14159;\nlet mut dec_hasher = DefaultHasher::new();\ndec.to_bits().hash(&mut dec_hasher);\nlet hash_dec = dec_hasher.finish();\n// 小数 3.14159 のハッシュ値は 2566941990314602357\n\nlet str = \"Hello アルゴリズム\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// 文字列「Hello アルゴリズム」のハッシュ値は 16092673739211250988\n\nlet arr = (&12836, &\"シャオハ\");\nlet mut tup_hasher = DefaultHasher::new();\narr.hash(&mut tup_hasher);\nlet hash_tup = tup_hasher.finish();\n// タプル (12836, \"シャオハ\") のハッシュ値は 1885128010422702749\n\nlet node = ListNode::new(42);\nlet mut hasher = DefaultHasher::new();\nnode.borrow().val.hash(&mut hasher);\nlet hash = hasher.finish();\n// ノードオブジェクト RefCell { value: ListNode { val: 42, next: None } } のハッシュ値は15387811073369036852\n
built_in_hash.c
// C は組み込みの hash code 関数を提供していない\n
built_in_hash.kt
val num = 3\nval hashNum = num.hashCode()\n// 整数 3 のハッシュ値は 3\n\nval bol = true\nval hashBol = bol.hashCode()\n// 真理値 true のハッシュ値は 1231\n\nval dec = 3.14159\nval hashDec = dec.hashCode()\n// 小数 3.14159 のハッシュ値は -1340954729\n\nval str = \"Hello アルゴリズム\"\nval hashStr = str.hashCode()\n// 文字列「Hello アルゴリズム」のハッシュ値は -727081396\n\nval arr = arrayOf<Any>(12836, \"シャオハ\")\nval hashTup = arr.hashCode()\n// 配列 [12836, シャオハ] のハッシュ値は 189568618\n\nval obj = ListNode(0)\nval hashObj = obj.hashCode()\n// ノードオブジェクト utils.ListNode@1d81eb93 のハッシュ値は 495053715\n
built_in_hash.rb
num = 3\nhash_num = num.hash\n# 整数 3 のハッシュ値は -4385856518450339636\n\nbol = true\nhash_bol = bol.hash\n# 真理値 true のハッシュ値は -1617938112149317027\n\ndec = 3.14159\nhash_dec = dec.hash\n# 小数 3.14159 のハッシュ値は -1479186995943067893\n\nstr = \"Hello アルゴリズム\"\nhash_str = str.hash\n# 文字列「Hello アルゴリズム」のハッシュ値は -4075943250025831763\n\ntup = [12836, 'シャオハ']\nhash_tup = tup.hash\n# タプル (12836, 'シャオハ') のハッシュ値は 1999544809202288822\n\nobj = ListNode.new(0)\nhash_obj = obj.hash\n# ノードオブジェクト #<ListNode:0x000078133140ab70> のハッシュ値は 4302940560806366381\n
可視化実行

https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

多くのプログラミング言語では、不変オブジェクトだけがハッシュテーブルの key として使えます。仮にリスト(動的配列)を key とすると、その内容が変化したときにハッシュ値も変わってしまうため、もとの value をハッシュテーブルから検索できなくなります。

カスタムオブジェクト(たとえば連結リストのノード)のメンバ変数は可変ですが、それでもハッシュ可能です。これは、オブジェクトのハッシュ値が通常はメモリアドレスに基づいて生成されるためです。オブジェクトの内容が変化しても、メモリアドレスが変わらなければ、ハッシュ値も変わりません。

注意深い人なら、異なるコンソールでプログラムを実行したときに、出力されるハッシュ値が異なることに気づくかもしれません。これは、Python インタプリタが起動のたびに文字列ハッシュ関数へランダムな salt 値を追加しているためです。この方法によって HashDoS 攻撃を効果的に防ぎ、ハッシュアルゴリズムの安全性を高めています。

","path":["第 6 章   ハッシュテーブル","6.3   ハッシュアルゴリズム"],"tags":[]},{"location":"chapter_hashing/hash_collision/","level":1,"title":"6.2   ハッシュ衝突","text":"

前節で述べたように、**通常、ハッシュ関数の入力空間は出力空間よりもはるかに大きい**ため、理論上ハッシュ衝突は避けられません。例えば、入力空間がすべての整数で、出力空間が配列の容量サイズである場合、必然的に複数の整数が同じバケットインデックスに写像されます。

ハッシュ衝突は検索結果の誤りを招き、ハッシュテーブルの利用可能性に深刻な影響を与えます。この問題を解決するために、ハッシュ衝突が発生するたびにハッシュテーブルを拡張し、衝突が消えるまで続けることが考えられます。この方法は単純で効果的ですが、効率が低すぎます。なぜなら、ハッシュテーブルの拡張には大量のデータ移動とハッシュ値の計算が必要だからです。効率を高めるために、次の戦略を採用できます。

  1. ハッシュテーブルのデータ構造を改良し、ハッシュ衝突が発生してもハッシュテーブルが正常に動作できるようにする。
  2. 必要な場合、すなわちハッシュ衝突が比較的深刻なときにのみ、拡張操作を実行する。

ハッシュテーブルの構造改善方法には、主に「チェイン法」と「オープンアドレッシング」があります。

","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#621","level":2,"title":"6.2.1   チェイン法","text":"

元のハッシュテーブルでは、各バケットには 1 つのキーと値のペアしか格納できません。チェイン法(separate chaining)では、単一要素を連結リストに置き換え、キーと値のペアを連結リストのノードとして扱い、衝突したすべてのキーと値のペアを同じ連結リストに格納します。下図はチェイン法によるハッシュテーブルの例を示しています。

図 6-5   チェイン法ハッシュテーブル

チェイン法で実装されたハッシュテーブルでは、操作方法が次のように変わります。

  • 要素の検索:入力 key をハッシュ関数に通してバケットインデックスを得ると、連結リストの先頭ノードにアクセスできます。その後、連結リストを走査して key を比較し、目的のキーと値のペアを探します。
  • 要素の追加:まずハッシュ関数で連結リストの先頭ノードにアクセスし、その後ノード(キーと値のペア)を連結リストに追加します。
  • 要素の削除:ハッシュ関数の結果に基づいて連結リストの先頭にアクセスし、続いて連結リストを走査して対象ノードを探し、削除します。

チェイン法には次の制約があります。

  • 使用メモリの増加:連結リストにはノードポインタが含まれるため、配列よりも多くのメモリを消費します。
  • 検索効率の低下:対応する要素を見つけるために連結リストを線形走査する必要があるためです。

以下のコードはチェイン法ハッシュテーブルの簡単な実装を示しています。注意すべき点は 2 つあります。

  • 連結リストの代わりにリスト(動的配列)を使って、コードを簡潔にしています。この設定では、ハッシュテーブル(配列)は複数のバケットを含み、各バケットは 1 つのリストです。
  • 以下の実装にはハッシュテーブルの拡張メソッドが含まれています。負荷率が \\(\\frac{2}{3}\\) を超えたとき、ハッシュテーブルを元の \\(2\\) 倍に拡張します。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_chaining.py
class HashMapChaining:\n    \"\"\"チェイン法ハッシュテーブル\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self.size = 0  # キーと値のペア数\n        self.capacity = 4  # ハッシュテーブル容量\n        self.load_thres = 2.0 / 3.0  # リサイズを発動する負荷率のしきい値\n        self.extend_ratio = 2  # 拡張倍率\n        self.buckets = [[] for _ in range(self.capacity)]  # バケット配列\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"ハッシュ関数\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"負荷率\"\"\"\n        return self.size / self.capacity\n\n    def get(self, key: int) -> str | None:\n        \"\"\"検索操作\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # バケットを走査し、key が見つかれば対応する val を返す\n        for pair in bucket:\n            if pair.key == key:\n                return pair.val\n        # key が見つからない場合は None を返す\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"追加操作\"\"\"\n        # 負荷率がしきい値を超えたら、リサイズを実行\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for pair in bucket:\n            if pair.key == key:\n                pair.val = val\n                return\n        # その key が存在しなければ、キーと値のペアを末尾に追加\n        pair = Pair(key, val)\n        bucket.append(pair)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"削除操作\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # バケットを走査してキーと値のペアを削除\n        for pair in bucket:\n            if pair.key == key:\n                bucket.remove(pair)\n                self.size -= 1\n                break\n\n    def extend(self):\n        \"\"\"ハッシュテーブルを拡張\"\"\"\n        # 元のハッシュテーブルを一時保存\n        buckets = self.buckets\n        # リサイズ後の新しいハッシュテーブルを初期化\n        self.capacity *= self.extend_ratio\n        self.buckets = [[] for _ in range(self.capacity)]\n        self.size = 0\n        # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for bucket in buckets:\n            for pair in bucket:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"ハッシュテーブルを出力\"\"\"\n        for bucket in self.buckets:\n            res = []\n            for pair in bucket:\n                res.append(str(pair.key) + \" -> \" + pair.val)\n            print(res)\n
hash_map_chaining.cpp
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n  private:\n    int size;                       // キーと値のペア数\n    int capacity;                   // ハッシュテーブル容量\n    double loadThres;               // リサイズを発動する負荷率のしきい値\n    int extendRatio;                // 拡張倍率\n    vector<vector<Pair *>> buckets; // バケット配列\n\n  public:\n    /* コンストラクタ */\n    HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {\n        buckets.resize(capacity);\n    }\n\n    /* デストラクタメソッド */\n    ~HashMapChaining() {\n        for (auto &bucket : buckets) {\n            for (Pair *pair : bucket) {\n                // メモリを解放する\n                delete pair;\n            }\n        }\n    }\n\n    /* ハッシュ関数 */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    double loadFactor() {\n        return (double)size / (double)capacity;\n    }\n\n    /* 検索操作 */\n    string get(int key) {\n        int index = hashFunc(key);\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                return pair->val;\n            }\n        }\n        // key が見つからない場合は空文字列を返す\n        return \"\";\n    }\n\n    /* 追加操作 */\n    void put(int key, string val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                pair->val = val;\n                return;\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        buckets[index].push_back(new Pair(key, val));\n        size++;\n    }\n\n    /* 削除操作 */\n    void remove(int key) {\n        int index = hashFunc(key);\n        auto &bucket = buckets[index];\n        // バケットを走査してキーと値のペアを削除\n        for (int i = 0; i < bucket.size(); i++) {\n            if (bucket[i]->key == key) {\n                Pair *tmp = bucket[i];\n                bucket.erase(bucket.begin() + i); // そこからキーと値の組を削除する\n                delete tmp;                       // メモリを解放する\n                size--;\n                return;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    void extend() {\n        // 元のハッシュテーブルを一時保存\n        vector<vector<Pair *>> bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets.clear();\n        buckets.resize(capacity);\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (auto &bucket : bucketsTmp) {\n            for (Pair *pair : bucket) {\n                put(pair->key, pair->val);\n                // メモリを解放する\n                delete pair;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    void print() {\n        for (auto &bucket : buckets) {\n            cout << \"[\";\n            for (Pair *pair : bucket) {\n                cout << pair->key << \" -> \" << pair->val << \", \";\n            }\n            cout << \"]\\n\";\n        }\n    }\n};\n
hash_map_chaining.java
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    int size; // キーと値のペア数\n    int capacity; // ハッシュテーブル容量\n    double loadThres; // リサイズを発動する負荷率のしきい値\n    int extendRatio; // 拡張倍率\n    List<List<Pair>> buckets; // バケット配列\n\n    /* コンストラクタ */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n    }\n\n    /* ハッシュ関数 */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* 検索操作 */\n    String get(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // key が見つからない場合は null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    void put(int key, String val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        Pair pair = new Pair(key, val);\n        bucket.add(pair);\n        size++;\n    }\n\n    /* 削除操作 */\n    void remove(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // バケットを走査してキーと値のペアを削除\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    void extend() {\n        // 元のハッシュテーブルを一時保存\n        List<List<Pair>> bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (List<Pair> bucket : bucketsTmp) {\n            for (Pair pair : bucket) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    void print() {\n        for (List<Pair> bucket : buckets) {\n            List<String> res = new ArrayList<>();\n            for (Pair pair : bucket) {\n                res.add(pair.key + \" -> \" + pair.val);\n            }\n            System.out.println(res);\n        }\n    }\n}\n
hash_map_chaining.cs
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    int size; // キーと値のペア数\n    int capacity; // ハッシュテーブル容量\n    double loadThres; // リサイズを発動する負荷率のしきい値\n    int extendRatio; // 拡張倍率\n    List<List<Pair>> buckets; // バケット配列\n\n    /* コンストラクタ */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n    }\n\n    /* ハッシュ関数 */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* 検索操作 */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        // バケットを走査し、key が見つかれば対応する val を返す\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // key が見つからない場合は null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    public void Put(int key, string val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        int index = HashFunc(key);\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        buckets[index].Add(new Pair(key, val));\n        size++;\n    }\n\n    /* 削除操作 */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // バケットを走査してキーと値のペアを削除\n        foreach (Pair pair in buckets[index].ToList()) {\n            if (pair.key == key) {\n                buckets[index].Remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    void Extend() {\n        // 元のハッシュテーブルを一時保存\n        List<List<Pair>> bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        foreach (List<Pair> bucket in bucketsTmp) {\n            foreach (Pair pair in bucket) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    public void Print() {\n        foreach (List<Pair> bucket in buckets) {\n            List<string> res = [];\n            foreach (Pair pair in bucket) {\n                res.Add(pair.key + \" -> \" + pair.val);\n            }\n            foreach (string kv in res) {\n                Console.WriteLine(kv);\n            }\n        }\n    }\n}\n
hash_map_chaining.go
/* チェイン法ハッシュテーブル */\ntype hashMapChaining struct {\n    size        int      // キーと値のペア数\n    capacity    int      // ハッシュテーブル容量\n    loadThres   float64  // リサイズを発動する負荷率のしきい値\n    extendRatio int      // 拡張倍率\n    buckets     [][]pair // バケット配列\n}\n\n/* コンストラクタ */\nfunc newHashMapChaining() *hashMapChaining {\n    buckets := make([][]pair, 4)\n    for i := 0; i < 4; i++ {\n        buckets[i] = make([]pair, 0)\n    }\n    return &hashMapChaining{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     buckets,\n    }\n}\n\n/* ハッシュ関数 */\nfunc (m *hashMapChaining) hashFunc(key int) int {\n    return key % m.capacity\n}\n\n/* 負荷率 */\nfunc (m *hashMapChaining) loadFactor() float64 {\n    return float64(m.size) / float64(m.capacity)\n}\n\n/* 検索操作 */\nfunc (m *hashMapChaining) get(key int) string {\n    idx := m.hashFunc(key)\n    bucket := m.buckets[idx]\n    // バケットを走査し、key が見つかれば対応する val を返す\n    for _, p := range bucket {\n        if p.key == key {\n            return p.val\n        }\n    }\n    // key が見つからない場合は空文字列を返す\n    return \"\"\n}\n\n/* 追加操作 */\nfunc (m *hashMapChaining) put(key int, val string) {\n    // 負荷率がしきい値を超えたら、リサイズを実行\n    if m.loadFactor() > m.loadThres {\n        m.extend()\n    }\n    idx := m.hashFunc(key)\n    // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n    for i := range m.buckets[idx] {\n        if m.buckets[idx][i].key == key {\n            m.buckets[idx][i].val = val\n            return\n        }\n    }\n    // その key が存在しなければ、キーと値のペアを末尾に追加\n    p := pair{\n        key: key,\n        val: val,\n    }\n    m.buckets[idx] = append(m.buckets[idx], p)\n    m.size += 1\n}\n\n/* 削除操作 */\nfunc (m *hashMapChaining) remove(key int) {\n    idx := m.hashFunc(key)\n    // バケットを走査してキーと値のペアを削除\n    for i, p := range m.buckets[idx] {\n        if p.key == key {\n            // スライスから削除する\n            m.buckets[idx] = append(m.buckets[idx][:i], m.buckets[idx][i+1:]...)\n            m.size -= 1\n            break\n        }\n    }\n}\n\n/* ハッシュテーブルを拡張 */\nfunc (m *hashMapChaining) extend() {\n    // 元のハッシュテーブルを一時保存\n    tmpBuckets := make([][]pair, len(m.buckets))\n    for i := 0; i < len(m.buckets); i++ {\n        tmpBuckets[i] = make([]pair, len(m.buckets[i]))\n        copy(tmpBuckets[i], m.buckets[i])\n    }\n    // リサイズ後の新しいハッシュテーブルを初期化\n    m.capacity *= m.extendRatio\n    m.buckets = make([][]pair, m.capacity)\n    for i := 0; i < m.capacity; i++ {\n        m.buckets[i] = make([]pair, 0)\n    }\n    m.size = 0\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for _, bucket := range tmpBuckets {\n        for _, p := range bucket {\n            m.put(p.key, p.val)\n        }\n    }\n}\n\n/* ハッシュテーブルを出力 */\nfunc (m *hashMapChaining) print() {\n    var builder strings.Builder\n\n    for _, bucket := range m.buckets {\n        builder.WriteString(\"[\")\n        for _, p := range bucket {\n            builder.WriteString(strconv.Itoa(p.key) + \" -> \" + p.val + \" \")\n        }\n        builder.WriteString(\"]\")\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
hash_map_chaining.swift
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    var size: Int // キーと値のペア数\n    var capacity: Int // ハッシュテーブル容量\n    var loadThres: Double // リサイズを発動する負荷率のしきい値\n    var extendRatio: Int // 拡張倍率\n    var buckets: [[Pair]] // バケット配列\n\n    /* コンストラクタ */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: [], count: capacity)\n    }\n\n    /* ハッシュ関数 */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* 負荷率 */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* 検索操作 */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for pair in bucket {\n            if pair.key == key {\n                return pair.val\n            }\n        }\n        // `key` が見つからなければ `nil` を返す\n        return nil\n    }\n\n    /* 追加操作 */\n    func put(key: Int, val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if loadFactor() > loadThres {\n            extend()\n        }\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for pair in bucket {\n            if pair.key == key {\n                pair.val = val\n                return\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        let pair = Pair(key: key, val: val)\n        buckets[index].append(pair)\n        size += 1\n    }\n\n    /* 削除操作 */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // バケットを走査してキーと値のペアを削除\n        for (pairIndex, pair) in bucket.enumerated() {\n            if pair.key == key {\n                buckets[index].remove(at: pairIndex)\n                size -= 1\n                break\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    func extend() {\n        // 元のハッシュテーブルを一時保存\n        let bucketsTmp = buckets\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio\n        buckets = Array(repeating: [], count: capacity)\n        size = 0\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for bucket in bucketsTmp {\n            for pair in bucket {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    func print() {\n        for bucket in buckets {\n            let res = bucket.map { \"\\($0.key) -> \\($0.val)\" }\n            Swift.print(res)\n        }\n    }\n}\n
hash_map_chaining.js
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    #size; // キーと値のペア数\n    #capacity; // ハッシュテーブル容量\n    #loadThres; // リサイズを発動する負荷率のしきい値\n    #extendRatio; // 拡張倍率\n    #buckets; // バケット配列\n\n    /* コンストラクタ */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* ハッシュ関数 */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* 負荷率 */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* 検索操作 */\n    get(key) {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // key が見つからない場合は null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    put(key, val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* 削除操作 */\n    remove(key) {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // バケットを走査してキーと値のペアを削除\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    #extend() {\n        // 元のハッシュテーブルを一時保存\n        const bucketsTmp = this.#buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    print() {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
hash_map_chaining.ts
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    #size: number; // キーと値のペア数\n    #capacity: number; // ハッシュテーブル容量\n    #loadThres: number; // リサイズを発動する負荷率のしきい値\n    #extendRatio: number; // 拡張倍率\n    #buckets: Pair[][]; // バケット配列\n\n    /* コンストラクタ */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* ハッシュ関数 */\n    #hashFunc(key: number): number {\n        return key % this.#capacity;\n    }\n\n    /* 負荷率 */\n    #loadFactor(): number {\n        return this.#size / this.#capacity;\n    }\n\n    /* 検索操作 */\n    get(key: number): string | null {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // key が見つからない場合は null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    put(key: number, val: string): void {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* 削除操作 */\n    remove(key: number): void {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // バケットを走査してキーと値のペアを削除\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    #extend(): void {\n        // 元のハッシュテーブルを一時保存\n        const bucketsTmp = this.#buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    print(): void {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
hash_map_chaining.dart
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n  late int size; // キーと値のペア数\n  late int capacity; // ハッシュテーブル容量\n  late double loadThres; // リサイズを発動する負荷率のしきい値\n  late int extendRatio; // 拡張倍率\n  late List<List<Pair>> buckets; // バケット配列\n\n  /* コンストラクタ */\n  HashMapChaining() {\n    size = 0;\n    capacity = 4;\n    loadThres = 2.0 / 3.0;\n    extendRatio = 2;\n    buckets = List.generate(capacity, (_) => []);\n  }\n\n  /* ハッシュ関数 */\n  int hashFunc(int key) {\n    return key % capacity;\n  }\n\n  /* 負荷率 */\n  double loadFactor() {\n    return size / capacity;\n  }\n\n  /* 検索操作 */\n  String? get(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // バケットを走査し、key が見つかれば対応する val を返す\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        return pair.val;\n      }\n    }\n    // key が見つからない場合は null を返す\n    return null;\n  }\n\n  /* 追加操作 */\n  void put(int key, String val) {\n    // 負荷率がしきい値を超えたら、リサイズを実行\n    if (loadFactor() > loadThres) {\n      extend();\n    }\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        pair.val = val;\n        return;\n      }\n    }\n    // その key が存在しなければ、キーと値のペアを末尾に追加\n    Pair pair = Pair(key, val);\n    bucket.add(pair);\n    size++;\n  }\n\n  /* 削除操作 */\n  void remove(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // バケットを走査してキーと値のペアを削除\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        bucket.remove(pair);\n        size--;\n        break;\n      }\n    }\n  }\n\n  /* ハッシュテーブルを拡張 */\n  void extend() {\n    // 元のハッシュテーブルを一時保存\n    List<List<Pair>> bucketsTmp = buckets;\n    // リサイズ後の新しいハッシュテーブルを初期化\n    capacity *= extendRatio;\n    buckets = List.generate(capacity, (_) => []);\n    size = 0;\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for (List<Pair> bucket in bucketsTmp) {\n      for (Pair pair in bucket) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* ハッシュテーブルを出力 */\n  void printHashMap() {\n    for (List<Pair> bucket in buckets) {\n      List<String> res = [];\n      for (Pair pair in bucket) {\n        res.add(\"${pair.key} -> ${pair.val}\");\n      }\n      print(res);\n    }\n  }\n}\n
hash_map_chaining.rs
/* チェイン法ハッシュテーブル */\nstruct HashMapChaining {\n    size: usize,\n    capacity: usize,\n    load_thres: f32,\n    extend_ratio: usize,\n    buckets: Vec<Vec<Pair>>,\n}\n\nimpl HashMapChaining {\n    /* コンストラクタ */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![vec![]; 4],\n        }\n    }\n\n    /* ハッシュ関数 */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % self.capacity\n    }\n\n    /* 負荷率 */\n    fn load_factor(&self) -> f32 {\n        self.size as f32 / self.capacity as f32\n    }\n\n    /* 削除操作 */\n    fn remove(&mut self, key: i32) -> Option<String> {\n        let index = self.hash_func(key);\n\n        // バケットを走査してキーと値のペアを削除\n        for (i, p) in self.buckets[index].iter_mut().enumerate() {\n            if p.key == key {\n                let pair = self.buckets[index].remove(i);\n                self.size -= 1;\n                return Some(pair.val);\n            }\n        }\n\n        // key が見つからない場合は None を返す\n        None\n    }\n\n    /* ハッシュテーブルを拡張 */\n    fn extend(&mut self) {\n        // 元のハッシュテーブルを一時保存\n        let buckets_tmp = std::mem::take(&mut self.buckets);\n\n        // リサイズ後の新しいハッシュテーブルを初期化\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![Vec::new(); self.capacity as usize];\n        self.size = 0;\n\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for bucket in buckets_tmp {\n            for pair in bucket {\n                self.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    fn print(&self) {\n        for bucket in &self.buckets {\n            let mut res = Vec::new();\n            for pair in bucket {\n                res.push(format!(\"{} -> {}\", pair.key, pair.val));\n            }\n            println!(\"{:?}\", res);\n        }\n    }\n\n    /* 追加操作 */\n    fn put(&mut self, key: i32, val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n\n        let index = self.hash_func(key);\n\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for pair in self.buckets[index].iter_mut() {\n            if pair.key == key {\n                pair.val = val;\n                return;\n            }\n        }\n\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        let pair = Pair { key, val };\n        self.buckets[index].push(pair);\n        self.size += 1;\n    }\n\n    /* 検索操作 */\n    fn get(&self, key: i32) -> Option<&str> {\n        let index = self.hash_func(key);\n\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for pair in self.buckets[index].iter() {\n            if pair.key == key {\n                return Some(&pair.val);\n            }\n        }\n\n        // key が見つからない場合は None を返す\n        None\n    }\n}\n
hash_map_chaining.c
/* 連結リストノード */\ntypedef struct Node {\n    Pair *pair;\n    struct Node *next;\n} Node;\n\n/* チェイン法ハッシュテーブル */\ntypedef struct {\n    int size;         // キーと値のペア数\n    int capacity;     // ハッシュテーブル容量\n    double loadThres; // リサイズを発動する負荷率のしきい値\n    int extendRatio;  // 拡張倍率\n    Node **buckets;   // バケット配列\n} HashMapChaining;\n\n/* コンストラクタ */\nHashMapChaining *newHashMapChaining() {\n    HashMapChaining *hashMap = (HashMapChaining *)malloc(sizeof(HashMapChaining));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    return hashMap;\n}\n\n/* デストラクタ */\nvoid delHashMapChaining(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        while (cur) {\n            Node *tmp = cur;\n            cur = cur->next;\n            free(tmp->pair);\n            free(tmp);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap);\n}\n\n/* ハッシュ関数 */\nint hashFunc(HashMapChaining *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* 負荷率 */\ndouble loadFactor(HashMapChaining *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* 検索操作 */\nchar *get(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    // バケットを走査し、key が見つかれば対応する val を返す\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            return cur->pair->val;\n        }\n        cur = cur->next;\n    }\n    return \"\"; // key が見つからない場合は空文字列を返す\n}\n\n/* 追加操作 */\nvoid put(HashMapChaining *hashMap, int key, const char *val) {\n    // 負荷率がしきい値を超えたら、リサイズを実行\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    int index = hashFunc(hashMap, key);\n    // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            strcpy(cur->pair->val, val); // 指定した `key` に遭遇したら、対応する `val` を更新して返す\n            return;\n        }\n        cur = cur->next;\n    }\n    // 該当する `key` がなければ、キーと値のペアを連結リストの先頭に追加する\n    Pair *newPair = (Pair *)malloc(sizeof(Pair));\n    newPair->key = key;\n    strcpy(newPair->val, val);\n    Node *newNode = (Node *)malloc(sizeof(Node));\n    newNode->pair = newPair;\n    newNode->next = hashMap->buckets[index];\n    hashMap->buckets[index] = newNode;\n    hashMap->size++;\n}\n\n/* ハッシュテーブルを拡張 */\nvoid extend(HashMapChaining *hashMap) {\n    // 元のハッシュテーブルを一時保存\n    int oldCapacity = hashMap->capacity;\n    Node **oldBuckets = hashMap->buckets;\n    // リサイズ後の新しいハッシュテーブルを初期化\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    hashMap->size = 0;\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for (int i = 0; i < oldCapacity; i++) {\n        Node *cur = oldBuckets[i];\n        while (cur) {\n            put(hashMap, cur->pair->key, cur->pair->val);\n            Node *temp = cur;\n            cur = cur->next;\n            // メモリを解放する\n            free(temp->pair);\n            free(temp);\n        }\n    }\n\n    free(oldBuckets);\n}\n\n/* 削除操作 */\nvoid removeItem(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    Node *cur = hashMap->buckets[index];\n    Node *pre = NULL;\n    while (cur) {\n        if (cur->pair->key == key) {\n            // そこからキーと値の組を削除する\n            if (pre) {\n                pre->next = cur->next;\n            } else {\n                hashMap->buckets[index] = cur->next;\n            }\n            // メモリを解放する\n            free(cur->pair);\n            free(cur);\n            hashMap->size--;\n            return;\n        }\n        pre = cur;\n        cur = cur->next;\n    }\n}\n\n/* ハッシュテーブルを出力 */\nvoid print(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        printf(\"[\");\n        while (cur) {\n            printf(\"%d -> %s, \", cur->pair->key, cur->pair->val);\n            cur = cur->next;\n        }\n        printf(\"]\\n\");\n    }\n}\n
hash_map_chaining.kt
/* チェイン法ハッシュテーブル */\nclass HashMapChaining {\n    var size: Int // キーと値のペア数\n    var capacity: Int // ハッシュテーブル容量\n    val loadThres: Double // リサイズを発動する負荷率のしきい値\n    val extendRatio: Int // 拡張倍率\n    var buckets: MutableList<MutableList<Pair>> // バケット配列\n\n    /* コンストラクタ */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n    }\n\n    /* ハッシュ関数 */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* 負荷率 */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* 検索操作 */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // バケットを走査し、key が見つかれば対応する val を返す\n        for (pair in bucket) {\n            if (pair.key == key) return pair._val\n        }\n        // key が見つからない場合は null を返す\n        return null\n    }\n\n    /* 追加操作 */\n    fun put(key: Int, _val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n        for (pair in bucket) {\n            if (pair.key == key) {\n                pair._val = _val\n                return\n            }\n        }\n        // その key が存在しなければ、キーと値のペアを末尾に追加\n        val pair = Pair(key, _val)\n        bucket.add(pair)\n        size++\n    }\n\n    /* 削除操作 */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // バケットを走査してキーと値のペアを削除\n        for (pair in bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair)\n                size--\n                break\n            }\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    fun extend() {\n        // 元のハッシュテーブルを一時保存\n        val bucketsTmp = buckets\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio\n        // mutablelist には固定サイズがない\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n        size = 0\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (bucket in bucketsTmp) {\n            for (pair in bucket) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    fun print() {\n        for (bucket in buckets) {\n            val res = mutableListOf<String>()\n            for (pair in bucket) {\n                val k = pair.key\n                val v = pair._val\n                res.add(\"$k -> $v\")\n            }\n            println(res)\n        }\n    }\n}\n
hash_map_chaining.rb
### キーアドレス法ハッシュテーブル ###\nclass HashMapChaining\n  ### コンストラクタ ###\n  def initialize\n    @size = 0 # キーと値のペア数\n    @capacity = 4 # ハッシュテーブル容量\n    @load_thres = 2.0 / 3.0 # リサイズを発動する負荷率のしきい値\n    @extend_ratio = 2 # 拡張倍率\n    @buckets = Array.new(@capacity) { [] } # バケット配列\n  end\n\n  ### ハッシュ関数 ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### 負荷率 ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### 検索操作 ###\n  def get(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # バケットを走査し、key が見つかれば対応する val を返す\n    for pair in bucket\n      return pair.val if pair.key == key\n    end\n    # `key` が見つからなければ `nil` を返す\n    nil\n  end\n\n  ### 追加操作 ###\n  def put(key, val)\n    # 負荷率がしきい値を超えたら、リサイズを実行\n    extend if load_factor > @load_thres\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # バケットを走査し、指定した key が見つかれば対応する val を更新して返す\n    for pair in bucket\n      if pair.key == key\n        pair.val = val\n        return\n      end\n    end\n    # その key が存在しなければ、キーと値のペアを末尾に追加\n    pair = Pair.new(key, val)\n    bucket << pair\n    @size += 1\n  end\n\n  ### 削除操作 ###\n  def remove(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # バケットを走査してキーと値のペアを削除\n    for pair in bucket\n      if pair.key == key\n        bucket.delete(pair)\n        @size -= 1\n        break\n      end\n    end\n  end\n\n  ### ハッシュテーブルを拡張 ###\n  def extend\n    # 元のハッシュテーブルを一時保存\n    buckets = @buckets\n    # リサイズ後の新しいハッシュテーブルを初期化\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity) { [] }\n    @size = 0\n    # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for bucket in buckets\n      for pair in bucket\n        put(pair.key, pair.val)\n      end\n    end\n  end\n\n  ### ハッシュテーブルを出力 ###\n  def print\n    for bucket in @buckets\n      res = []\n      for pair in bucket\n        res << \"#{pair.key} -> #{pair.val}\"\n      end\n      pp res\n    end\n  end\nend\n
コードの可視化

全画面で見る >

注意すべきなのは、連結リストが長い場合、検索効率 \\(O(n)\\) は非常に低いことです。このとき、連結リストを「AVL 木」または「赤黒木」に変換することで、検索操作の時間計算量を \\(O(\\log n)\\) に最適化できます。

","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#622","level":2,"title":"6.2.2   オープンアドレッシング","text":"

オープンアドレッシング(open addressing)では追加のデータ構造を導入せず、「複数回の探索」によってハッシュ衝突を処理します。探索方法には主に線形探索、二次探索、多重ハッシュなどがあります。

以下では線形探索を例に、オープンアドレッシングハッシュテーブルの動作の仕組みを説明します。

","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#1","level":3,"title":"1.   線形探索","text":"

線形探索では、固定ステップ長の線形探索によって探索を行います。その操作方法は通常のハッシュテーブルとは異なります。

  • 要素の挿入:ハッシュ関数によってバケットインデックスを計算し、バケット内にすでに要素がある場合は、衝突位置から後方へ線形に走査し(ステップ長は通常 \\(1\\) )、空のバケットが見つかるまで進み、その中に要素を挿入します。
  • 要素の検索:ハッシュ衝突が見つかった場合は、同じステップ長で後方へ線形走査を行い、対応する要素が見つかるまで続け、 value を返します。空のバケットに遭遇した場合は、対象要素がハッシュテーブル内に存在しないことを意味するため、 None を返します。

下図はオープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布を示しています。このハッシュ関数では、末尾 2 桁が同じ key はすべて同じバケットに写像されます。線形探索によって、それらはそのバケットとその後続のバケットに順に格納されます。

図 6-6   オープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布

しかし、**線形探索では「クラスタリング現象」が起こりやすい**です。具体的には、配列内で連続して占有された位置が長いほど、それらの連続位置でハッシュ衝突が発生する可能性が高くなり、さらにその位置の集積成長を促して悪循環を生み、最終的には追加・削除・検索・更新操作の効率低下を招きます。

注意すべきなのは、**オープンアドレッシングハッシュテーブルでは要素を直接削除できない**ことです。これは、要素を削除すると配列内に空バケット None が生じ、要素を検索するときに線形探索がその空バケットに到達した時点で返ってしまうため、その空バケットより後ろの要素には二度とアクセスできなくなるからです。結果として、プログラムがそれらの要素を存在しないと誤判定する可能性があります。下図のとおりです。

図 6-7   オープンアドレッシングで要素を削除したことによる検索問題

この問題を解決するために、遅延削除(lazy deletion)の仕組みを採用できます。これは要素をハッシュテーブルから直接取り除かず、代わりに定数 TOMBSTONE を使ってこのバケットをマークします。この仕組みでは、NoneTOMBSTONE はどちらも空バケットを表し、どちらにもキーと値のペアを配置できます。ただし異なるのは、線形探索が TOMBSTONE に到達した場合は、その先にキーと値のペアが存在する可能性があるため、探索を続けるべきだという点です。

しかし、遅延削除はハッシュテーブルの性能劣化を加速させる可能性があります。これは、削除操作のたびに削除マークが 1 つ生成され、TOMBSTONE が増えるにつれて探索時間も増加するためです。線形探索では、対象要素を見つけるまでに複数の TOMBSTONE を飛び越える必要があるかもしれません。

そのため、線形探索では、遭遇した最初の TOMBSTONE のインデックスを記録し、見つかった対象要素とその TOMBSTONE を交換することを考えます。こうする利点は、要素を検索または追加するたびに、要素が理想位置(探索開始点)により近いバケットへ移動し、検索効率が向上することです。

以下のコードは、遅延削除を含むオープンアドレッシング(線形探索)ハッシュテーブルを実装したものです。ハッシュテーブルの空間をより十分に活用するために、ハッシュテーブルを「環状配列」とみなし、配列末尾を越えたら先頭に戻って探索を続けます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_open_addressing.py
class HashMapOpenAddressing:\n    \"\"\"オープンアドレス法ハッシュテーブル\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self.size = 0  # キーと値のペア数\n        self.capacity = 4  # ハッシュテーブル容量\n        self.load_thres = 2.0 / 3.0  # リサイズを発動する負荷率のしきい値\n        self.extend_ratio = 2  # 拡張倍率\n        self.buckets: list[Pair | None] = [None] * self.capacity  # バケット配列\n        self.TOMBSTONE = Pair(-1, \"-1\")  # 削除済みマーク\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"ハッシュ関数\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"負荷率\"\"\"\n        return self.size / self.capacity\n\n    def find_bucket(self, key: int) -> int:\n        \"\"\"key に対応するバケットインデックスを探す\"\"\"\n        index = self.hash_func(key)\n        first_tombstone = -1\n        # 線形プロービングを行い、空バケットに達したら終了\n        while self.buckets[index] is not None:\n            # key が見つかったら、対応するバケットのインデックスを返す\n            if self.buckets[index].key == key:\n                # 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if first_tombstone != -1:\n                    self.buckets[first_tombstone] = self.buckets[index]\n                    self.buckets[index] = self.TOMBSTONE\n                    return first_tombstone  # 移動後のバケットインデックスを返す\n                return index  # バケットのインデックスを返す\n            # 最初に見つかった削除マークを記録\n            if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:\n                first_tombstone = index\n            # バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % self.capacity\n        # key が存在しない場合は追加位置のインデックスを返す\n        return index if first_tombstone == -1 else first_tombstone\n\n    def get(self, key: int) -> str:\n        \"\"\"検索操作\"\"\"\n        # key に対応するバケットインデックスを探す\n        index = self.find_bucket(key)\n        # キーと値の組が見つかったら、対応する val を返す\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            return self.buckets[index].val\n        # キーと値のペアが存在しない場合は `None` を返す\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"追加操作\"\"\"\n        # 負荷率がしきい値を超えたら、リサイズを実行\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        # key に対応するバケットインデックスを探す\n        index = self.find_bucket(key)\n        # キーと値の組が見つかったら、val を上書きして返す\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index].val = val\n            return\n        # キーと値の組が存在しない場合は、その組を追加する\n        self.buckets[index] = Pair(key, val)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"削除操作\"\"\"\n        # key に対応するバケットインデックスを探す\n        index = self.find_bucket(key)\n        # キーと値の組が見つかったら、削除マーカーで上書きする\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index] = self.TOMBSTONE\n            self.size -= 1\n\n    def extend(self):\n        \"\"\"ハッシュテーブルを拡張\"\"\"\n        # 元のハッシュテーブルを一時保存\n        buckets_tmp = self.buckets\n        # リサイズ後の新しいハッシュテーブルを初期化\n        self.capacity *= self.extend_ratio\n        self.buckets = [None] * self.capacity\n        self.size = 0\n        # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for pair in buckets_tmp:\n            if pair not in [None, self.TOMBSTONE]:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"ハッシュテーブルを出力\"\"\"\n        for pair in self.buckets:\n            if pair is None:\n                print(\"None\")\n            elif pair is self.TOMBSTONE:\n                print(\"TOMBSTONE\")\n            else:\n                print(pair.key, \"->\", pair.val)\n
hash_map_open_addressing.cpp
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n  private:\n    int size;                             // キーと値のペア数\n    int capacity = 4;                     // ハッシュテーブル容量\n    const double loadThres = 2.0 / 3.0;     // リサイズを発動する負荷率のしきい値\n    const int extendRatio = 2;            // 拡張倍率\n    vector<Pair *> buckets;               // バケット配列\n    Pair *TOMBSTONE = new Pair(-1, \"-1\"); // 削除済みマーク\n\n  public:\n    /* コンストラクタ */\n    HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {\n    }\n\n    /* デストラクタメソッド */\n    ~HashMapOpenAddressing() {\n        for (Pair *pair : buckets) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                delete pair;\n            }\n        }\n        delete TOMBSTONE;\n    }\n\n    /* ハッシュ関数 */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    double loadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (buckets[index] != nullptr) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (buckets[index]->key == key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* 検索操作 */\n    string get(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            return buckets[index]->val;\n        }\n        // キーと値の組が存在しない場合は空文字列を返す\n        return \"\";\n    }\n\n    /* 追加操作 */\n    void put(int key, string val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            buckets[index]->val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* 削除操作 */\n    void remove(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            delete buckets[index];\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    void extend() {\n        // 元のハッシュテーブルを一時保存\n        vector<Pair *> bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets = vector<Pair *>(capacity, nullptr);\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (Pair *pair : bucketsTmp) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                put(pair->key, pair->val);\n                delete pair;\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    void print() {\n        for (Pair *pair : buckets) {\n            if (pair == nullptr) {\n                cout << \"nullptr\" << endl;\n            } else if (pair == TOMBSTONE) {\n                cout << \"TOMBSTONE\" << endl;\n            } else {\n                cout << pair->key << \" -> \" << pair->val << endl;\n            }\n        }\n    }\n};\n
hash_map_open_addressing.java
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    private int size; // キーと値のペア数\n    private int capacity = 4; // ハッシュテーブル容量\n    private final double loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値\n    private final int extendRatio = 2; // 拡張倍率\n    private Pair[] buckets; // バケット配列\n    private final Pair TOMBSTONE = new Pair(-1, \"-1\"); // 削除済みマーク\n\n    /* コンストラクタ */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* ハッシュ関数 */\n    private int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    private double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    private int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (buckets[index] != null) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (buckets[index].key == key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* 検索操作 */\n    public String get(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // キーと値の組が存在しなければ null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    public void put(int key, String val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* 削除操作 */\n    public void remove(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = findBucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    private void extend() {\n        // 元のハッシュテーブルを一時保存\n        Pair[] bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (Pair pair : bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    public void print() {\n        for (Pair pair : buckets) {\n            if (pair == null) {\n                System.out.println(\"null\");\n            } else if (pair == TOMBSTONE) {\n                System.out.println(\"TOMBSTONE\");\n            } else {\n                System.out.println(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.cs
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    int size; // キーと値のペア数\n    int capacity = 4; // ハッシュテーブル容量\n    double loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値\n    int extendRatio = 2; // 拡張倍率\n    Pair[] buckets; // バケット配列\n    Pair TOMBSTONE = new(-1, \"-1\"); // 削除済みマーク\n\n    /* コンストラクタ */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* ハッシュ関数 */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 負荷率 */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    int FindBucket(int key) {\n        int index = HashFunc(key);\n        int firstTombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (buckets[index] != null) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (buckets[index].key == key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* 検索操作 */\n    public string? Get(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = FindBucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // キーと値の組が存在しなければ null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    public void Put(int key, string val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        // key に対応するバケットインデックスを探す\n        int index = FindBucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* 削除操作 */\n    public void Remove(int key) {\n        // key に対応するバケットインデックスを探す\n        int index = FindBucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    void Extend() {\n        // 元のハッシュテーブルを一時保存\n        Pair[] bucketsTmp = buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        foreach (Pair pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    public void Print() {\n        foreach (Pair pair in buckets) {\n            if (pair == null) {\n                Console.WriteLine(\"null\");\n            } else if (pair == TOMBSTONE) {\n                Console.WriteLine(\"TOMBSTONE\");\n            } else {\n                Console.WriteLine(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.go
/* オープンアドレス法ハッシュテーブル */\ntype hashMapOpenAddressing struct {\n    size        int     // キーと値のペア数\n    capacity    int     // ハッシュテーブル容量\n    loadThres   float64 // リサイズを発動する負荷率のしきい値\n    extendRatio int     // 拡張倍率\n    buckets     []*pair // バケット配列\n    TOMBSTONE   *pair   // 削除済みマーク\n}\n\n/* コンストラクタ */\nfunc newHashMapOpenAddressing() *hashMapOpenAddressing {\n    return &hashMapOpenAddressing{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     make([]*pair, 4),\n        TOMBSTONE:   &pair{-1, \"-1\"},\n    }\n}\n\n/* ハッシュ関数 */\nfunc (h *hashMapOpenAddressing) hashFunc(key int) int {\n    return key % h.capacity // キーに基づいてハッシュ値を計算\n}\n\n/* 負荷率 */\nfunc (h *hashMapOpenAddressing) loadFactor() float64 {\n    return float64(h.size) / float64(h.capacity) // 現在の負荷率を計算\n}\n\n/* key に対応するバケットインデックスを探す */\nfunc (h *hashMapOpenAddressing) findBucket(key int) int {\n    index := h.hashFunc(key) // 初期インデックスを取得\n    firstTombstone := -1     // 最初に遭遇した `TOMBSTONE` の位置を記録する\n    for h.buckets[index] != nil {\n        if h.buckets[index].key == key {\n            if firstTombstone != -1 {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                h.buckets[firstTombstone] = h.buckets[index]\n                h.buckets[index] = h.TOMBSTONE\n                return firstTombstone // 移動後のバケットインデックスを返す\n            }\n            return index // 見つかったインデックスを返す\n        }\n        if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE {\n            firstTombstone = index // 最初に遭遇した削除マークの位置を記録する\n        }\n        index = (index + 1) % h.capacity // 線形探索を行い、末尾を越えたら先頭に戻る\n    }\n    // key が存在しない場合は追加位置のインデックスを返す\n    if firstTombstone != -1 {\n        return firstTombstone\n    }\n    return index\n}\n\n/* 検索操作 */\nfunc (h *hashMapOpenAddressing) get(key int) string {\n    index := h.findBucket(key) // key に対応するバケットインデックスを探す\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        return h.buckets[index].val // キーと値の組が見つかったら、対応する val を返す\n    }\n    return \"\" // キーと値のペアが存在しない場合は `\"\"` を返す\n}\n\n/* 追加操作 */\nfunc (h *hashMapOpenAddressing) put(key int, val string) {\n    if h.loadFactor() > h.loadThres {\n        h.extend() // 負荷率がしきい値を超えたら、リサイズを実行\n    }\n    index := h.findBucket(key) // key に対応するバケットインデックスを探す\n    if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE {\n        h.buckets[index] = &pair{key, val} // キーと値の組が存在しない場合は、その組を追加する\n        h.size++\n    } else {\n        h.buckets[index].val = val // キーと値のペアが見つかった場合は、`val` を上書きする\n    }\n}\n\n/* 削除操作 */\nfunc (h *hashMapOpenAddressing) remove(key int) {\n    index := h.findBucket(key) // key に対応するバケットインデックスを探す\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        h.buckets[index] = h.TOMBSTONE // キーと値の組が見つかったら、削除マーカーで上書きする\n        h.size--\n    }\n}\n\n/* ハッシュテーブルを拡張 */\nfunc (h *hashMapOpenAddressing) extend() {\n    oldBuckets := h.buckets               // 元のハッシュテーブルを一時保存\n    h.capacity *= h.extendRatio           // 容量を更新\n    h.buckets = make([]*pair, h.capacity) // リサイズ後の新しいハッシュテーブルを初期化\n    h.size = 0                            // サイズをリセットする\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for _, pair := range oldBuckets {\n        if pair != nil && pair != h.TOMBSTONE {\n            h.put(pair.key, pair.val)\n        }\n    }\n}\n\n/* ハッシュテーブルを出力 */\nfunc (h *hashMapOpenAddressing) print() {\n    for _, pair := range h.buckets {\n        if pair == nil {\n            fmt.Println(\"nil\")\n        } else if pair == h.TOMBSTONE {\n            fmt.Println(\"TOMBSTONE\")\n        } else {\n            fmt.Printf(\"%d -> %s\\n\", pair.key, pair.val)\n        }\n    }\n}\n
hash_map_open_addressing.swift
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    var size: Int // キーと値のペア数\n    var capacity: Int // ハッシュテーブル容量\n    var loadThres: Double // リサイズを発動する負荷率のしきい値\n    var extendRatio: Int // 拡張倍率\n    var buckets: [Pair?] // バケット配列\n    var TOMBSTONE: Pair // 削除済みマーク\n\n    /* コンストラクタ */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: nil, count: capacity)\n        TOMBSTONE = Pair(key: -1, val: \"-1\")\n    }\n\n    /* ハッシュ関数 */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* 負荷率 */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    func findBucket(key: Int) -> Int {\n        var index = hashFunc(key: key)\n        var firstTombstone = -1\n        // 線形プロービングを行い、空バケットに達したら終了\n        while buckets[index] != nil {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if buckets[index]!.key == key {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if firstTombstone != -1 {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // 移動後のバケットインデックスを返す\n                }\n                return index // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if firstTombstone == -1 && buckets[index] == TOMBSTONE {\n                firstTombstone = index\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % capacity\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone == -1 ? index : firstTombstone\n    }\n\n    /* 検索操作 */\n    func get(key: Int) -> String? {\n        // key に対応するバケットインデックスを探す\n        let index = findBucket(key: key)\n        // キーと値の組が見つかったら、対応する val を返す\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            return buckets[index]!.val\n        }\n        // キーと値の組が存在しなければ null を返す\n        return nil\n    }\n\n    /* 追加操作 */\n    func put(key: Int, val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if loadFactor() > loadThres {\n            extend()\n        }\n        // key に対応するバケットインデックスを探す\n        let index = findBucket(key: key)\n        // キーと値の組が見つかったら、val を上書きして返す\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index]!.val = val\n            return\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        buckets[index] = Pair(key: key, val: val)\n        size += 1\n    }\n\n    /* 削除操作 */\n    func remove(key: Int) {\n        // key に対応するバケットインデックスを探す\n        let index = findBucket(key: key)\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index] = TOMBSTONE\n            size -= 1\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    func extend() {\n        // 元のハッシュテーブルを一時保存\n        let bucketsTmp = buckets\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio\n        buckets = Array(repeating: nil, count: capacity)\n        size = 0\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for pair in bucketsTmp {\n            if let pair, pair != TOMBSTONE {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    func print() {\n        for pair in buckets {\n            if pair == nil {\n                Swift.print(\"null\")\n            } else if pair == TOMBSTONE {\n                Swift.print(\"TOMBSTONE\")\n            } else {\n                Swift.print(\"\\(pair!.key) -> \\(pair!.val)\")\n            }\n        }\n    }\n}\n
hash_map_open_addressing.js
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    #size; // キーと値のペア数\n    #capacity; // ハッシュテーブル容量\n    #loadThres; // リサイズを発動する負荷率のしきい値\n    #extendRatio; // 拡張倍率\n    #buckets; // バケット配列\n    #TOMBSTONE; // 削除済みマーク\n\n    /* コンストラクタ */\n    constructor() {\n        this.#size = 0; // キーと値のペア数\n        this.#capacity = 4; // ハッシュテーブル容量\n        this.#loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値\n        this.#extendRatio = 2; // 拡張倍率\n        this.#buckets = Array(this.#capacity).fill(null); // バケット配列\n        this.#TOMBSTONE = new Pair(-1, '-1'); // 削除済みマーク\n    }\n\n    /* ハッシュ関数 */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* 負荷率 */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    #findBucket(key) {\n        let index = this.#hashFunc(key);\n        let firstTombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (this.#buckets[index] !== null) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (this.#buckets[index].key === key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone !== -1) {\n                    this.#buckets[firstTombstone] = this.#buckets[index];\n                    this.#buckets[index] = this.#TOMBSTONE;\n                    return firstTombstone; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (\n                firstTombstone === -1 &&\n                this.#buckets[index] === this.#TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % this.#capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* 検索操作 */\n    get(key) {\n        // key に対応するバケットインデックスを探す\n        const index = this.#findBucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            return this.#buckets[index].val;\n        }\n        // キーと値の組が存在しなければ null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    put(key, val) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        // key に対応するバケットインデックスを探す\n        const index = this.#findBucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index].val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        this.#buckets[index] = new Pair(key, val);\n        this.#size++;\n    }\n\n    /* 削除操作 */\n    remove(key) {\n        // key に対応するバケットインデックスを探す\n        const index = this.#findBucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index] = this.#TOMBSTONE;\n            this.#size--;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    #extend() {\n        // 元のハッシュテーブルを一時保存\n        const bucketsTmp = this.#buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = Array(this.#capacity).fill(null);\n        this.#size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.#TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    print() {\n        for (const pair of this.#buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.#TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.ts
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    private size: number; // キーと値のペア数\n    private capacity: number; // ハッシュテーブル容量\n    private loadThres: number; // リサイズを発動する負荷率のしきい値\n    private extendRatio: number; // 拡張倍率\n    private buckets: Array<Pair | null>; // バケット配列\n    private TOMBSTONE: Pair; // 削除済みマーク\n\n    /* コンストラクタ */\n    constructor() {\n        this.size = 0; // キーと値のペア数\n        this.capacity = 4; // ハッシュテーブル容量\n        this.loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値\n        this.extendRatio = 2; // 拡張倍率\n        this.buckets = Array(this.capacity).fill(null); // バケット配列\n        this.TOMBSTONE = new Pair(-1, '-1'); // 削除済みマーク\n    }\n\n    /* ハッシュ関数 */\n    private hashFunc(key: number): number {\n        return key % this.capacity;\n    }\n\n    /* 負荷率 */\n    private loadFactor(): number {\n        return this.size / this.capacity;\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    private findBucket(key: number): number {\n        let index = this.hashFunc(key);\n        let firstTombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (this.buckets[index] !== null) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (this.buckets[index]!.key === key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone !== -1) {\n                    this.buckets[firstTombstone] = this.buckets[index];\n                    this.buckets[index] = this.TOMBSTONE;\n                    return firstTombstone; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (\n                firstTombstone === -1 &&\n                this.buckets[index] === this.TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % this.capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* 検索操作 */\n    get(key: number): string | null {\n        // key に対応するバケットインデックスを探す\n        const index = this.findBucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            return this.buckets[index]!.val;\n        }\n        // キーと値の組が存在しなければ null を返す\n        return null;\n    }\n\n    /* 追加操作 */\n    put(key: number, val: string): void {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (this.loadFactor() > this.loadThres) {\n            this.extend();\n        }\n        // key に対応するバケットインデックスを探す\n        const index = this.findBucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index]!.val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        this.buckets[index] = new Pair(key, val);\n        this.size++;\n    }\n\n    /* 削除操作 */\n    remove(key: number): void {\n        // key に対応するバケットインデックスを探す\n        const index = this.findBucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index] = this.TOMBSTONE;\n            this.size--;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    private extend(): void {\n        // 元のハッシュテーブルを一時保存\n        const bucketsTmp = this.buckets;\n        // リサイズ後の新しいハッシュテーブルを初期化\n        this.capacity *= this.extendRatio;\n        this.buckets = Array(this.capacity).fill(null);\n        this.size = 0;\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    print(): void {\n        for (const pair of this.buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.dart
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n  late int _size; // キーと値のペア数\n  int _capacity = 4; // ハッシュテーブル容量\n  double _loadThres = 2.0 / 3.0; // リサイズを発動する負荷率のしきい値\n  int _extendRatio = 2; // 拡張倍率\n  late List<Pair?> _buckets; // バケット配列\n  Pair _TOMBSTONE = Pair(-1, \"-1\"); // 削除済みマーク\n\n  /* コンストラクタ */\n  HashMapOpenAddressing() {\n    _size = 0;\n    _buckets = List.generate(_capacity, (index) => null);\n  }\n\n  /* ハッシュ関数 */\n  int hashFunc(int key) {\n    return key % _capacity;\n  }\n\n  /* 負荷率 */\n  double loadFactor() {\n    return _size / _capacity;\n  }\n\n  /* key に対応するバケットインデックスを探す */\n  int findBucket(int key) {\n    int index = hashFunc(key);\n    int firstTombstone = -1;\n    // 線形プロービングを行い、空バケットに達したら終了\n    while (_buckets[index] != null) {\n      // key が見つかったら、対応するバケットのインデックスを返す\n      if (_buckets[index]!.key == key) {\n        // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n        if (firstTombstone != -1) {\n          _buckets[firstTombstone] = _buckets[index];\n          _buckets[index] = _TOMBSTONE;\n          return firstTombstone; // 移動後のバケットインデックスを返す\n        }\n        return index; // バケットのインデックスを返す\n      }\n      // 最初に見つかった削除マークを記録\n      if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) {\n        firstTombstone = index;\n      }\n      // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n      index = (index + 1) % _capacity;\n    }\n    // key が存在しない場合は追加位置のインデックスを返す\n    return firstTombstone == -1 ? index : firstTombstone;\n  }\n\n  /* 検索操作 */\n  String? get(int key) {\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(key);\n    // キーと値の組が見つかったら、対応する val を返す\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      return _buckets[index]!.val;\n    }\n    // キーと値の組が存在しなければ null を返す\n    return null;\n  }\n\n  /* 追加操作 */\n  void put(int key, String val) {\n    // 負荷率がしきい値を超えたら、リサイズを実行\n    if (loadFactor() > _loadThres) {\n      extend();\n    }\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(key);\n    // キーと値の組が見つかったら、val を上書きして返す\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index]!.val = val;\n      return;\n    }\n    // キーと値の組が存在しない場合は、その組を追加する\n    _buckets[index] = new Pair(key, val);\n    _size++;\n  }\n\n  /* 削除操作 */\n  void remove(int key) {\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(key);\n    // キーと値の組が見つかったら、削除マーカーで上書きする\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index] = _TOMBSTONE;\n      _size--;\n    }\n  }\n\n  /* ハッシュテーブルを拡張 */\n  void extend() {\n    // 元のハッシュテーブルを一時保存\n    List<Pair?> bucketsTmp = _buckets;\n    // リサイズ後の新しいハッシュテーブルを初期化\n    _capacity *= _extendRatio;\n    _buckets = List.generate(_capacity, (index) => null);\n    _size = 0;\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for (Pair? pair in bucketsTmp) {\n      if (pair != null && pair != _TOMBSTONE) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* ハッシュテーブルを出力 */\n  void printHashMap() {\n    for (Pair? pair in _buckets) {\n      if (pair == null) {\n        print(\"null\");\n      } else if (pair == _TOMBSTONE) {\n        print(\"TOMBSTONE\");\n      } else {\n        print(\"${pair.key} -> ${pair.val}\");\n      }\n    }\n  }\n}\n
hash_map_open_addressing.rs
/* オープンアドレス法ハッシュテーブル */\nstruct HashMapOpenAddressing {\n    size: usize,                // キーと値のペア数\n    capacity: usize,            // ハッシュテーブル容量\n    load_thres: f64,            // リサイズを発動する負荷率のしきい値\n    extend_ratio: usize,        // 拡張倍率\n    buckets: Vec<Option<Pair>>, // バケット配列\n    TOMBSTONE: Option<Pair>,    // 削除済みマーク\n}\n\nimpl HashMapOpenAddressing {\n    /* コンストラクタ */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![None; 4],\n            TOMBSTONE: Some(Pair {\n                key: -1,\n                val: \"-1\".to_string(),\n            }),\n        }\n    }\n\n    /* ハッシュ関数 */\n    fn hash_func(&self, key: i32) -> usize {\n        (key % self.capacity as i32) as usize\n    }\n\n    /* 負荷率 */\n    fn load_factor(&self) -> f64 {\n        self.size as f64 / self.capacity as f64\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    fn find_bucket(&mut self, key: i32) -> usize {\n        let mut index = self.hash_func(key);\n        let mut first_tombstone = -1;\n        // 線形プロービングを行い、空バケットに達したら終了\n        while self.buckets[index].is_some() {\n            // `key` に遭遇したら、対応するバケットのインデックスを返す\n            if self.buckets[index].as_ref().unwrap().key == key {\n                // 以前に削除マークに遭遇していた場合は、キーと値のペアをそのインデックスへ移動する\n                if first_tombstone != -1 {\n                    self.buckets[first_tombstone as usize] = self.buckets[index].take();\n                    self.buckets[index] = self.TOMBSTONE.clone();\n                    return first_tombstone as usize; // 移動後のバケットインデックスを返す\n                }\n                return index; // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE {\n                first_tombstone = index as i32;\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % self.capacity;\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        if first_tombstone == -1 {\n            index\n        } else {\n            first_tombstone as usize\n        }\n    }\n\n    /* 検索操作 */\n    fn get(&mut self, key: i32) -> Option<&str> {\n        // key に対応するバケットインデックスを探す\n        let index = self.find_bucket(key);\n        // キーと値の組が見つかったら、対応する val を返す\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            return self.buckets[index].as_ref().map(|pair| &pair.val as &str);\n        }\n        // キーと値の組が存在しなければ null を返す\n        None\n    }\n\n    /* 追加操作 */\n    fn put(&mut self, key: i32, val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n        // key に対応するバケットインデックスを探す\n        let index = self.find_bucket(key);\n        // キーと値の組が見つかったら、val を上書きして返す\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index].as_mut().unwrap().val = val;\n            return;\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        self.buckets[index] = Some(Pair { key, val });\n        self.size += 1;\n    }\n\n    /* 削除操作 */\n    fn remove(&mut self, key: i32) {\n        // key に対応するバケットインデックスを探す\n        let index = self.find_bucket(key);\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index] = self.TOMBSTONE.clone();\n            self.size -= 1;\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    fn extend(&mut self) {\n        // 元のハッシュテーブルを一時保存\n        let buckets_tmp = self.buckets.clone();\n        // リサイズ後の新しいハッシュテーブルを初期化\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![None; self.capacity];\n        self.size = 0;\n\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for pair in buckets_tmp {\n            if pair.is_none() || pair == self.TOMBSTONE {\n                continue;\n            }\n            let pair = pair.unwrap();\n\n            self.put(pair.key, pair.val);\n        }\n    }\n    /* ハッシュテーブルを出力 */\n    fn print(&self) {\n        for pair in &self.buckets {\n            if pair.is_none() {\n                println!(\"null\");\n            } else if pair == &self.TOMBSTONE {\n                println!(\"TOMBSTONE\");\n            } else {\n                let pair = pair.as_ref().unwrap();\n                println!(\"{} -> {}\", pair.key, pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.c
/* オープンアドレス法ハッシュテーブル */\ntypedef struct {\n    int size;         // キーと値のペア数\n    int capacity;     // ハッシュテーブル容量\n    double loadThres; // リサイズを発動する負荷率のしきい値\n    int extendRatio;  // 拡張倍率\n    Pair **buckets;   // バケット配列\n    Pair *TOMBSTONE;  // 削除済みマーク\n} HashMapOpenAddressing;\n\n/* コンストラクタ */\nHashMapOpenAddressing *newHashMapOpenAddressing() {\n    HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->TOMBSTONE = (Pair *)malloc(sizeof(Pair));\n    hashMap->TOMBSTONE->key = -1;\n    hashMap->TOMBSTONE->val = \"-1\";\n\n    return hashMap;\n}\n\n/* デストラクタ */\nvoid delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap->TOMBSTONE);\n    free(hashMap);\n}\n\n/* ハッシュ関数 */\nint hashFunc(HashMapOpenAddressing *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* 負荷率 */\ndouble loadFactor(HashMapOpenAddressing *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* key に対応するバケットインデックスを探す */\nint findBucket(HashMapOpenAddressing *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    int firstTombstone = -1;\n    // 線形プロービングを行い、空バケットに達したら終了\n    while (hashMap->buckets[index] != NULL) {\n        // key が見つかったら、対応するバケットのインデックスを返す\n        if (hashMap->buckets[index]->key == key) {\n            // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n            if (firstTombstone != -1) {\n                hashMap->buckets[firstTombstone] = hashMap->buckets[index];\n                hashMap->buckets[index] = hashMap->TOMBSTONE;\n                return firstTombstone; // 移動後のバケットインデックスを返す\n            }\n            return index; // バケットのインデックスを返す\n        }\n        // 最初に見つかった削除マークを記録\n        if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) {\n            firstTombstone = index;\n        }\n        // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n        index = (index + 1) % hashMap->capacity;\n    }\n    // key が存在しない場合は追加位置のインデックスを返す\n    return firstTombstone == -1 ? index : firstTombstone;\n}\n\n/* 検索操作 */\nchar *get(HashMapOpenAddressing *hashMap, int key) {\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(hashMap, key);\n    // キーと値の組が見つかったら、対応する val を返す\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        return hashMap->buckets[index]->val;\n    }\n    // キーと値の組が存在しない場合は空文字列を返す\n    return \"\";\n}\n\n/* 追加操作 */\nvoid put(HashMapOpenAddressing *hashMap, int key, char *val) {\n    // 負荷率がしきい値を超えたら、リサイズを実行\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(hashMap, key);\n    // キーと値の組が見つかったら、val を上書きして返す\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        free(hashMap->buckets[index]->val);\n        hashMap->buckets[index]->val = (char *)malloc(sizeof(strlen(val) + 1));\n        strcpy(hashMap->buckets[index]->val, val);\n        hashMap->buckets[index]->val[strlen(val)] = '\\0';\n        return;\n    }\n    // キーと値の組が存在しない場合は、その組を追加する\n    Pair *pair = (Pair *)malloc(sizeof(Pair));\n    pair->key = key;\n    pair->val = (char *)malloc(sizeof(strlen(val) + 1));\n    strcpy(pair->val, val);\n    pair->val[strlen(val)] = '\\0';\n\n    hashMap->buckets[index] = pair;\n    hashMap->size++;\n}\n\n/* 削除操作 */\nvoid removeItem(HashMapOpenAddressing *hashMap, int key) {\n    // key に対応するバケットインデックスを探す\n    int index = findBucket(hashMap, key);\n    // キーと値の組が見つかったら、削除マーカーで上書きする\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        Pair *pair = hashMap->buckets[index];\n        free(pair->val);\n        free(pair);\n        hashMap->buckets[index] = hashMap->TOMBSTONE;\n        hashMap->size--;\n    }\n}\n\n/* ハッシュテーブルを拡張 */\nvoid extend(HashMapOpenAddressing *hashMap) {\n    // 元のハッシュテーブルを一時保存\n    Pair **bucketsTmp = hashMap->buckets;\n    int oldCapacity = hashMap->capacity;\n    // リサイズ後の新しいハッシュテーブルを初期化\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->size = 0;\n    // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for (int i = 0; i < oldCapacity; i++) {\n        Pair *pair = bucketsTmp[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            put(hashMap, pair->key, pair->val);\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(bucketsTmp);\n}\n\n/* ハッシュテーブルを出力 */\nvoid print(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair == NULL) {\n            printf(\"NULL\\n\");\n        } else if (pair == hashMap->TOMBSTONE) {\n            printf(\"TOMBSTONE\\n\");\n        } else {\n            printf(\"%d -> %s\\n\", pair->key, pair->val);\n        }\n    }\n}\n
hash_map_open_addressing.kt
/* オープンアドレス法ハッシュテーブル */\nclass HashMapOpenAddressing {\n    private var size: Int               // キーと値のペア数\n    private var capacity: Int           // ハッシュテーブル容量\n    private val loadThres: Double       // リサイズを発動する負荷率のしきい値\n    private val extendRatio: Int        // 拡張倍率\n    private var buckets: Array<Pair?>   // バケット配列\n    private val TOMBSTONE: Pair         // 削除済みマーク\n\n    /* コンストラクタ */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = arrayOfNulls(capacity)\n        TOMBSTONE = Pair(-1, \"-1\")\n    }\n\n    /* ハッシュ関数 */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* 負荷率 */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* key に対応するバケットインデックスを探す */\n    fun findBucket(key: Int): Int {\n        var index = hashFunc(key)\n        var firstTombstone = -1\n        // 線形プロービングを行い、空バケットに達したら終了\n        while (buckets[index] != null) {\n            // key が見つかったら、対応するバケットのインデックスを返す\n            if (buckets[index]?.key == key) {\n                // 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // 移動後のバケットインデックスを返す\n                }\n                return index // バケットのインデックスを返す\n            }\n            // 最初に見つかった削除マークを記録\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index\n            }\n            // バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n            index = (index + 1) % capacity\n        }\n        // key が存在しない場合は追加位置のインデックスを返す\n        return if (firstTombstone == -1) index else firstTombstone\n    }\n\n    /* 検索操作 */\n    fun get(key: Int): String? {\n        // key に対応するバケットインデックスを探す\n        val index = findBucket(key)\n        // キーと値の組が見つかったら、対応する val を返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index]?._val\n        }\n        // キーと値の組が存在しなければ null を返す\n        return null\n    }\n\n    /* 追加操作 */\n    fun put(key: Int, _val: String) {\n        // 負荷率がしきい値を超えたら、リサイズを実行\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        // key に対応するバケットインデックスを探す\n        val index = findBucket(key)\n        // キーと値の組が見つかったら、val を上書きして返す\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index]!!._val = _val\n            return\n        }\n        // キーと値の組が存在しない場合は、その組を追加する\n        buckets[index] = Pair(key, _val)\n        size++\n    }\n\n    /* 削除操作 */\n    fun remove(key: Int) {\n        // key に対応するバケットインデックスを探す\n        val index = findBucket(key)\n        // キーと値の組が見つかったら、削除マーカーで上書きする\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE\n            size--\n        }\n    }\n\n    /* ハッシュテーブルを拡張 */\n    fun extend() {\n        // 元のハッシュテーブルを一時保存\n        val bucketsTmp = buckets\n        // リサイズ後の新しいハッシュテーブルを初期化\n        capacity *= extendRatio\n        buckets = arrayOfNulls(capacity)\n        size = 0\n        // キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n        for (pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* ハッシュテーブルを出力 */\n    fun print() {\n        for (pair in buckets) {\n            if (pair == null) {\n                println(\"null\")\n            } else if (pair == TOMBSTONE) {\n                println(\"TOMESTOME\")\n            } else {\n                println(\"${pair.key} -> ${pair._val}\")\n            }\n        }\n    }\n}\n
hash_map_open_addressing.rb
### オープンアドレス法ハッシュテーブル ###\nclass HashMapOpenAddressing\n  TOMBSTONE = Pair.new(-1, '-1') # 削除済みマーク\n\n  ### コンストラクタ ###\n  def initialize\n    @size = 0 # キーと値のペア数\n    @capacity = 4 # ハッシュテーブル容量\n    @load_thres = 2.0 / 3.0 # リサイズを発動する負荷率のしきい値\n    @extend_ratio = 2 # 拡張倍率\n    @buckets = Array.new(@capacity) # バケット配列\n  end\n\n  ### ハッシュ関数 ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### 負荷率 ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### key に対応するバケットインデックスを検索 ###\n  def find_bucket(key)\n    index = hash_func(key)\n    first_tombstone = -1\n    # 線形プロービングを行い、空バケットに達したら終了\n    while !@buckets[index].nil?\n      # key が見つかったら、対応するバケットのインデックスを返す\n      if @buckets[index].key == key\n        # 以前に削除マークが見つかっていれば、そのインデックスへキーと値のペアを移動\n        if first_tombstone != -1\n          @buckets[first_tombstone] = @buckets[index]\n          @buckets[index] = TOMBSTONE\n          return first_tombstone # 移動後のバケットインデックスを返す\n        end\n        return index # バケットのインデックスを返す\n      end\n      # 最初に見つかった削除マークを記録\n      first_tombstone = index if first_tombstone == -1 && @buckets[index] == TOMBSTONE\n      # バケットのインデックスを計算し、末尾を越えたら先頭に戻る\n      index = (index + 1) % @capacity\n    end\n    # key が存在しない場合は追加位置のインデックスを返す\n    first_tombstone == -1 ? index : first_tombstone\n  end\n\n  ### 検索操作 ###\n  def get(key)\n    # key に対応するバケットインデックスを探す\n    index = find_bucket(key)\n    # キーと値の組が見つかったら、対応する val を返す\n    return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index])\n    # キーと値のペアが存在しない場合は `nil` を返す\n    nil\n  end\n\n  ### 追加操作 ###\n  def put(key, val)\n    # 負荷率がしきい値を超えたら、リサイズを実行\n    extend if load_factor > @load_thres\n    # key に対応するバケットインデックスを探す\n    index = find_bucket(key)\n    # キーと値のペアが見つかった場合は、`val` を上書きして返す\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index].val = val\n      return\n    end\n    # キーと値の組が存在しない場合は、その組を追加する\n    @buckets[index] = Pair.new(key, val)\n    @size += 1\n  end\n\n  ### 削除操作 ###\n  def remove(key)\n    # key に対応するバケットインデックスを探す\n    index = find_bucket(key)\n    # キーと値の組が見つかったら、削除マーカーで上書きする\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index] = TOMBSTONE\n      @size -= 1\n    end\n  end\n\n  ### ハッシュテーブルを拡張 ###\n  def extend\n    # 元のハッシュテーブルを一時保存\n    buckets_tmp = @buckets\n    # リサイズ後の新しいハッシュテーブルを初期化\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity)\n    @size = 0\n    # キーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移す\n    for pair in buckets_tmp\n      put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair)\n    end\n  end\n\n  ### ハッシュテーブルを出力 ###\n  def print\n    for pair in @buckets\n      if pair.nil?\n        puts \"Nil\"\n      elsif pair == TOMBSTONE\n        puts \"TOMBSTONE\"\n      else\n        puts \"#{pair.key} -> #{pair.val}\"\n      end\n    end\n  end\nend\n
","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#2","level":3,"title":"2.   二次探索","text":"

二次探索は線形探索に似ており、オープンアドレッシングの一般的な戦略の 1 つです。衝突が発生したとき、二次探索では単純に固定歩数を飛ばすのではなく、「探索回数の二乗」に相当する歩数、すなわち \\(1, 4, 9, \\dots\\) 歩を飛ばします。

二次探索には主に次の利点があります。

  • 二次探索は、探索回数の二乗の距離を飛ばすことで、線形探索のクラスタリング効果を緩和しようとします。
  • 二次探索はより大きな距離を飛ばして空き位置を探すため、データ分布がより均一になるのに役立ちます。

しかし、二次探索は完璧ではありません。

  • 依然としてクラスタリング現象は存在し、ある位置が他の位置より占有されやすいことがあります。
  • 二乗の増加により、二次探索はハッシュテーブル全体を探索できない可能性があります。これは、ハッシュテーブルに空バケットがあっても、二次探索ではそこに到達できないことがあることを意味します。
","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#3","level":3,"title":"3.   多重ハッシュ","text":"

その名のとおり、多重ハッシュ法では複数のハッシュ関数 \\(f_1(x)\\)、\\(f_2(x)\\)、\\(f_3(x)\\)、\\(\\dots\\) を使って探索を行います。

  • 要素の挿入:ハッシュ関数 \\(f_1(x)\\) で衝突が発生した場合は、\\(f_2(x)\\) を試し、以下同様に、空き位置が見つかるまで続けてから要素を挿入します。
  • 要素の検索:同じハッシュ関数の順序で探索し、対象要素が見つかった時点で返します。空き位置に遭遇するか、すべてのハッシュ関数を試しても見つからない場合は、ハッシュテーブル内にその要素は存在しないため、 None を返します。

線形探索と比べると、多重ハッシュ法はクラスタリングを起こしにくい一方で、複数のハッシュ関数により追加の計算量が発生します。

Tip

注意してください。オープンアドレッシング(線形探索、二次探索、多重ハッシュ)のハッシュテーブルには、いずれも「要素を直接削除できない」という問題があります。

","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#623","level":2,"title":"6.2.3   プログラミング言語の選択","text":"

各種プログラミング言語は異なるハッシュテーブル実装戦略を採用しています。以下にいくつか例を挙げます。

  • Python はオープンアドレッシングを採用しています。辞書 dict は疑似乱数を用いて探索します。
  • Java はチェイン法を採用しています。JDK 1.8 以降、HashMap 内の配列長が 64 に達し、かつ連結リスト長が 8 に達すると、連結リストは検索性能を高めるため赤黒木に変換されます。
  • Go はチェイン法を採用しています。Go では各バケットに最大 8 個のキーと値のペアを格納でき、容量を超えるとオーバーフローバケットを連結します。オーバーフローバケットが多すぎる場合は、性能を確保するために特殊な等量拡張操作を実行します。
","path":["第 6 章   ハッシュテーブル","6.2   ハッシュ衝突"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   ハッシュテーブル","text":"

ハッシュテーブル(hash table)は、散列表とも呼ばれ、キー key と値 value の対応関係を構築することで、高効率な要素検索を実現します。具体的には、ハッシュテーブルにキー key を入力すると、対応する値 value を \\(O(1)\\) 時間で取得できます。

以下の図に示すように、\\(n\\) 人の学生がいるとし、各学生は「名前」と「学籍番号」の 2 つの情報を持っています。もし「学籍番号を入力すると対応する名前を返す」という検索機能を実現したいなら、下図のようなハッシュテーブルを用いることができます。

図 6-1   ハッシュテーブルの抽象表現

ハッシュテーブルのほかに、配列や連結リストでも検索機能を実現できます。それらの効率比較を次の表に示します。

  • 要素の追加:要素を配列(連結リスト)の末尾に追加するだけでよく、\\(O(1)\\) 時間です。
  • 要素の検索:配列(連結リスト)は無秩序なので、すべての要素を走査する必要があり、\\(O(n)\\) 時間かかります。
  • 要素の削除:先に要素を検索してから配列(連結リスト)から削除する必要があり、\\(O(n)\\) 時間かかります。

表 6-1   要素検索効率の比較

配列 連結リスト ハッシュテーブル 要素の検索 \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) 要素の追加 \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) 要素の削除 \\(O(n)\\) \\(O(n)\\) \\(O(1)\\)

以上から分かるように、ハッシュテーブルにおける追加・削除・検索・更新の時間計算量はいずれも \\(O(1)\\) であり、非常に高効率です。

","path":["第 6 章   ハッシュテーブル","6.1   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/hash_map/#611","level":2,"title":"6.1.1   ハッシュテーブルの基本操作","text":"

ハッシュテーブルの一般的な操作には、初期化、検索、キーと値のペアの追加、キーと値のペアの削除などがあります。コード例は以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
# ハッシュテーブルを初期化\nhmap: dict = {}\n\n# 追加操作\n# ハッシュテーブルにキーと値のペア (key, value) を追加\nhmap[12836] = \"シャオハ\"\nhmap[15937] = \"シャオルオ\"\nhmap[16750] = \"シャオスワン\"\nhmap[13276] = \"シャオファ\"\nhmap[10583] = \"シャオヤー\"\n\n# 検索操作\n# ハッシュテーブルにキー key を入力し、値 value を取得\nname: str = hmap[15937]\n\n# 削除操作\n# ハッシュテーブルからキーと値のペア (key, value) を削除\nhmap.pop(10583)\n
hash_map.cpp
/* ハッシュテーブルを初期化 */\nunordered_map<int, string> map;\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap[12836] = \"シャオハ\";\nmap[15937] = \"シャオルオ\";\nmap[16750] = \"シャオスワン\";\nmap[13276] = \"シャオファ\";\nmap[10583] = \"シャオヤー\";\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nstring name = map[15937];\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.erase(10583);\n
hash_map.java
/* ハッシュテーブルを初期化 */\nMap<Integer, String> map = new HashMap<>();\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap.put(12836, \"シャオハ\");\nmap.put(15937, \"シャオルオ\");\nmap.put(16750, \"シャオスワン\");\nmap.put(13276, \"シャオファ\");\nmap.put(10583, \"シャオヤー\");\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nString name = map.get(15937);\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.remove(10583);\n
hash_map.cs
/* ハッシュテーブルを初期化 */\nDictionary<int, string> map = new() {\n    /* 追加操作 */\n    // ハッシュテーブルにキーと値のペア (key, value) を追加\n    { 12836, \"シャオハ\" },\n    { 15937, \"シャオルオ\" },\n    { 16750, \"シャオスワン\" },\n    { 13276, \"シャオファ\" },\n    { 10583, \"シャオヤー\" }\n};\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nstring name = map[15937];\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.Remove(10583);\n
hash_map_test.go
/* ハッシュテーブルを初期化 */\nhmap := make(map[int]string)\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nhmap[12836] = \"シャオハ\"\nhmap[15937] = \"シャオルオ\"\nhmap[16750] = \"シャオスワン\"\nhmap[13276] = \"シャオファ\"\nhmap[10583] = \"シャオヤー\"\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nname := hmap[15937]\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\ndelete(hmap, 10583)\n
hash_map.swift
/* ハッシュテーブルを初期化 */\nvar map: [Int: String] = [:]\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap[12836] = \"シャオハ\"\nmap[15937] = \"シャオルオ\"\nmap[16750] = \"シャオスワン\"\nmap[13276] = \"シャオファ\"\nmap[10583] = \"シャオヤー\"\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nlet name = map[15937]!\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.removeValue(forKey: 10583)\n
hash_map.js
/* ハッシュテーブルを初期化 */\nconst map = new Map();\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap.set(12836, 'シャオハ');\nmap.set(15937, 'シャオルオ');\nmap.set(16750, 'シャオスワン');\nmap.set(13276, 'シャオファ');\nmap.set(10583, 'シャオヤー');\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nlet name = map.get(15937);\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.delete(10583);\n
hash_map.ts
/* ハッシュテーブルを初期化 */\nconst map = new Map<number, string>();\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap.set(12836, 'シャオハ');\nmap.set(15937, 'シャオルオ');\nmap.set(16750, 'シャオスワン');\nmap.set(13276, 'シャオファ');\nmap.set(10583, 'シャオヤー');\nconsole.info('\\n追加後のハッシュテーブルは次のとおりです\\nKey -> Value');\nconsole.info(map);\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nlet name = map.get(15937);\nconsole.info('\\n学籍番号 15937 を入力し、名前を検索: ' + name);\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.delete(10583);\nconsole.info('\\n10583 を削除した後のハッシュテーブル\\nKey -> Value');\nconsole.info(map);\n
hash_map.dart
/* ハッシュテーブルを初期化 */\nMap<int, String> map = {};\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap[12836] = \"シャオハ\";\nmap[15937] = \"シャオルオ\";\nmap[16750] = \"シャオスワン\";\nmap[13276] = \"シャオファ\";\nmap[10583] = \"シャオヤー\";\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nString name = map[15937];\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.remove(10583);\n
hash_map.rs
use std::collections::HashMap;\n\n/* ハッシュテーブルを初期化 */\nlet mut map: HashMap<i32, String> = HashMap::new();\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap.insert(12836, \"シャオハ\".to_string());\nmap.insert(15937, \"シャオルオ\".to_string());\nmap.insert(16750, \"シャオスワン\".to_string());\nmap.insert(13279, \"シャオファ\".to_string());\nmap.insert(10583, \"シャオヤー\".to_string());\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nlet _name: Option<&String> = map.get(&15937);\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nlet _removed_value: Option<String> = map.remove(&10583);\n
hash_map.c
// C には組み込みのハッシュテーブルはありません\n
hash_map.kt
/* ハッシュテーブルを初期化 */\nval map = HashMap<Int,String>()\n\n/* 追加操作 */\n// ハッシュテーブルにキーと値のペア (key, value) を追加\nmap[12836] = \"シャオハ\"\nmap[15937] = \"シャオルオ\"\nmap[16750] = \"シャオスワン\"\nmap[13276] = \"シャオファ\"\nmap[10583] = \"シャオヤー\"\n\n/* 検索操作 */\n// ハッシュテーブルにキー key を入力し、値 value を取得\nval name = map[15937]\n\n/* 削除操作 */\n// ハッシュテーブルからキーと値のペア (key, value) を削除\nmap.remove(10583)\n
hash_map.rb
# ハッシュテーブルを初期化\nhmap = {}\n\n# 追加操作\n# ハッシュテーブルにキーと値のペア (key, value) を追加\nhmap[12836] = \"シャオハ\"\nhmap[15937] = \"シャオルオ\"\nhmap[16750] = \"シャオスワン\"\nhmap[13276] = \"シャオファ\"\nhmap[10583] = \"シャオヤー\"\n\n# 検索操作\n# ハッシュテーブルにキー key を入力し、値 value を取得\nname = hmap[15937]\n\n# 削除操作\n# ハッシュテーブルからキーと値のペア (key, value) を削除\nhmap.delete(10583)\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

ハッシュテーブルには、キーと値のペア、キー、値を走査する 3 つの一般的な方法があります。コード例は以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
# ハッシュテーブルを走査\n# キーと値のペア key->value を走査\nfor key, value in hmap.items():\n    print(key, \"->\", value)\n# キー key のみを走査\nfor key in hmap.keys():\n    print(key)\n# 値 value のみを走査\nfor value in hmap.values():\n    print(value)\n
hash_map.cpp
/* ハッシュテーブルを走査 */\n// キーと値のペア key->value を走査\nfor (auto kv: map) {\n    cout << kv.first << \" -> \" << kv.second << endl;\n}\n// イテレータを使って key->value を走査\nfor (auto iter = map.begin(); iter != map.end(); iter++) {\n    cout << iter->first << \"->\" << iter->second << endl;\n}\n
hash_map.java
/* ハッシュテーブルを走査 */\n// キーと値のペア key->value を走査\nfor (Map.Entry <Integer, String> kv: map.entrySet()) {\n    System.out.println(kv.getKey() + \" -> \" + kv.getValue());\n}\n// キー key のみを走査\nfor (int key: map.keySet()) {\n    System.out.println(key);\n}\n// 値 value のみを走査\nfor (String val: map.values()) {\n    System.out.println(val);\n}\n
hash_map.cs
/* ハッシュテーブルを走査 */\n// キーと値のペア Key->Value を走査\nforeach (var kv in map) {\n    Console.WriteLine(kv.Key + \" -> \" + kv.Value);\n}\n// キー key のみを走査\nforeach (int key in map.Keys) {\n    Console.WriteLine(key);\n}\n// 値 value のみを走査\nforeach (string val in map.Values) {\n    Console.WriteLine(val);\n}\n
hash_map_test.go
/* ハッシュテーブルを走査 */\n// キーと値のペア key->value を走査\nfor key, value := range hmap {\n    fmt.Println(key, \"->\", value)\n}\n// キー key のみを走査\nfor key := range hmap {\n    fmt.Println(key)\n}\n// 値 value のみを走査\nfor _, value := range hmap {\n    fmt.Println(value)\n}\n
hash_map.swift
/* ハッシュテーブルを走査 */\n// キーと値のペア Key->Value を走査\nfor (key, value) in map {\n    print(\"\\(key) -> \\(value)\")\n}\n// キー Key のみを走査\nfor key in map.keys {\n    print(key)\n}\n// 値 Value のみを走査\nfor value in map.values {\n    print(value)\n}\n
hash_map.js
/* ハッシュテーブルを走査 */\nconsole.info('\\nキーと値のペア Key->Value を走査');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nキー Key のみを走査');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\n値 Value のみを走査');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
hash_map.ts
/* ハッシュテーブルを走査 */\nconsole.info('\\nキーと値のペア Key->Value を走査');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\nキー Key のみを走査');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\n値 Value のみを走査');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
hash_map.dart
/* ハッシュテーブルを走査 */\n// キーと値のペア Key->Value を走査\nmap.forEach((key, value) {\n  print('$key -> $value');\n});\n\n// キー Key のみを走査\nmap.keys.forEach((key) {\n  print(key);\n});\n\n// 値 Value のみを走査\nmap.values.forEach((value) {\n  print(value);\n});\n
hash_map.rs
/* ハッシュテーブルを走査 */\n// キーと値のペア Key->Value を走査\nfor (key, value) in &map {\n    println!(\"{key} -> {value}\");\n}\n\n// キー Key のみを走査\nfor key in map.keys() {\n    println!(\"{key}\");\n}\n\n// 値 Value のみを走査\nfor value in map.values() {\n    println!(\"{value}\");\n}\n
hash_map.c
// C には組み込みのハッシュテーブルはありません\n
hash_map.kt
/* ハッシュテーブルを走査 */\n// キーと値のペア key->value を走査\nfor ((key, value) in map) {\n    println(\"$key -> $value\")\n}\n// キー key のみを走査\nfor (key in map.keys) {\n    println(key)\n}\n// 値 value のみを走査\nfor (_val in map.values) {\n    println(_val)\n}\n
hash_map.rb
# ハッシュテーブルを走査\n# キーと値のペア key->value を走査\nhmap.entries.each { |key, value| puts \"#{key} -> #{value}\" }\n\n# キー key のみを走査\nhmap.keys.each { |key| puts key }\n\n# 値 value のみを走査\nhmap.values.each { |val| puts val }\n
可視化実行

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 6 章   ハッシュテーブル","6.1   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/hash_map/#612","level":2,"title":"6.1.2   ハッシュテーブルの簡単な実装","text":"

まずは最も単純なケースとして、**1 つの配列だけでハッシュテーブルを実装する**ことを考えます。ハッシュテーブルでは、配列中の各空き位置をバケット(bucket)と呼び、各バケットには 1 つのキーと値のペアを格納できます。したがって、検索操作とは key に対応するバケットを見つけ、そのバケットから value を取得することです。

では、key に基づいて対応するバケットをどのように特定するのでしょうか。これはハッシュ関数(hash function)によって実現されます。ハッシュ関数の役割は、大きな入力空間をより小さな出力空間に写像することです。ハッシュテーブルでは、入力空間はすべての key 、出力空間はすべてのバケット(配列インデックス)です。言い換えると、key を入力すると、ハッシュ関数によってその key に対応するキーと値のペアの配列内での格納位置を求められます。

key を入力したとき、ハッシュ関数の計算過程は次の 2 段階に分かれます。

  1. あるハッシュアルゴリズム hash() を用いてハッシュ値を計算します。
  2. ハッシュ値をバケット数(配列長)capacity で剰余し、その key に対応するバケット(配列インデックス)index を求めます。
index = hash(key) % capacity\n

その後、index を使ってハッシュテーブル内の対応するバケットにアクセスし、value を取得できます。

配列長を capacity = 100 、ハッシュアルゴリズムを hash(key) = key とすると、ハッシュ関数は key % 100 となります。次の図では、key を学籍番号、value を名前の例として、ハッシュ関数の動作原理を示します。

図 6-2   ハッシュ関数の動作原理

以下のコードは、単純なハッシュテーブルを実装したものです。ここでは、キーと値のペアを表すために keyvalue をクラス Pair にまとめています。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_hash_map.py
class Pair:\n    \"\"\"キーと値の組\"\"\"\n\n    def __init__(self, key: int, val: str):\n        self.key = key\n        self.val = val\n\nclass ArrayHashMap:\n    \"\"\"配列ベースのハッシュテーブル\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        # 100 個のバケットを含む配列を初期化\n        self.buckets: list[Pair | None] = [None] * 100\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"ハッシュ関数\"\"\"\n        index = key % 100\n        return index\n\n    def get(self, key: int) -> str | None:\n        \"\"\"検索操作\"\"\"\n        index: int = self.hash_func(key)\n        pair: Pair = self.buckets[index]\n        if pair is None:\n            return None\n        return pair.val\n\n    def put(self, key: int, val: str):\n        \"\"\"追加と更新の操作\"\"\"\n        pair = Pair(key, val)\n        index: int = self.hash_func(key)\n        self.buckets[index] = pair\n\n    def remove(self, key: int):\n        \"\"\"削除操作\"\"\"\n        index: int = self.hash_func(key)\n        # None に設定し、削除を表す\n        self.buckets[index] = None\n\n    def entry_set(self) -> list[Pair]:\n        \"\"\"すべてのキーと値のペアを取得\"\"\"\n        result: list[Pair] = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair)\n        return result\n\n    def key_set(self) -> list[int]:\n        \"\"\"すべてのキーを取得\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.key)\n        return result\n\n    def value_set(self) -> list[str]:\n        \"\"\"すべての値を取得\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.val)\n        return result\n\n    def print(self):\n        \"\"\"ハッシュテーブルを出力\"\"\"\n        for pair in self.buckets:\n            if pair is not None:\n                print(pair.key, \"->\", pair.val)\n
array_hash_map.cpp
/* キーと値の組 */\nstruct Pair {\n  public:\n    int key;\n    string val;\n    Pair(int key, string val) {\n        this->key = key;\n        this->val = val;\n    }\n};\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n  private:\n    vector<Pair *> buckets;\n\n  public:\n    ArrayHashMap() {\n        // 100 個のバケットを含む配列を初期化\n        buckets = vector<Pair *>(100);\n    }\n\n    ~ArrayHashMap() {\n        // メモリを解放する\n        for (const auto &bucket : buckets) {\n            delete bucket;\n        }\n        buckets.clear();\n    }\n\n    /* ハッシュ関数 */\n    int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* 検索操作 */\n    string get(int key) {\n        int index = hashFunc(key);\n        Pair *pair = buckets[index];\n        if (pair == nullptr)\n            return \"\";\n        return pair->val;\n    }\n\n    /* 追加操作 */\n    void put(int key, string val) {\n        Pair *pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* 削除操作 */\n    void remove(int key) {\n        int index = hashFunc(key);\n        // メモリを解放して nullptr に設定する\n        delete buckets[index];\n        buckets[index] = nullptr;\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    vector<Pair *> pairSet() {\n        vector<Pair *> pairSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                pairSet.push_back(pair);\n            }\n        }\n        return pairSet;\n    }\n\n    /* すべてのキーを取得 */\n    vector<int> keySet() {\n        vector<int> keySet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                keySet.push_back(pair->key);\n            }\n        }\n        return keySet;\n    }\n\n    /* すべての値を取得 */\n    vector<string> valueSet() {\n        vector<string> valueSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                valueSet.push_back(pair->val);\n            }\n        }\n        return valueSet;\n    }\n\n    /* ハッシュテーブルを出力 */\n    void print() {\n        for (Pair *kv : pairSet()) {\n            cout << kv->key << \" -> \" << kv->val << endl;\n        }\n    }\n};\n
array_hash_map.java
/* キーと値の組 */\nclass Pair {\n    public int key;\n    public String val;\n\n    public Pair(int key, String val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    private List<Pair> buckets;\n\n    public ArrayHashMap() {\n        // 100 個のバケットを含む配列を初期化\n        buckets = new ArrayList<>();\n        for (int i = 0; i < 100; i++) {\n            buckets.add(null);\n        }\n    }\n\n    /* ハッシュ関数 */\n    private int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* 検索操作 */\n    public String get(int key) {\n        int index = hashFunc(key);\n        Pair pair = buckets.get(index);\n        if (pair == null)\n            return null;\n        return pair.val;\n    }\n\n    /* 追加操作 */\n    public void put(int key, String val) {\n        Pair pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets.set(index, pair);\n    }\n\n    /* 削除操作 */\n    public void remove(int key) {\n        int index = hashFunc(key);\n        // null に設定し、削除を表す\n        buckets.set(index, null);\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    public List<Pair> pairSet() {\n        List<Pair> pairSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                pairSet.add(pair);\n        }\n        return pairSet;\n    }\n\n    /* すべてのキーを取得 */\n    public List<Integer> keySet() {\n        List<Integer> keySet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                keySet.add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* すべての値を取得 */\n    public List<String> valueSet() {\n        List<String> valueSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                valueSet.add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* ハッシュテーブルを出力 */\n    public void print() {\n        for (Pair kv : pairSet()) {\n            System.out.println(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
array_hash_map.cs
/* キーと値の組 int->string */\nclass Pair(int key, string val) {\n    public int key = key;\n    public string val = val;\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    List<Pair?> buckets;\n    public ArrayHashMap() {\n        // 100 個のバケットを含む配列を初期化\n        buckets = [];\n        for (int i = 0; i < 100; i++) {\n            buckets.Add(null);\n        }\n    }\n\n    /* ハッシュ関数 */\n    int HashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* 検索操作 */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        Pair? pair = buckets[index];\n        if (pair == null) return null;\n        return pair.val;\n    }\n\n    /* 追加操作 */\n    public void Put(int key, string val) {\n        Pair pair = new(key, val);\n        int index = HashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* 削除操作 */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // null に設定し、削除を表す\n        buckets[index] = null;\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    public List<Pair> PairSet() {\n        List<Pair> pairSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                pairSet.Add(pair);\n        }\n        return pairSet;\n    }\n\n    /* すべてのキーを取得 */\n    public List<int> KeySet() {\n        List<int> keySet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                keySet.Add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* すべての値を取得 */\n    public List<string> ValueSet() {\n        List<string> valueSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                valueSet.Add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* ハッシュテーブルを出力 */\n    public void Print() {\n        foreach (Pair kv in PairSet()) {\n            Console.WriteLine(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
array_hash_map.go
/* キーと値の組 */\ntype pair struct {\n    key int\n    val string\n}\n\n/* 配列ベースのハッシュテーブル */\ntype arrayHashMap struct {\n    buckets []*pair\n}\n\n/* ハッシュテーブルを初期化 */\nfunc newArrayHashMap() *arrayHashMap {\n    // 100 個のバケットを含む配列を初期化\n    buckets := make([]*pair, 100)\n    return &arrayHashMap{buckets: buckets}\n}\n\n/* ハッシュ関数 */\nfunc (a *arrayHashMap) hashFunc(key int) int {\n    index := key % 100\n    return index\n}\n\n/* 検索操作 */\nfunc (a *arrayHashMap) get(key int) string {\n    index := a.hashFunc(key)\n    pair := a.buckets[index]\n    if pair == nil {\n        return \"Not Found\"\n    }\n    return pair.val\n}\n\n/* 追加操作 */\nfunc (a *arrayHashMap) put(key int, val string) {\n    pair := &pair{key: key, val: val}\n    index := a.hashFunc(key)\n    a.buckets[index] = pair\n}\n\n/* 削除操作 */\nfunc (a *arrayHashMap) remove(key int) {\n    index := a.hashFunc(key)\n    // nil に設定し、削除を表す\n    a.buckets[index] = nil\n}\n\n/* すべてのキーのペアを取得する */\nfunc (a *arrayHashMap) pairSet() []*pair {\n    var pairs []*pair\n    for _, pair := range a.buckets {\n        if pair != nil {\n            pairs = append(pairs, pair)\n        }\n    }\n    return pairs\n}\n\n/* すべてのキーを取得 */\nfunc (a *arrayHashMap) keySet() []int {\n    var keys []int\n    for _, pair := range a.buckets {\n        if pair != nil {\n            keys = append(keys, pair.key)\n        }\n    }\n    return keys\n}\n\n/* すべての値を取得 */\nfunc (a *arrayHashMap) valueSet() []string {\n    var values []string\n    for _, pair := range a.buckets {\n        if pair != nil {\n            values = append(values, pair.val)\n        }\n    }\n    return values\n}\n\n/* ハッシュテーブルを出力 */\nfunc (a *arrayHashMap) print() {\n    for _, pair := range a.buckets {\n        if pair != nil {\n            fmt.Println(pair.key, \"->\", pair.val)\n        }\n    }\n}\n
array_hash_map.swift
/* キーと値の組 */\nclass Pair: Equatable {\n    public var key: Int\n    public var val: String\n\n    public init(key: Int, val: String) {\n        self.key = key\n        self.val = val\n    }\n\n    public static func == (lhs: Pair, rhs: Pair) -> Bool {\n        lhs.key == rhs.key && lhs.val == rhs.val\n    }\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    private var buckets: [Pair?]\n\n    init() {\n        // 100 個のバケットを含む配列を初期化\n        buckets = Array(repeating: nil, count: 100)\n    }\n\n    /* ハッシュ関数 */\n    private func hashFunc(key: Int) -> Int {\n        let index = key % 100\n        return index\n    }\n\n    /* 検索操作 */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let pair = buckets[index]\n        return pair?.val\n    }\n\n    /* 追加操作 */\n    func put(key: Int, val: String) {\n        let pair = Pair(key: key, val: val)\n        let index = hashFunc(key: key)\n        buckets[index] = pair\n    }\n\n    /* 削除操作 */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        // nil に設定し、削除を表す\n        buckets[index] = nil\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    func pairSet() -> [Pair] {\n        buckets.compactMap { $0 }\n    }\n\n    /* すべてのキーを取得 */\n    func keySet() -> [Int] {\n        buckets.compactMap { $0?.key }\n    }\n\n    /* すべての値を取得 */\n    func valueSet() -> [String] {\n        buckets.compactMap { $0?.val }\n    }\n\n    /* ハッシュテーブルを出力 */\n    func print() {\n        for pair in pairSet() {\n            Swift.print(\"\\(pair.key) -> \\(pair.val)\")\n        }\n    }\n}\n
array_hash_map.js
/* キーと値の組 Number -> String */\nclass Pair {\n    constructor(key, val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    #buckets;\n    constructor() {\n        // 100 個のバケットを含む配列を初期化\n        this.#buckets = new Array(100).fill(null);\n    }\n\n    /* ハッシュ関数 */\n    #hashFunc(key) {\n        return key % 100;\n    }\n\n    /* 検索操作 */\n    get(key) {\n        let index = this.#hashFunc(key);\n        let pair = this.#buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* 追加操作 */\n    set(key, val) {\n        let index = this.#hashFunc(key);\n        this.#buckets[index] = new Pair(key, val);\n    }\n\n    /* 削除操作 */\n    delete(key) {\n        let index = this.#hashFunc(key);\n        // null に設定し、削除を表す\n        this.#buckets[index] = null;\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    entries() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* すべてのキーを取得 */\n    keys() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* すべての値を取得 */\n    values() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* ハッシュテーブルを出力 */\n    print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
array_hash_map.ts
/* キーと値の組 Number -> String */\nclass Pair {\n    public key: number;\n    public val: string;\n\n    constructor(key: number, val: string) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    private readonly buckets: (Pair | null)[];\n\n    constructor() {\n        // 100 個のバケットを含む配列を初期化\n        this.buckets = new Array(100).fill(null);\n    }\n\n    /* ハッシュ関数 */\n    private hashFunc(key: number): number {\n        return key % 100;\n    }\n\n    /* 検索操作 */\n    public get(key: number): string | null {\n        let index = this.hashFunc(key);\n        let pair = this.buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* 追加操作 */\n    public set(key: number, val: string) {\n        let index = this.hashFunc(key);\n        this.buckets[index] = new Pair(key, val);\n    }\n\n    /* 削除操作 */\n    public delete(key: number) {\n        let index = this.hashFunc(key);\n        // null に設定し、削除を表す\n        this.buckets[index] = null;\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    public entries(): (Pair | null)[] {\n        let arr: (Pair | null)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* すべてのキーを取得 */\n    public keys(): (number | undefined)[] {\n        let arr: (number | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* すべての値を取得 */\n    public values(): (string | undefined)[] {\n        let arr: (string | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* ハッシュテーブルを出力 */\n    public print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
array_hash_map.dart
/* キーと値の組 */\nclass Pair {\n  int key;\n  String val;\n  Pair(this.key, this.val);\n}\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n  late List<Pair?> _buckets;\n\n  ArrayHashMap() {\n    // 100 個のバケットを含む配列を初期化\n    _buckets = List.filled(100, null);\n  }\n\n  /* ハッシュ関数 */\n  int _hashFunc(int key) {\n    final int index = key % 100;\n    return index;\n  }\n\n  /* 検索操作 */\n  String? get(int key) {\n    final int index = _hashFunc(key);\n    final Pair? pair = _buckets[index];\n    if (pair == null) {\n      return null;\n    }\n    return pair.val;\n  }\n\n  /* 追加操作 */\n  void put(int key, String val) {\n    final Pair pair = Pair(key, val);\n    final int index = _hashFunc(key);\n    _buckets[index] = pair;\n  }\n\n  /* 削除操作 */\n  void remove(int key) {\n    final int index = _hashFunc(key);\n    _buckets[index] = null;\n  }\n\n  /* すべてのキーと値のペアを取得 */\n  List<Pair> pairSet() {\n    List<Pair> pairSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        pairSet.add(pair);\n      }\n    }\n    return pairSet;\n  }\n\n  /* すべてのキーを取得 */\n  List<int> keySet() {\n    List<int> keySet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        keySet.add(pair.key);\n      }\n    }\n    return keySet;\n  }\n\n  /* すべての値を取得 */\n  List<String> values() {\n    List<String> valueSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        valueSet.add(pair.val);\n      }\n    }\n    return valueSet;\n  }\n\n  /* ハッシュテーブルを出力 */\n  void printHashMap() {\n    for (final Pair kv in pairSet()) {\n      print(\"${kv.key} -> ${kv.val}\");\n    }\n  }\n}\n
array_hash_map.rs
/* キーと値の組 */\n#[derive(Debug, Clone, PartialEq)]\npub struct Pair {\n    pub key: i32,\n    pub val: String,\n}\n\n/* 配列ベースのハッシュテーブル */\npub struct ArrayHashMap {\n    buckets: Vec<Option<Pair>>,\n}\n\nimpl ArrayHashMap {\n    pub fn new() -> ArrayHashMap {\n        // 100 個のバケットを含む配列を初期化\n        Self {\n            buckets: vec![None; 100],\n        }\n    }\n\n    /* ハッシュ関数 */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % 100\n    }\n\n    /* 検索操作 */\n    pub fn get(&self, key: i32) -> Option<&String> {\n        let index = self.hash_func(key);\n        self.buckets[index].as_ref().map(|pair| &pair.val)\n    }\n\n    /* 追加操作 */\n    pub fn put(&mut self, key: i32, val: &str) {\n        let index = self.hash_func(key);\n        self.buckets[index] = Some(Pair {\n            key,\n            val: val.to_string(),\n        });\n    }\n\n    /* 削除操作 */\n    pub fn remove(&mut self, key: i32) {\n        let index = self.hash_func(key);\n        // None に設定し、削除を表す\n        self.buckets[index] = None;\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    pub fn entry_set(&self) -> Vec<&Pair> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref())\n            .collect()\n    }\n\n    /* すべてのキーを取得 */\n    pub fn key_set(&self) -> Vec<&i32> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.key))\n            .collect()\n    }\n\n    /* すべての値を取得 */\n    pub fn value_set(&self) -> Vec<&String> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.val))\n            .collect()\n    }\n\n    /* ハッシュテーブルを出力 */\n    pub fn print(&self) {\n        for pair in self.entry_set() {\n            println!(\"{} -> {}\", pair.key, pair.val);\n        }\n    }\n}\n
array_hash_map.c
/* キーと値の組 int->string */\ntypedef struct {\n    int key;\n    char *val;\n} Pair;\n\n/* 配列ベースのハッシュテーブル */\ntypedef struct {\n    Pair *buckets[MAX_SIZE];\n} ArrayHashMap;\n\n/* コンストラクタ */\nArrayHashMap *newArrayHashMap() {\n    ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap));\n    for (int i=0; i < MAX_SIZE; i++) {\n        hmap->buckets[i] = NULL;\n    }\n    return hmap;\n}\n\n/* デストラクタ */\nvoid delArrayHashMap(ArrayHashMap *hmap) {\n    for (int i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            free(hmap->buckets[i]->val);\n            free(hmap->buckets[i]);\n        }\n    }\n    free(hmap);\n}\n\n/* 追加操作 */\nvoid put(ArrayHashMap *hmap, const int key, const char *val) {\n    Pair *Pair = malloc(sizeof(Pair));\n    Pair->key = key;\n    Pair->val = malloc(strlen(val) + 1);\n    strcpy(Pair->val, val);\n\n    int index = hashFunc(key);\n    hmap->buckets[index] = Pair;\n}\n\n/* 削除操作 */\nvoid removeItem(ArrayHashMap *hmap, const int key) {\n    int index = hashFunc(key);\n    free(hmap->buckets[index]->val);\n    free(hmap->buckets[index]);\n    hmap->buckets[index] = NULL;\n}\n\n/* すべてのキーと値のペアを取得 */\nvoid pairSet(ArrayHashMap *hmap, MapSet *set) {\n    Pair *entries;\n    int i = 0, index = 0;\n    int total = 0;\n    /* 有効なキーと値のペア数を集計 */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    entries = malloc(sizeof(Pair) * total);\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            entries[index].key = hmap->buckets[i]->key;\n            entries[index].val = malloc(strlen(hmap->buckets[i]->val) + 1);\n            strcpy(entries[index].val, hmap->buckets[i]->val);\n            index++;\n        }\n    }\n    set->set = entries;\n    set->len = total;\n}\n\n/* すべてのキーを取得 */\nvoid keySet(ArrayHashMap *hmap, MapSet *set) {\n    int *keys;\n    int i = 0, index = 0;\n    int total = 0;\n    /* 有効なキーと値のペア数を集計 */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    keys = malloc(total * sizeof(int));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            keys[index] = hmap->buckets[i]->key;\n            index++;\n        }\n    }\n    set->set = keys;\n    set->len = total;\n}\n\n/* すべての値を取得 */\nvoid valueSet(ArrayHashMap *hmap, MapSet *set) {\n    char **vals;\n    int i = 0, index = 0;\n    int total = 0;\n    /* 有効なキーと値のペア数を集計 */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    vals = malloc(total * sizeof(char *));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            vals[index] = hmap->buckets[i]->val;\n            index++;\n        }\n    }\n    set->set = vals;\n    set->len = total;\n}\n\n/* ハッシュテーブルを出力 */\nvoid print(ArrayHashMap *hmap) {\n    int i;\n    MapSet set;\n    pairSet(hmap, &set);\n    Pair *entries = (Pair *)set.set;\n    for (i = 0; i < set.len; i++) {\n        printf(\"%d -> %s\\n\", entries[i].key, entries[i].val);\n    }\n    free(set.set);\n}\n
array_hash_map.kt
/* キーと値の組 */\nclass Pair(\n    var key: Int,\n    var _val: String\n)\n\n/* 配列ベースのハッシュテーブル */\nclass ArrayHashMap {\n    // 100 個のバケットを含む配列を初期化\n    private val buckets = arrayOfNulls<Pair>(100)\n\n    /* ハッシュ関数 */\n    fun hashFunc(key: Int): Int {\n        val index = key % 100\n        return index\n    }\n\n    /* 検索操作 */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val pair = buckets[index] ?: return null\n        return pair._val\n    }\n\n    /* 追加操作 */\n    fun put(key: Int, _val: String) {\n        val pair = Pair(key, _val)\n        val index = hashFunc(key)\n        buckets[index] = pair\n    }\n\n    /* 削除操作 */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        // null に設定し、削除を表す\n        buckets[index] = null\n    }\n\n    /* すべてのキーと値のペアを取得 */\n    fun pairSet(): MutableList<Pair> {\n        val pairSet = mutableListOf<Pair>()\n        for (pair in buckets) {\n            if (pair != null)\n                pairSet.add(pair)\n        }\n        return pairSet\n    }\n\n    /* すべてのキーを取得 */\n    fun keySet(): MutableList<Int> {\n        val keySet = mutableListOf<Int>()\n        for (pair in buckets) {\n            if (pair != null)\n                keySet.add(pair.key)\n        }\n        return keySet\n    }\n\n    /* すべての値を取得 */\n    fun valueSet(): MutableList<String> {\n        val valueSet = mutableListOf<String>()\n        for (pair in buckets) {\n            if (pair != null)\n                valueSet.add(pair._val)\n        }\n        return valueSet\n    }\n\n    /* ハッシュテーブルを出力 */\n    fun print() {\n        for (kv in pairSet()) {\n            val key = kv.key\n            val _val = kv._val\n            println(\"$key -> $_val\")\n        }\n    }\n}\n
array_hash_map.rb
### キーと値のペア ###\nclass Pair\n  attr_accessor :key, :val\n\n  def initialize(key, val)\n    @key = key\n    @val = val\n  end\nend\n\n### 配列で実装したハッシュテーブル ###\nclass ArrayHashMap\n  ### コンストラクタ ###\n  def initialize\n    # 100 個のバケットを含む配列を初期化\n    @buckets = Array.new(100)\n  end\n\n  ### ハッシュ関数 ###\n  def hash_func(key)\n    index = key % 100\n  end\n\n  ### 検索操作 ###\n  def get(key)\n    index = hash_func(key)\n    pair = @buckets[index]\n\n    return if pair.nil?\n    pair.val\n  end\n\n  ### 追加操作 ###\n  def put(key, val)\n    pair = Pair.new(key, val)\n    index = hash_func(key)\n    @buckets[index] = pair\n  end\n\n  ### 削除操作 ###\n  def remove(key)\n    index = hash_func(key)\n    # nil に設定し、削除を表す\n    @buckets[index] = nil\n  end\n\n  ### すべてのキーと値のペアを取得 ###\n  def entry_set\n    result = []\n    @buckets.each { |pair| result << pair unless pair.nil? }\n    result\n  end\n\n  ### すべてのキーを取得 ###\n  def key_set\n    result = []\n    @buckets.each { |pair| result << pair.key unless pair.nil? }\n    result\n  end\n\n  ### すべての値を取得 ###\n  def value_set\n    result = []\n    @buckets.each { |pair| result << pair.val unless pair.nil? }\n    result\n  end\n\n  ### ハッシュテーブルを出力 ###\n  def print\n    @buckets.each { |pair| puts \"#{pair.key} -> #{pair.val}\" unless pair.nil? }\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 6 章   ハッシュテーブル","6.1   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/hash_map/#613","level":2,"title":"6.1.3   ハッシュ衝突と拡張","text":"

本質的には、ハッシュ関数の役割は、すべての key からなる入力空間を、配列のすべてのインデックスからなる出力空間に写像することです。しかし、入力空間は多くの場合、出力空間よりはるかに大きいため、理論上は必ず「複数の入力が同じ出力に対応する」状況が存在します。

上の例のハッシュ関数では、入力 key の下 2 桁が同じであれば、出力結果も同じになります。たとえば、学籍番号 12836 と 20336 の 2 人の学生を検索すると、次の結果を得ます:

12836 % 100 = 36\n20336 % 100 = 36\n

次の図に示すように、2 つの学籍番号が同じ名前を指してしまっており、これは明らかに誤りです。このような、複数の入力が同じ出力に対応する状況をハッシュ衝突(hash collision)と呼びます。

図 6-3   ハッシュ衝突の例

容易に分かるように、ハッシュテーブルの容量 \\(n\\) が大きいほど、複数の key が同じバケットに割り当てられる確率は低くなり、衝突も少なくなります。したがって、ハッシュテーブルを拡張することでハッシュ衝突を減らせます。

次の図に示すように、拡張前はキーと値のペア (136, A)(236, D) が衝突していますが、拡張後は衝突が解消されます。

図 6-4   ハッシュテーブルの拡張

配列の拡張と同様に、ハッシュテーブルの拡張ではすべてのキーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移し替える必要があり、非常に時間がかかります。また、ハッシュテーブルの容量 capacity が変わるため、ハッシュ関数を使ってすべてのキーと値のペアの格納位置を再計算しなければならず、これによって拡張過程の計算コストがさらに増加します。そのため、プログラミング言語では通常、頻繁な拡張を防ぐために十分大きなハッシュテーブル容量をあらかじめ確保します。

負荷率(load factor)はハッシュテーブルにおける重要な概念であり、ハッシュテーブル内の要素数をバケット数で割ったものとして定義され、ハッシュ衝突の深刻さを測るために用いられます。また、ハッシュテーブル拡張の発動条件としてもよく使われます。例えば Java では、負荷率が \\(0.75\\) を超えると、システムはハッシュテーブルを元の \\(2\\) 倍に拡張します。

","path":["第 6 章   ハッシュテーブル","6.1   ハッシュテーブル"],"tags":[]},{"location":"chapter_hashing/summary/","level":1,"title":"6.4   まとめ","text":"","path":["第 6 章   ハッシュテーブル","6.4   まとめ"],"tags":[]},{"location":"chapter_hashing/summary/#1","level":3,"title":"1.   重要ポイントの振り返り","text":"
  • key を入力すると、ハッシュテーブルは \\(O(1)\\) 時間で value を検索でき、非常に高効率である。
  • 一般的なハッシュテーブルの操作には、検索、キーと値のペアの追加、キーと値のペアの削除、ハッシュテーブルの走査などがある。
  • ハッシュ関数は key を配列インデックスに写像し、それによって対応するバケットにアクセスして value を取得する。
  • 異なる 2 つの key が、ハッシュ関数を通した後に同じ配列インデックスになることがあり、検索結果の誤りを引き起こす。この現象をハッシュ衝突と呼ぶ。
  • ハッシュテーブルの容量が大きいほど、ハッシュ衝突の確率は低くなる。そのため、ハッシュテーブルを拡張することでハッシュ衝突を緩和できる。配列の拡張と同様に、ハッシュテーブルの拡張操作のコストは大きい。
  • 負荷率は、ハッシュテーブル内の要素数をバケット数で割ったものと定義され、ハッシュ衝突の深刻さを反映する。ハッシュテーブル拡張を発動する条件としてよく用いられる。
  • 連鎖方式では、単一要素を連結リストに変換し、衝突したすべての要素を同じ連結リストに格納する。しかし、連結リストが長すぎると検索効率が低下するため、さらに連結リストを赤黒木に変換して効率を高めることができる。
  • オープンアドレス法は複数回の探索によってハッシュ衝突を処理する。線形探索は固定のステップ幅を用いるが、要素を削除できず、クラスタリングが発生しやすいという欠点がある。二重ハッシュは複数のハッシュ関数を用いて探索するため、線形探索に比べてクラスタリングが起きにくいが、複数のハッシュ関数によって計算量が増える。
  • プログラミング言語ごとに、異なるハッシュテーブル実装が採用されている。たとえば、Java の HashMap は連鎖方式を使用し、Python の Dict はオープンアドレス法を採用している。
  • ハッシュテーブルでは、ハッシュアルゴリズムに決定性、高効率、均一分布という特徴が求められる。暗号学では、ハッシュアルゴリズムはさらに耐衝突性とアバランシェ効果も備えるべきである。
  • ハッシュアルゴリズムは通常、大きな素数を法として用い、ハッシュ値の均一分布を最大限に保証してハッシュ衝突を減らす。
  • 一般的なハッシュアルゴリズムには MD5、SHA-1、SHA-2、SHA-3 などがある。MD5 はファイル完全性の検証によく用いられ、SHA-2 はセキュリティ用途やプロトコルでよく用いられる。
  • プログラミング言語は通常、データ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュテーブル内のバケットインデックスの計算に用いる。通常、ハッシュ可能なのは不変オブジェクトだけである。
","path":["第 6 章   ハッシュテーブル","6.4   まとめ"],"tags":[]},{"location":"chapter_hashing/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:ハッシュテーブルの時間計算量が \\(O(n)\\) になるのはどのような場合ですか?

ハッシュ衝突が深刻な場合、ハッシュテーブルの時間計算量は \\(O(n)\\) に劣化する。ハッシュ関数の設計が適切で、容量設定が合理的で、衝突が比較的均等な場合、時間計算量は \\(O(1)\\) である。プログラミング言語組み込みのハッシュテーブルを使うとき、通常は時間計算量を \\(O(1)\\) とみなす。

Q:なぜハッシュ関数 \\(f(x) = x\\) を使わないのですか? そうすれば衝突は起きません。

\\(f(x) = x\\) というハッシュ関数では、各要素は一意のバケットインデックスに対応し、これは配列と等価である。しかし、入力空間は通常、出力空間(配列長)よりはるかに大きいため、ハッシュ関数の最後のステップはたいてい配列長での剰余になる。言い換えると、ハッシュテーブルの目的は、大きな状態空間をより小さな空間に写像し、\\(O(1)\\) の検索効率を提供することである。

Q:ハッシュテーブルの基礎実装は配列、連結リスト、二分木なのに、なぜそれらより高効率になり得るのですか?

まず、ハッシュテーブルは時間効率が高くなる一方で、空間効率は低くなる。ハッシュテーブルには、かなりの部分で未使用のメモリが存在する。

次に、時間効率が高くなるのは特定の利用場面に限られる。ある機能が同じ時間計算量で配列や連結リストによって実装できるなら、通常はハッシュテーブルより速い。これは、ハッシュ関数の計算にコストがかかり、時間計算量の定数項がより大きいからである。

最後に、ハッシュテーブルの時間計算量は劣化する可能性がある。たとえば連鎖方式では、連結リストや赤黒木で検索操作を行うため、なお \\(O(n)\\) 時間に劣化するリスクがある。

Q:二重ハッシュにも要素を直接削除できない欠点がありますか? 削除済みとマークした領域は再利用できますか?

二重ハッシュはオープンアドレス法の一種であり、オープンアドレス法はいずれも要素を直接削除できないという欠点があるため、削除のマーク付けが必要になる。削除済みとマークされた領域は再利用できる。新しい要素をハッシュテーブルに挿入し、ハッシュ関数によって削除済みとマークされた位置を見つけた場合、その位置は新しい要素に使用できる。こうすることで、ハッシュテーブルの探索系列を変えずに保ちつつ、空間利用率も確保できる。

Q:なぜ線形探索では、要素を探すときにハッシュ衝突が発生するのですか?

探索時には、ハッシュ関数で対応するバケットとキーと値のペアを見つけ、key が一致しないことが分かると、それはハッシュ衝突を意味する。そのため、線形探索法では事前に設定したステップ幅に従って順に探索し、正しいキーと値のペアを見つけるか、見つからずに終了するまで続ける。

Q:なぜハッシュテーブルの拡張でハッシュ衝突を緩和できるのですか?

ハッシュ関数の最後のステップは、たいてい配列長 \\(n\\) での剰余を取り、出力値を配列インデックスの範囲内に収めることである。拡張後は配列長 \\(n\\) が変化し、key に対応するインデックスも変化する可能性がある。もともと同じバケットに入っていた複数の key は、拡張後には複数のバケットに割り当てられる可能性があり、それによってハッシュ衝突が緩和される。

Q:高効率な読み書きのためなら、配列を直接使えばよいのではないですか?

データの key が連続した小範囲の整数であれば、配列を直接使えばよく、単純で高効率である。しかし key が別の型(たとえば文字列)の場合は、ハッシュ関数を用いて key を配列インデックスに写像し、さらにバケット配列を通じて要素を格納する必要がある。このような構造がハッシュテーブルである。

","path":["第 6 章   ハッシュテーブル","6.4   まとめ"],"tags":[]},{"location":"chapter_heap/","level":1,"title":"第 8 章   ヒープ","text":"

Abstract

ヒープは連なる山々の峰のように、幾重にも重なり、さまざまな形をしている。

いくつもの山の高さはまちまちだが、最も高い峰がいつも最初に目に入る。

","path":["第 8 章   ヒープ"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"章の内容","text":"
  • 8.1   ヒープ
  • 8.2   ヒープ構築
  • 8.3   Top-k 問題
  • 8.4   まとめ
","path":["第 8 章   ヒープ"],"tags":[]},{"location":"chapter_heap/build_heap/","level":1,"title":"8.2   ヒープ構築","text":"

場合によっては、リスト内のすべての要素を使ってヒープを構築したいことがあります。この過程を「ヒープ構築」と呼びます。

","path":["第 8 章   ヒープ","8.2   ヒープ構築"],"tags":[]},{"location":"chapter_heap/build_heap/#821","level":2,"title":"8.2.1   ヒープへの挿入操作による実現","text":"

まず空のヒープを作成し、次にリストを走査して、各要素に対して順に「ヒープへの挿入操作」を実行します。つまり、要素をヒープの末尾に追加してから、その要素に対して「下から上へ」のヒープ化を行います。

要素が1つヒープに挿入されるたびに、ヒープの長さは1増加します。ノードは上から下へ順に二分木へ追加されるため、ヒープは「上から下へ」構築されます。

要素数を \\(n\\) とすると、各要素のヒープへの挿入操作には \\(O(\\log{n})\\) の時間がかかるため、このヒープ構築法の時間計算量は \\(O(n \\log n)\\) です。

","path":["第 8 章   ヒープ","8.2   ヒープ構築"],"tags":[]},{"location":"chapter_heap/build_heap/#822","level":2,"title":"8.2.2   走査によるヒープ化で実現","text":"

実際には、より効率的なヒープ構築法を実現でき、全体は2つの手順に分かれます。

  1. リストのすべての要素をそのままヒープに追加します。この時点では、ヒープの性質はまだ満たされていません。
  2. ヒープを逆順で走査し(レベル順走査の逆順)、各非葉ノードに対して順に「上から下へ」のヒープ化を実行します。

あるノードをヒープ化するたびに、そのノードを根とする部分木は合法な部分ヒープになります。また、逆順で走査するため、ヒープは「下から上へ」構築されます。

逆順走査を選ぶのは、この方法なら現在のノードの下にある部分木がすでに合法な部分ヒープであることを保証でき、そのうえで現在のノードをヒープ化してはじめて有効になるからです。

なお、葉ノードには子ノードがないため、それ自体が自然に合法な部分ヒープであり、ヒープ化は不要です。以下のコードが示すように、最後の非葉ノードは最後のノードの親ノードであり、そこから逆順に走査してヒープ化を実行します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def __init__(self, nums: list[int]):\n    \"\"\"コンストラクタ。入力リストに基づいてヒープを構築する\"\"\"\n    # リスト要素をそのままヒープに追加\n    self.max_heap = nums\n    # 葉ノード以外のすべてのノードをヒープ化\n    for i in range(self.parent(self.size() - 1), -1, -1):\n        self.sift_down(i)\n
my_heap.cpp
/* コンストラクタ。入力リストに基づいてヒープを構築する */\nMaxHeap(vector<int> nums) {\n    // リスト要素をそのままヒープに追加\n    maxHeap = nums;\n    // 葉ノード以外のすべてのノードをヒープ化\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
my_heap.java
/* コンストラクタ。入力リストに基づいてヒープを構築する */\nMaxHeap(List<Integer> nums) {\n    // リスト要素をそのままヒープに追加\n    maxHeap = new ArrayList<>(nums);\n    // 葉ノード以外のすべてのノードをヒープ化\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
my_heap.cs
/* コンストラクタ。入力リストに基づいてヒープを構築 */\nMaxHeap(IEnumerable<int> nums) {\n    // リスト要素をそのままヒープに追加\n    maxHeap = new List<int>(nums);\n    // 葉ノード以外のすべてのノードをヒープ化\n    var size = Parent(this.Size() - 1);\n    for (int i = size; i >= 0; i--) {\n        SiftDown(i);\n    }\n}\n
my_heap.go
/* コンストラクタ。スライスからヒープを構築する */\nfunc newMaxHeap(nums []any) *maxHeap {\n    // リスト要素をそのままヒープに追加\n    h := &maxHeap{data: nums}\n    for i := h.parent(len(h.data) - 1); i >= 0; i-- {\n        // 葉ノード以外のすべてのノードをヒープ化\n        h.siftDown(i)\n    }\n    return h\n}\n
my_heap.swift
/* コンストラクタ。入力リストに基づいてヒープを構築する */\ninit(nums: [Int]) {\n    // リスト要素をそのままヒープに追加\n    maxHeap = nums\n    // 葉ノード以外のすべてのノードをヒープ化\n    for i in (0 ... parent(i: size() - 1)).reversed() {\n        siftDown(i: i)\n    }\n}\n
my_heap.js
/* コンストラクタ。空のヒープを作成するか、入力リストからヒープを構築する */\nconstructor(nums) {\n    // リスト要素をそのままヒープに追加\n    this.#maxHeap = nums === undefined ? [] : [...nums];\n    // 葉ノード以外のすべてのノードをヒープ化\n    for (let i = this.#parent(this.size() - 1); i >= 0; i--) {\n        this.#siftDown(i);\n    }\n}\n
my_heap.ts
/* コンストラクタ。空のヒープを作成するか、入力リストからヒープを構築する */\nconstructor(nums?: number[]) {\n    // リスト要素をそのままヒープに追加\n    this.maxHeap = nums === undefined ? [] : [...nums];\n    // 葉ノード以外のすべてのノードをヒープ化\n    for (let i = this.parent(this.size() - 1); i >= 0; i--) {\n        this.siftDown(i);\n    }\n}\n
my_heap.dart
/* コンストラクタ。入力リストに基づいてヒープを構築する */\nMaxHeap(List<int> nums) {\n  // リスト要素をそのままヒープに追加\n  _maxHeap = nums;\n  // 葉ノード以外のすべてのノードをヒープ化\n  for (int i = _parent(size() - 1); i >= 0; i--) {\n    siftDown(i);\n  }\n}\n
my_heap.rs
/* コンストラクタ。入力リストに基づいてヒープを構築する */\nfn new(nums: Vec<i32>) -> Self {\n    // リスト要素をそのままヒープに追加\n    let mut heap = MaxHeap { max_heap: nums };\n    // 葉ノード以外のすべてのノードをヒープ化\n    for i in (0..=Self::parent(heap.size() - 1)).rev() {\n        heap.sift_down(i);\n    }\n    heap\n}\n
my_heap.c
/* コンストラクタ。スライスからヒープを構築する */\nMaxHeap *newMaxHeap(int nums[], int size) {\n    // すべての要素をヒープに入れる\n    MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap));\n    maxHeap->size = size;\n    memcpy(maxHeap->data, nums, size * sizeof(int));\n    for (int i = parent(maxHeap, size - 1); i >= 0; i--) {\n        // 葉ノード以外のすべてのノードをヒープ化\n        siftDown(maxHeap, i);\n    }\n    return maxHeap;\n}\n
my_heap.kt
/* 最大ヒープ */\nclass MaxHeap(nums: MutableList<Int>?) {\n    // 配列ではなくリストを使うことで、拡張を考慮する必要がない\n    private val maxHeap = mutableListOf<Int>()\n\n    /* コンストラクタ。入力リストに基づいてヒープを構築する */\n    init {\n        // リスト要素をそのままヒープに追加\n        maxHeap.addAll(nums!!)\n        // 葉ノード以外のすべてのノードをヒープ化\n        for (i in parent(size() - 1) downTo 0) {\n            siftDown(i)\n        }\n    }\n\n    /* 左子ノードのインデックスを取得 */\n    private fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* 右子ノードのインデックスを取得 */\n    private fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* 親ノードのインデックスを取得 */\n    private fun parent(i: Int): Int {\n        return (i - 1) / 2 // 切り捨て除算\n    }\n\n    /* 要素を交換 */\n    private fun swap(i: Int, j: Int) {\n        val temp = maxHeap[i]\n        maxHeap[i] = maxHeap[j]\n        maxHeap[j] = temp\n    }\n\n    /* ヒープのサイズを取得 */\n    fun size(): Int {\n        return maxHeap.size\n    }\n\n    /* ヒープが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        /* ヒープが空かどうかを判定 */\n        return size() == 0\n    }\n\n    /* ヒープ先頭要素にアクセス */\n    fun peek(): Int {\n        return maxHeap[0]\n    }\n\n    /* 要素をヒープに追加 */\n    fun push(_val: Int) {\n        // ノードを追加\n        maxHeap.add(_val)\n        // 下から上へヒープ化\n        siftUp(size() - 1)\n    }\n\n    /* ノード i から始めて、下から上へヒープ化 */\n    private fun siftUp(it: Int) {\n        // Kotlin の関数引数は不変のため、一時変数を作成する\n        var i = it\n        while (true) {\n            // ノード i の親ノードを取得\n            val p = parent(i)\n            // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n            if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n            // 2 つのノードを交換\n            swap(i, p)\n            // ループで下から上へヒープ化\n            i = p\n        }\n    }\n\n    /* 要素をヒープから取り出す */\n    fun pop(): Int {\n        // 空判定の処理\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        swap(0, size() - 1)\n        // ノードを削除\n        val _val = maxHeap.removeAt(size() - 1)\n        // 上から下へヒープ化\n        siftDown(0)\n        // ヒープ先頭要素を返す\n        return _val\n    }\n\n    /* ノード i から始めて、上から下へヒープ化 */\n    private fun siftDown(it: Int) {\n        // Kotlin の関数引数は不変のため、一時変数を作成する\n        var i = it\n        while (true) {\n            // ノード i, l, r のうち値が最大のノードを ma とする\n            val l = left(i)\n            val r = right(i)\n            var ma = i\n            if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n            if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n            // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n            if (ma == i) break\n            // 2 つのノードを交換\n            swap(i, ma)\n            // ループで上から下へヒープ化\n            i = ma\n        }\n    }\n\n    /* ヒープ(二分木)を出力 */\n    fun print() {\n        val queue = PriorityQueue { a: Int, b: Int -> b - a }\n        queue.addAll(maxHeap)\n        printHeap(queue)\n    }\n}\n
my_heap.rb
### コンストラクタ。入力リストに基づいてヒープを構築 ###\ndef initialize(nums)\n  # リスト要素をそのままヒープに追加\n  @max_heap = nums\n  # 葉ノード以外のすべてのノードをヒープ化\n  parent(size - 1).downto(0) do |i|\n    sift_down(i)\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 8 章   ヒープ","8.2   ヒープ構築"],"tags":[]},{"location":"chapter_heap/build_heap/#823","level":2,"title":"8.2.3   計算量の分析","text":"

以下では、2つ目のヒープ構築法の時間計算量を求めてみましょう。

  • 完全二分木のノード数を \\(n\\) とすると、葉ノード数は \\((n + 1) / 2\\) です。ここで \\(/\\) は切り捨て除算を表します。したがって、ヒープ化が必要なノード数は \\((n - 1) / 2\\) です。
  • 上から下へのヒープ化の過程では、各ノードは最大で葉ノードまでヒープ化されるため、最大反復回数は二分木の高さ \\(\\log n\\) です。

上の2つを掛け合わせると、ヒープ構築過程の時間計算量は \\(O(n \\log n)\\) となります。しかし、この見積もりは正確ではありません。二分木では下層のノード数が上層よりはるかに多いという性質を考慮していないためです。

次に、より正確な計算を行います。計算を簡単にするため、ノード数が \\(n\\) 、高さが \\(h\\) の「満二分木」を仮定します。この仮定は計算結果の正しさに影響しません。

図 8-5   満二分木の各層のノード数

上図に示すように、ノードを「上から下へヒープ化」する最大反復回数は、そのノードから葉ノードまでの距離に等しく、この距離こそが「ノードの高さ」です。したがって、各層の「ノード数 \\(\\times\\) ノードの高さ」を合計すれば、**すべてのノードのヒープ化反復回数の総和**が得られます。

\\[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{(h-1)}\\times1 \\]

上式を簡約するには中学の数列の知識を用います。まず \\(T(h)\\) に \\(2\\) を掛けると、次のようになります。

\\[ \\begin{aligned} T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{h-1}\\times1 \\newline 2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \\dots + 2^{h}\\times1 \\newline \\end{aligned} \\]

ずらして引く方法を用い、下式の \\(2 T(h)\\) から上式の \\(T(h)\\) を引くと、次が得られます。

\\[ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \\dots + 2^{h-1} + 2^h \\]

上式を見ると、\\(T(h)\\) は等比数列であることがわかるため、和の公式を直接用いて、時間計算量は次のように求められます。

\\[ \\begin{aligned} T(h) & = 2 \\frac{1 - 2^h}{1 - 2} - h \\newline & = 2^{h+1} - h - 2 \\newline & = O(2^h) \\end{aligned} \\]

さらに、高さ \\(h\\) の満二分木のノード数は \\(n = 2^{h+1} - 1\\) であるため、計算量は容易に \\(O(2^h) = O(n)\\) とわかります。以上の導出は、**入力リストからヒープを構築する時間計算量が \\(O(n)\\) であり、非常に効率的である**ことを示しています。

","path":["第 8 章   ヒープ","8.2   ヒープ構築"],"tags":[]},{"location":"chapter_heap/heap/","level":1,"title":"8.1   ヒープ","text":"

ヒープ(heap)は、特定の条件を満たす完全二分木であり、主に次の 2 種類に分けられます。

  • 最小ヒープ(min heap):任意のノードの値 \\(\\leq\\) その子ノードの値。
  • 最大ヒープ(max heap):任意のノードの値 \\(\\geq\\) その子ノードの値。

図 8-1   最小ヒープと最大ヒープ

ヒープは完全二分木の特殊な例であり、次の性質を持ちます。

  • 最下層のノードは左から順に埋められ、ほかの層のノードはすべて埋まっています。
  • 二分木の根ノードを「ヒープ頂点」、最下層で最も右にあるノードを「ヒープ底」と呼びます。
  • 最大ヒープ(最小ヒープ)では、ヒープ頂点の要素(根ノード)の値が最大(最小)です。
","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#811","level":2,"title":"8.1.1   ヒープの基本操作","text":"

ここで注意したいのは、多くのプログラミング言語が提供しているのは優先度付きキュー(priority queue)であり、これは優先度順に並ぶキューとして定義される抽象データ構造だということです。

実際には、ヒープは通常、優先度付きキューの実装に用いられ、最大ヒープは要素が大きい順に取り出される優先度付きキューに相当します。利用の観点では、「優先度付きキュー」と「ヒープ」は等価なデータ構造とみなせます。そのため、本書では両者を特に区別せず、まとめて「ヒープ」と呼びます。

ヒープの基本操作を以下の表に示します。メソッド名はプログラミング言語によって異なります。

表 8-1   ヒープの操作効率

メソッド名 説明 時間計算量 push() 要素をヒープに追加 \\(O(\\log n)\\) pop() ヒープ頂点の要素を取り出す \\(O(\\log n)\\) peek() ヒープ頂点の要素にアクセス(最大 / 最小ヒープではそれぞれ最大 / 最小値) \\(O(1)\\) size() ヒープ内の要素数を取得 \\(O(1)\\) isEmpty() ヒープが空かどうかを判定 \\(O(1)\\)

実際の応用では、プログラミング言語が提供するヒープクラス(または優先度付きキュークラス)をそのまま使えます。

ソートアルゴリズムにおける「昇順」と「降順」と同様に、flag を設定したり Comparator を変更したりすることで、「最小ヒープ」と「最大ヒープ」を切り替えられます。コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap.py
# 最小ヒープを初期化\nmin_heap, flag = [], 1\n# 最大ヒープを初期化\nmax_heap, flag = [], -1\n\n# Python の heapq モジュールはデフォルトで最小ヒープを実装している\n# 「要素を負にして」からヒープに追加すると、大小関係を反転させて最大ヒープを実現できる\n# この例では、flag = 1 のときは最小ヒープ、flag = -1 のときは最大ヒープに対応する\n\n# 要素をヒープに追加\nheapq.heappush(max_heap, flag * 1)\nheapq.heappush(max_heap, flag * 3)\nheapq.heappush(max_heap, flag * 2)\nheapq.heappush(max_heap, flag * 5)\nheapq.heappush(max_heap, flag * 4)\n\n# ヒープ頂点の要素を取得\npeek: int = flag * max_heap[0] # 5\n\n# ヒープ頂点の要素を取り出す\n# 取り出された要素は大きい順の列になる\nval = flag * heapq.heappop(max_heap) # 5\nval = flag * heapq.heappop(max_heap) # 4\nval = flag * heapq.heappop(max_heap) # 3\nval = flag * heapq.heappop(max_heap) # 2\nval = flag * heapq.heappop(max_heap) # 1\n\n# ヒープのサイズを取得\nsize: int = len(max_heap)\n\n# ヒープが空かどうかを判定\nis_empty: bool = not max_heap\n\n# 入力リストからヒープを構築\nmin_heap: list[int] = [1, 3, 2, 5, 4]\nheapq.heapify(min_heap)\n
heap.cpp
/* ヒープを初期化 */\n// 最小ヒープを初期化\npriority_queue<int, vector<int>, greater<int>> minHeap;\n// 最大ヒープを初期化\npriority_queue<int, vector<int>, less<int>> maxHeap;\n\n/* 要素をヒープに追加 */\nmaxHeap.push(1);\nmaxHeap.push(3);\nmaxHeap.push(2);\nmaxHeap.push(5);\nmaxHeap.push(4);\n\n/* ヒープ頂点の要素を取得 */\nint peek = maxHeap.top(); // 5\n\n/* ヒープ頂点の要素を取り出す */\n// 取り出された要素は大きい順の列になる\nmaxHeap.pop(); // 5\nmaxHeap.pop(); // 4\nmaxHeap.pop(); // 3\nmaxHeap.pop(); // 2\nmaxHeap.pop(); // 1\n\n/* ヒープのサイズを取得 */\nint size = maxHeap.size();\n\n/* ヒープが空かどうかを判定 */\nbool isEmpty = maxHeap.empty();\n\n/* 入力リストからヒープを構築 */\nvector<int> input{1, 3, 2, 5, 4};\npriority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());\n
heap.java
/* ヒープを初期化 */\n// 最小ヒープを初期化\nQueue<Integer> minHeap = new PriorityQueue<>();\n// 最大ヒープを初期化(lambda 式で Comparator を変更すればよい)\nQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);\n\n/* 要素をヒープに追加 */\nmaxHeap.offer(1);\nmaxHeap.offer(3);\nmaxHeap.offer(2);\nmaxHeap.offer(5);\nmaxHeap.offer(4);\n\n/* ヒープ頂点の要素を取得 */\nint peek = maxHeap.peek(); // 5\n\n/* ヒープ頂点の要素を取り出す */\n// 取り出された要素は大きい順の列になる\npeek = maxHeap.poll(); // 5\npeek = maxHeap.poll(); // 4\npeek = maxHeap.poll(); // 3\npeek = maxHeap.poll(); // 2\npeek = maxHeap.poll(); // 1\n\n/* ヒープのサイズを取得 */\nint size = maxHeap.size();\n\n/* ヒープが空かどうかを判定 */\nboolean isEmpty = maxHeap.isEmpty();\n\n/* 入力リストからヒープを構築 */\nminHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));\n
heap.cs
/* ヒープを初期化 */\n// 最小ヒープを初期化\nPriorityQueue<int, int> minHeap = new();\n// 最大ヒープを初期化(lambda 式で Comparer を変更すればよい)\nPriorityQueue<int, int> maxHeap = new(Comparer<int>.Create((x, y) => y.CompareTo(x)));\n\n/* 要素をヒープに追加 */\nmaxHeap.Enqueue(1, 1);\nmaxHeap.Enqueue(3, 3);\nmaxHeap.Enqueue(2, 2);\nmaxHeap.Enqueue(5, 5);\nmaxHeap.Enqueue(4, 4);\n\n/* ヒープ頂点の要素を取得 */\nint peek = maxHeap.Peek();//5\n\n/* ヒープ頂点の要素を取り出す */\n// 取り出された要素は大きい順の列になる\npeek = maxHeap.Dequeue();  // 5\npeek = maxHeap.Dequeue();  // 4\npeek = maxHeap.Dequeue();  // 3\npeek = maxHeap.Dequeue();  // 2\npeek = maxHeap.Dequeue();  // 1\n\n/* ヒープのサイズを取得 */\nint size = maxHeap.Count;\n\n/* ヒープが空かどうかを判定 */\nbool isEmpty = maxHeap.Count == 0;\n\n/* 入力リストからヒープを構築 */\nminHeap = new PriorityQueue<int, int>([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);\n
heap.go
// Go では、heap.Interface を実装することで整数の最大ヒープを構築できる\n// heap.Interface を実装するには、同時に sort.Interface も実装する必要がある\ntype intHeap []any\n\n// Push は heap.Interface のメソッドで、要素をヒープに追加する\nfunc (h *intHeap) Push(x any) {\n    // Push と Pop は pointer receiver を引数に取る\n    // スライスの内容を調整するだけでなく、スライスの長さも変更するため。\n    *h = append(*h, x.(int))\n}\n\n// Pop は heap.Interface のメソッドで、ヒープ頂点の要素を取り出す\nfunc (h *intHeap) Pop() any {\n    // 取り出す要素は末尾に格納されている\n    last := (*h)[len(*h)-1]\n    *h = (*h)[:len(*h)-1]\n    return last\n}\n\n// Len は sort.Interface のメソッド\nfunc (h *intHeap) Len() int {\n    return len(*h)\n}\n\n// Less は sort.Interface のメソッド\nfunc (h *intHeap) Less(i, j int) bool {\n    // 最小ヒープを実装する場合は、不等号を小なりに変更する\n    return (*h)[i].(int) > (*h)[j].(int)\n}\n\n// Swap は sort.Interface のメソッド\nfunc (h *intHeap) Swap(i, j int) {\n    (*h)[i], (*h)[j] = (*h)[j], (*h)[i]\n}\n\n// Top はヒープ頂点の要素を取得\nfunc (h *intHeap) Top() any {\n    return (*h)[0]\n}\n\n/* Driver Code */\nfunc TestHeap(t *testing.T) {\n    /* ヒープを初期化 */\n    // 最大ヒープを初期化\n    maxHeap := &intHeap{}\n    heap.Init(maxHeap)\n    /* 要素をヒープに追加 */\n    // heap.Interface のメソッドを呼び出して要素を追加する\n    heap.Push(maxHeap, 1)\n    heap.Push(maxHeap, 3)\n    heap.Push(maxHeap, 2)\n    heap.Push(maxHeap, 4)\n    heap.Push(maxHeap, 5)\n\n    /* ヒープ頂点の要素を取得 */\n    top := maxHeap.Top()\n    fmt.Printf(\"ヒープ頂点の要素は %d\\n\", top)\n\n    /* ヒープ頂点の要素を取り出す */\n    // heap.Interface のメソッドを呼び出して要素を削除する\n    heap.Pop(maxHeap) // 5\n    heap.Pop(maxHeap) // 4\n    heap.Pop(maxHeap) // 3\n    heap.Pop(maxHeap) // 2\n    heap.Pop(maxHeap) // 1\n\n    /* ヒープのサイズを取得 */\n    size := len(*maxHeap)\n    fmt.Printf(\"ヒープ内の要素数は %d\\n\", size)\n\n    /* ヒープが空かどうかを判定 */\n    isEmpty := len(*maxHeap) == 0\n    fmt.Printf(\"ヒープは空か %t\\n\", isEmpty)\n}\n
heap.swift
/* ヒープを初期化 */\n// Swift の Heap 型は最大ヒープと最小ヒープの両方をサポートしており、swift-collections の導入が必要\nvar heap = Heap<Int>()\n\n/* 要素をヒープに追加 */\nheap.insert(1)\nheap.insert(3)\nheap.insert(2)\nheap.insert(5)\nheap.insert(4)\n\n/* ヒープ頂点の要素を取得 */\nvar peek = heap.max()!\n\n/* ヒープ頂点の要素を取り出す */\npeek = heap.removeMax() // 5\npeek = heap.removeMax() // 4\npeek = heap.removeMax() // 3\npeek = heap.removeMax() // 2\npeek = heap.removeMax() // 1\n\n/* ヒープのサイズを取得 */\nlet size = heap.count\n\n/* ヒープが空かどうかを判定 */\nlet isEmpty = heap.isEmpty\n\n/* 入力リストからヒープを構築 */\nlet heap2 = Heap([1, 3, 2, 5, 4])\n
heap.js
// JavaScript には組み込みの Heap クラスがない\n
heap.ts
// TypeScript には組み込みの Heap クラスがない\n
heap.dart
// Dart には組み込みの Heap クラスがない\n
heap.rs
use std::collections::BinaryHeap;\nuse std::cmp::Reverse;\n\n/* ヒープを初期化 */\n// 最小ヒープを初期化\nlet mut min_heap = BinaryHeap::<Reverse<i32>>::new();\n// 最大ヒープを初期化\nlet mut max_heap = BinaryHeap::new();\n\n/* 要素をヒープに追加 */\nmax_heap.push(1);\nmax_heap.push(3);\nmax_heap.push(2);\nmax_heap.push(5);\nmax_heap.push(4);\n\n/* ヒープ頂点の要素を取得 */\nlet peek = max_heap.peek().unwrap();  // 5\n\n/* ヒープ頂点の要素を取り出す */\n// 取り出された要素は大きい順の列になる\nlet peek = max_heap.pop().unwrap();   // 5\nlet peek = max_heap.pop().unwrap();   // 4\nlet peek = max_heap.pop().unwrap();   // 3\nlet peek = max_heap.pop().unwrap();   // 2\nlet peek = max_heap.pop().unwrap();   // 1\n\n/* ヒープのサイズを取得 */\nlet size = max_heap.len();\n\n/* ヒープが空かどうかを判定 */\nlet is_empty = max_heap.is_empty();\n\n/* 入力リストからヒープを構築 */\nlet min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);\n
heap.c
// C には組み込みの Heap クラスがない\n
heap.kt
/* ヒープを初期化 */\n// 最小ヒープを初期化\nvar minHeap = PriorityQueue<Int>()\n// 最大ヒープを初期化(lambda 式で Comparator を変更すればよい)\nval maxHeap = PriorityQueue { a: Int, b: Int -> b - a }\n\n/* 要素をヒープに追加 */\nmaxHeap.offer(1)\nmaxHeap.offer(3)\nmaxHeap.offer(2)\nmaxHeap.offer(5)\nmaxHeap.offer(4)\n\n/* ヒープ頂点の要素を取得 */\nvar peek = maxHeap.peek() // 5\n\n/* ヒープ頂点の要素を取り出す */\n// 取り出された要素は大きい順の列になる\npeek = maxHeap.poll() // 5\npeek = maxHeap.poll() // 4\npeek = maxHeap.poll() // 3\npeek = maxHeap.poll() // 2\npeek = maxHeap.poll() // 1\n\n/* ヒープのサイズを取得 */\nval size = maxHeap.size\n\n/* ヒープが空かどうかを判定 */\nval isEmpty = maxHeap.isEmpty()\n\n/* 入力リストからヒープを構築 */\nminHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))\n
heap.rb
# Ruby には組み込みの Heap クラスがない\n
実行を可視化

https://pythontutor.com/render.html#code=import%20heapq%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20min_heap,%20flag%20%3D%20%5B%5D,%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20max_heap,%20flag%20%3D%20%5B%5D,%20-1%0A%20%20%20%20%0A%20%20%20%20%23%20Python%20%E7%9A%84%20heapq%20%E6%A8%A1%E5%9D%97%E9%BB%98%E8%AE%A4%E5%AE%9E%E7%8E%B0%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E8%80%83%E8%99%91%E5%B0%86%E2%80%9C%E5%85%83%E7%B4%A0%E5%8F%96%E8%B4%9F%E2%80%9D%E5%90%8E%E5%86%8D%E5%85%A5%E5%A0%86%EF%BC%8C%E8%BF%99%E6%A0%B7%E5%B0%B1%E5%8F%AF%E4%BB%A5%E5%B0%86%E5%A4%A7%E5%B0%8F%E5%85%B3%E7%B3%BB%E9%A2%A0%E5%80%92%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%AE%9E%E7%8E%B0%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E5%9C%A8%E6%9C%AC%E7%A4%BA%E4%BE%8B%E4%B8%AD%EF%BC%8Cflag%20%3D%201%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%B0%8F%E9%A1%B6%E5%A0%86%EF%BC%8Cflag%20%3D%20-1%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%201%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%203%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%202%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%205%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%204%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20flag%20*%20max_heap%5B0%5D%20%23%205%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%E5%87%BA%E5%A0%86%0A%20%20%20%20%23%20%E5%87%BA%E5%A0%86%E5%85%83%E7%B4%A0%E4%BC%9A%E5%BD%A2%E6%88%90%E4%B8%80%E4%B8%AA%E4%BB%8E%E5%A4%A7%E5%88%B0%E5%B0%8F%E7%9A%84%E5%BA%8F%E5%88%97%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%205%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%204%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%203%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%202%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%201%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E5%A4%A7%E5%B0%8F%0A%20%20%20%20size%20%3D%20len%28max_heap%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%A0%86%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20not%20max_heap%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%BE%93%E5%85%A5%E5%88%97%E8%A1%A8%E5%B9%B6%E5%BB%BA%E5%A0%86%0A%20%20%20%20min_heap%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20heapq.heapify%28min_heap%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#812","level":2,"title":"8.1.2   ヒープの実装","text":"

以下では最大ヒープを実装します。最小ヒープに変換したい場合は、すべての大小比較ロジックを反転させるだけです(たとえば、\\(\\geq\\) を \\(\\leq\\) に置き換えます)。興味のある読者は自分で実装してみてください。

","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#1","level":3,"title":"1.   ヒープの格納と表現","text":"

「二分木」の章で述べたように、完全二分木は配列で表現するのに非常に適しています。ヒープはまさに完全二分木の一種なので、ここでは配列を使ってヒープを格納します。

配列で二分木を表す場合、要素はノードの値を表し、インデックスは二分木におけるノードの位置を表します。ノード間の参照関係はインデックスの対応式によって実現できます。

次の図に示すように、インデックス \\(i\\) に対して、左子ノードのインデックスは \\(2i + 1\\) 、右子ノードのインデックスは \\(2i + 2\\) 、親ノードのインデックスは \\((i - 1) / 2\\)(切り捨て除算)です。インデックスが範囲外であれば、空ノードまたはノードが存在しないことを表します。

図 8-2   ヒープの表現と格納

インデックスの対応式は関数にまとめておくと、後続で使いやすくなります:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def left(self, i: int) -> int:\n    \"\"\"左子ノードのインデックスを取得\"\"\"\n    return 2 * i + 1\n\ndef right(self, i: int) -> int:\n    \"\"\"右子ノードのインデックスを取得\"\"\"\n    return 2 * i + 2\n\ndef parent(self, i: int) -> int:\n    \"\"\"親ノードのインデックスを取得\"\"\"\n    return (i - 1) // 2  # 切り捨て除算\n
my_heap.cpp
/* 左子ノードのインデックスを取得 */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint parent(int i) {\n    return (i - 1) / 2; // 切り捨て除算\n}\n
my_heap.java
/* 左子ノードのインデックスを取得 */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint parent(int i) {\n    return (i - 1) / 2; // 切り捨て除算\n}\n
my_heap.cs
/* 左子ノードのインデックスを取得 */\nint Left(int i) {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nint Right(int i) {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint Parent(int i) {\n    return (i - 1) / 2; // 切り捨て除算\n}\n
my_heap.go
/* 左子ノードのインデックスを取得 */\nfunc (h *maxHeap) left(i int) int {\n    return 2*i + 1\n}\n\n/* 右子ノードのインデックスを取得 */\nfunc (h *maxHeap) right(i int) int {\n    return 2*i + 2\n}\n\n/* 親ノードのインデックスを取得 */\nfunc (h *maxHeap) parent(i int) int {\n    // 切り捨て除算\n    return (i - 1) / 2\n}\n
my_heap.swift
/* 左子ノードのインデックスを取得 */\nfunc left(i: Int) -> Int {\n    2 * i + 1\n}\n\n/* 右子ノードのインデックスを取得 */\nfunc right(i: Int) -> Int {\n    2 * i + 2\n}\n\n/* 親ノードのインデックスを取得 */\nfunc parent(i: Int) -> Int {\n    (i - 1) / 2 // 切り捨て除算\n}\n
my_heap.js
/* 左子ノードのインデックスを取得 */\n#left(i) {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\n#right(i) {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\n#parent(i) {\n    return Math.floor((i - 1) / 2); // 切り捨て除算\n}\n
my_heap.ts
/* 左子ノードのインデックスを取得 */\nleft(i: number): number {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nright(i: number): number {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nparent(i: number): number {\n    return Math.floor((i - 1) / 2); // 切り捨て除算\n}\n
my_heap.dart
/* 左子ノードのインデックスを取得 */\nint _left(int i) {\n  return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nint _right(int i) {\n  return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint _parent(int i) {\n  return (i - 1) ~/ 2; // 切り捨て除算\n}\n
my_heap.rs
/* 左子ノードのインデックスを取得 */\nfn left(i: usize) -> usize {\n    2 * i + 1\n}\n\n/* 右子ノードのインデックスを取得 */\nfn right(i: usize) -> usize {\n    2 * i + 2\n}\n\n/* 親ノードのインデックスを取得 */\nfn parent(i: usize) -> usize {\n    (i - 1) / 2 // 切り捨て除算\n}\n
my_heap.c
/* 左子ノードのインデックスを取得 */\nint left(MaxHeap *maxHeap, int i) {\n    return 2 * i + 1;\n}\n\n/* 右子ノードのインデックスを取得 */\nint right(MaxHeap *maxHeap, int i) {\n    return 2 * i + 2;\n}\n\n/* 親ノードのインデックスを取得 */\nint parent(MaxHeap *maxHeap, int i) {\n    return (i - 1) / 2; // 切り捨て\n}\n
my_heap.kt
/* 左子ノードのインデックスを取得 */\nfun left(i: Int): Int {\n    return 2 * i + 1\n}\n\n/* 右子ノードのインデックスを取得 */\nfun right(i: Int): Int {\n    return 2 * i + 2\n}\n\n/* 親ノードのインデックスを取得 */\nfun parent(i: Int): Int {\n    return (i - 1) / 2 // 切り捨て除算\n}\n
my_heap.rb
### 左子ノードのインデックスを取得 ###\ndef left(i)\n  2 * i + 1\nend\n\n### 右子ノードのインデックスを取得 ###\ndef right(i)\n  2 * i + 2\nend\n\n### 親ノードのインデックスを取得 ###\ndef parent(i)\n  (i - 1) / 2     # 切り捨て除算\nend\n
","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#2","level":3,"title":"2.   ヒープ頂点の要素にアクセス","text":"

ヒープ頂点の要素は二分木の根ノード、すなわちリストの先頭要素です:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def peek(self) -> int:\n    \"\"\"ヒープ先頭要素にアクセス\"\"\"\n    return self.max_heap[0]\n
my_heap.cpp
/* ヒープ先頭要素にアクセス */\nint peek() {\n    return maxHeap[0];\n}\n
my_heap.java
/* ヒープ先頭要素にアクセス */\nint peek() {\n    return maxHeap.get(0);\n}\n
my_heap.cs
/* ヒープ先頭要素にアクセス */\nint Peek() {\n    return maxHeap[0];\n}\n
my_heap.go
/* ヒープ先頭要素にアクセス */\nfunc (h *maxHeap) peek() any {\n    return h.data[0]\n}\n
my_heap.swift
/* ヒープ先頭要素にアクセス */\nfunc peek() -> Int {\n    maxHeap[0]\n}\n
my_heap.js
/* ヒープ先頭要素にアクセス */\npeek() {\n    return this.#maxHeap[0];\n}\n
my_heap.ts
/* ヒープ先頭要素にアクセス */\npeek(): number {\n    return this.maxHeap[0];\n}\n
my_heap.dart
/* ヒープ先頭要素にアクセス */\nint peek() {\n  return _maxHeap[0];\n}\n
my_heap.rs
/* ヒープ先頭要素にアクセス */\nfn peek(&self) -> Option<i32> {\n    self.max_heap.first().copied()\n}\n
my_heap.c
/* ヒープ先頭要素にアクセス */\nint peek(MaxHeap *maxHeap) {\n    return maxHeap->data[0];\n}\n
my_heap.kt
/* ヒープ先頭要素にアクセス */\nfun peek(): Int {\n    return maxHeap[0]\n}\n
my_heap.rb
### ヒープ先頭要素を参照 ###\ndef peek\n  @max_heap[0]\nend\n
コードの可視化

全画面で見る >

","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#3","level":3,"title":"3.   要素をヒープに追加","text":"

与えられた要素 val を、まずヒープの底に追加します。追加後、val がヒープ内のほかの要素より大きい可能性があるため、ヒープ条件が崩れているかもしれません。そのため、挿入ノードから根ノードまでの経路上にある各ノードを修復する必要があります。この操作をヒープ化(heapify)と呼びます。

ヒープへ追加したノードから始めて、**下から上へヒープ化**を行います。次の図のように、挿入ノードとその親ノードの値を比較し、挿入ノードのほうが大きければそれらを交換します。その後もこの操作を繰り返し、下から上へ各ノードを修復して、根ノードを越えるか交換不要のノードに達した時点で終了します。

<1><2><3><4><5><6><7><8><9>

図 8-3   要素をヒープに追加する手順

ノード総数を \\(n\\) とすると、木の高さは \\(O(\\log n)\\) です。したがって、ヒープ化操作のループ回数は高々 \\(O(\\log n)\\) であり、要素をヒープに追加する操作の時間計算量は \\(O(\\log n)\\) です。コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def push(self, val: int):\n    \"\"\"要素をヒープに追加\"\"\"\n    # ノードを追加\n    self.max_heap.append(val)\n    # 下から上へヒープ化\n    self.sift_up(self.size() - 1)\n\ndef sift_up(self, i: int):\n    \"\"\"ノード i から始めて、下から上へヒープ化\"\"\"\n    while True:\n        # ノード i の親ノードを取得\n        p = self.parent(i)\n        # 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n            break\n        # 2 つのノードを交換\n        self.swap(i, p)\n        # ループで下から上へヒープ化\n        i = p\n
my_heap.cpp
/* 要素をヒープに追加 */\nvoid push(int val) {\n    // ノードを追加\n    maxHeap.push_back(val);\n    // 下から上へヒープ化\n    siftUp(size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nvoid siftUp(int i) {\n    while (true) {\n        // ノード i の親ノードを取得\n        int p = parent(i);\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // 2 つのノードを交換\n        swap(maxHeap[i], maxHeap[p]);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.java
/* 要素をヒープに追加 */\nvoid push(int val) {\n    // ノードを追加\n    maxHeap.add(val);\n    // 下から上へヒープ化\n    siftUp(size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nvoid siftUp(int i) {\n    while (true) {\n        // ノード i の親ノードを取得\n        int p = parent(i);\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n            break;\n        // 2 つのノードを交換\n        swap(i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.cs
/* 要素をヒープに追加 */\nvoid Push(int val) {\n    // ノードを追加\n    maxHeap.Add(val);\n    // 下から上へヒープ化\n    SiftUp(Size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nvoid SiftUp(int i) {\n    while (true) {\n        // ノード i の親ノードを取得\n        int p = Parent(i);\n        // 「根ノードを越えた」または「ノードの修復が不要」な場合は、ヒープ化を終了する\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // 2 つのノードを交換\n        Swap(i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.go
/* 要素をヒープに追加 */\nfunc (h *maxHeap) push(val any) {\n    // ノードを追加\n    h.data = append(h.data, val)\n    // 下から上へヒープ化\n    h.siftUp(len(h.data) - 1)\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nfunc (h *maxHeap) siftUp(i int) {\n    for true {\n        // ノード i の親ノードを取得\n        p := h.parent(i)\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 || h.data[i].(int) <= h.data[p].(int) {\n            break\n        }\n        // 2 つのノードを交換\n        h.swap(i, p)\n        // ループで下から上へヒープ化\n        i = p\n    }\n}\n
my_heap.swift
/* 要素をヒープに追加 */\nfunc push(val: Int) {\n    // ノードを追加\n    maxHeap.append(val)\n    // 下から上へヒープ化\n    siftUp(i: size() - 1)\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nfunc siftUp(i: Int) {\n    var i = i\n    while true {\n        // ノード i の親ノードを取得\n        let p = parent(i: i)\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if p < 0 || maxHeap[i] <= maxHeap[p] {\n            break\n        }\n        // 2 つのノードを交換\n        swap(i: i, j: p)\n        // ループで下から上へヒープ化\n        i = p\n    }\n}\n
my_heap.js
/* 要素をヒープに追加 */\npush(val) {\n    // ノードを追加\n    this.#maxHeap.push(val);\n    // 下から上へヒープ化\n    this.#siftUp(this.size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\n#siftUp(i) {\n    while (true) {\n        // ノード i の親ノードを取得\n        const p = this.#parent(i);\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break;\n        // 2 つのノードを交換\n        this.#swap(i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.ts
/* 要素をヒープに追加 */\npush(val: number): void {\n    // ノードを追加\n    this.maxHeap.push(val);\n    // 下から上へヒープ化\n    this.siftUp(this.size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nsiftUp(i: number): void {\n    while (true) {\n        // ノード i の親ノードを取得\n        const p = this.parent(i);\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break;\n        // 2 つのノードを交換\n        this.swap(i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.dart
/* 要素をヒープに追加 */\nvoid push(int val) {\n  // ノードを追加\n  _maxHeap.add(val);\n  // 下から上へヒープ化\n  siftUp(size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nvoid siftUp(int i) {\n  while (true) {\n    // ノード i の親ノードを取得\n    int p = _parent(i);\n    // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n    if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {\n      break;\n    }\n    // 2 つのノードを交換\n    _swap(i, p);\n    // ループで下から上へヒープ化\n    i = p;\n  }\n}\n
my_heap.rs
/* 要素をヒープに追加 */\nfn push(&mut self, val: i32) {\n    // ノードを追加\n    self.max_heap.push(val);\n    // 下から上へヒープ化\n    self.sift_up(self.size() - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nfn sift_up(&mut self, mut i: usize) {\n    loop {\n        // ノード i はすでにヒープの先頭ノードなので、ヒープ化を終了する\n        if i == 0 {\n            break;\n        }\n        // ノード i の親ノードを取得\n        let p = Self::parent(i);\n        // 「ノードの修復が不要」になったら、ヒープ化を終了\n        if self.max_heap[i] <= self.max_heap[p] {\n            break;\n        }\n        // 2 つのノードを交換\n        self.swap(i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.c
/* 要素をヒープに追加 */\nvoid push(MaxHeap *maxHeap, int val) {\n    // 通常は、これほど多くのノードを追加すべきではない\n    if (maxHeap->size == MAX_SIZE) {\n        printf(\"heap is full!\");\n        return;\n    }\n    // ノードを追加\n    maxHeap->data[maxHeap->size] = val;\n    maxHeap->size++;\n\n    // 下から上へヒープ化\n    siftUp(maxHeap, maxHeap->size - 1);\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nvoid siftUp(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // ノード i の親ノードを取得\n        int p = parent(maxHeap, i);\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {\n            break;\n        }\n        // 2 つのノードを交換\n        swap(maxHeap, i, p);\n        // ループで下から上へヒープ化\n        i = p;\n    }\n}\n
my_heap.kt
/* 要素をヒープに追加 */\nfun push(_val: Int) {\n    // ノードを追加\n    maxHeap.add(_val)\n    // 下から上へヒープ化\n    siftUp(size() - 1)\n}\n\n/* ノード i から始めて、下から上へヒープ化 */\nfun siftUp(it: Int) {\n    // Kotlin の関数引数は不変のため、一時変数を作成する\n    var i = it\n    while (true) {\n        // ノード i の親ノードを取得\n        val p = parent(i)\n        // 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n        if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n        // 2 つのノードを交換\n        swap(i, p)\n        // ループで下から上へヒープ化\n        i = p\n    }\n}\n
my_heap.rb
### 要素をヒープに挿入 ###\ndef push(val)\n  # ノードを追加\n  @max_heap << val\n  # 下から上へヒープ化\n  sift_up(size - 1)\nend\n\n### ノード i から下から上へヒープ化 ###\ndef sift_up(i)\n  loop do\n    # ノード i の親ノードを取得\n    p = parent(i)\n    # 「根ノードを越えた」または「ノードの修復が不要」になったらヒープ化を終了\n    break if p < 0 || @max_heap[i] <= @max_heap[p]\n    # 2 つのノードを交換\n    swap(i, p)\n    # ループで下から上へヒープ化\n    i = p\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#4","level":3,"title":"4.   ヒープ頂点の要素を取り出す","text":"

ヒープ頂点の要素は二分木の根ノード、すなわちリストの先頭要素です。もし先頭要素をそのまま削除すると、二分木内のすべてのノードのインデックスが変化してしまい、その後のヒープ化による修復が困難になります。要素インデックスの変動をできるだけ小さくするため、次の手順を取ります。

  1. ヒープ頂点の要素とヒープ底の要素を交換する(根ノードと最も右の葉ノードを交換する)。
  2. 交換後、ヒープ底をリストから削除する(すでに交換済みであるため、実際に削除されるのは元のヒープ頂点の要素であることに注意)。
  3. 根ノードから開始し、**上から下へヒープ化**を行う。

次の図のように、**「上から下へのヒープ化」の方向は「下から上へのヒープ化」と逆**です。根ノードの値を 2 つの子ノードと比較し、最大の子ノードと根ノードを交換します。その後、この操作を繰り返し、葉ノードを越えるか交換不要のノードに達した時点で終了します。

<1><2><3><4><5><6><7><8><9><10>

図 8-4   ヒープ頂点の要素を取り出す手順

要素をヒープに追加する操作と同様に、ヒープ頂点の要素を取り出す操作の時間計算量も \\(O(\\log n)\\) です。コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def pop(self) -> int:\n    \"\"\"要素をヒープから取り出す\"\"\"\n    # 空判定の処理\n    if self.is_empty():\n        raise IndexError(\"ヒープが空です\")\n    # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    self.swap(0, self.size() - 1)\n    # ノードを削除\n    val = self.max_heap.pop()\n    # 上から下へヒープ化\n    self.sift_down(0)\n    # ヒープ先頭要素を返す\n    return val\n\ndef sift_down(self, i: int):\n    \"\"\"ノード i から始めて、上から下へヒープ化\"\"\"\n    while True:\n        # ノード i, l, r のうち値が最大のノードを ma とする\n        l, r, ma = self.left(i), self.right(i), i\n        if l < self.size() and self.max_heap[l] > self.max_heap[ma]:\n            ma = l\n        if r < self.size() and self.max_heap[r] > self.max_heap[ma]:\n            ma = r\n        # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i:\n            break\n        # 2 つのノードを交換\n        self.swap(i, ma)\n        # ループで上から下へヒープ化\n        i = ma\n
my_heap.cpp
/* 要素をヒープから取り出す */\nvoid pop() {\n    // 空判定の処理\n    if (isEmpty()) {\n        throw out_of_range(\"ヒープが空です\");\n    }\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    swap(maxHeap[0], maxHeap[size() - 1]);\n    // ノードを削除\n    maxHeap.pop_back();\n    // 上から下へヒープ化\n    siftDown(0);\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nvoid siftDown(int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i)\n            break;\n        swap(maxHeap[i], maxHeap[ma]);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.java
/* 要素をヒープから取り出す */\nint pop() {\n    // 空判定の処理\n    if (isEmpty())\n        throw new IndexOutOfBoundsException();\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    swap(0, size() - 1);\n    // ノードを削除\n    int val = maxHeap.remove(size() - 1);\n    // 上から下へヒープ化\n    siftDown(0);\n    // ヒープ先頭要素を返す\n    return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nvoid siftDown(int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap.get(l) > maxHeap.get(ma))\n            ma = l;\n        if (r < size() && maxHeap.get(r) > maxHeap.get(ma))\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i)\n            break;\n        // 2 つのノードを交換\n        swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.cs
/* 要素をヒープから取り出す */\nint Pop() {\n    // 空判定の処理\n    if (IsEmpty())\n        throw new IndexOutOfRangeException();\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    Swap(0, Size() - 1);\n    // ノードを削除\n    int val = maxHeap.Last();\n    maxHeap.RemoveAt(Size() - 1);\n    // 上から下へヒープ化\n    SiftDown(0);\n    // ヒープ先頭要素を返す\n    return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nvoid SiftDown(int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = Left(i), r = Right(i), ma = i;\n        if (l < Size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < Size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // 「ノード i が最大」または「葉ノードを越えた」場合は、ヒープ化を終了する\n        if (ma == i) break;\n        // 2 つのノードを交換\n        Swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.go
/* 要素をヒープから取り出す */\nfunc (h *maxHeap) pop() any {\n    // 空判定の処理\n    if h.isEmpty() {\n        fmt.Println(\"error\")\n        return nil\n    }\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    h.swap(0, h.size()-1)\n    // ノードを削除\n    val := h.data[len(h.data)-1]\n    h.data = h.data[:len(h.data)-1]\n    // 上から下へヒープ化\n    h.siftDown(0)\n\n    // ヒープ先頭要素を返す\n    return val\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nfunc (h *maxHeap) siftDown(i int) {\n    for true {\n        // ノード i, l, r のうち値が最大のノードを max とする\n        l, r, max := h.left(i), h.right(i), i\n        if l < h.size() && h.data[l].(int) > h.data[max].(int) {\n            max = l\n        }\n        if r < h.size() && h.data[r].(int) > h.data[max].(int) {\n            max = r\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if max == i {\n            break\n        }\n        // 2 つのノードを交換\n        h.swap(i, max)\n        // ループで上から下へヒープ化\n        i = max\n    }\n}\n
my_heap.swift
/* 要素をヒープから取り出す */\nfunc pop() -> Int {\n    // 空判定の処理\n    if isEmpty() {\n        fatalError(\"ヒープが空です\")\n    }\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    swap(i: 0, j: size() - 1)\n    // ノードを削除\n    let val = maxHeap.remove(at: size() - 1)\n    // 上から下へヒープ化\n    siftDown(i: 0)\n    // ヒープ先頭要素を返す\n    return val\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nfunc siftDown(i: Int) {\n    var i = i\n    while true {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let l = left(i: i)\n        let r = right(i: i)\n        var ma = i\n        if l < size(), maxHeap[l] > maxHeap[ma] {\n            ma = l\n        }\n        if r < size(), maxHeap[r] > maxHeap[ma] {\n            ma = r\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i {\n            break\n        }\n        // 2 つのノードを交換\n        swap(i: i, j: ma)\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n
my_heap.js
/* 要素をヒープから取り出す */\npop() {\n    // 空判定の処理\n    if (this.isEmpty()) throw new Error('ヒープが空です');\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    this.#swap(0, this.size() - 1);\n    // ノードを削除\n    const val = this.#maxHeap.pop();\n    // 上から下へヒープ化\n    this.#siftDown(0);\n    // ヒープ先頭要素を返す\n    return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\n#siftDown(i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        const l = this.#left(i),\n            r = this.#right(i);\n        let ma = i;\n        if (l < this.size() && this.#maxHeap[l] > this.#maxHeap[ma]) ma = l;\n        if (r < this.size() && this.#maxHeap[r] > this.#maxHeap[ma]) ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma === i) break;\n        // 2 つのノードを交換\n        this.#swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.ts
/* 要素をヒープから取り出す */\npop(): number {\n    // 空判定の処理\n    if (this.isEmpty()) throw new RangeError('Heap is empty.');\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    this.swap(0, this.size() - 1);\n    // ノードを削除\n    const val = this.maxHeap.pop();\n    // 上から下へヒープ化\n    this.siftDown(0);\n    // ヒープ先頭要素を返す\n    return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nsiftDown(i: number): void {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        const l = this.left(i),\n            r = this.right(i);\n        let ma = i;\n        if (l < this.size() && this.maxHeap[l] > this.maxHeap[ma]) ma = l;\n        if (r < this.size() && this.maxHeap[r] > this.maxHeap[ma]) ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma === i) break;\n        // 2 つのノードを交換\n        this.swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.dart
/* 要素をヒープから取り出す */\nint pop() {\n  // 空判定の処理\n  if (isEmpty()) throw Exception('ヒープが空です');\n  // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n  _swap(0, size() - 1);\n  // ノードを削除\n  int val = _maxHeap.removeLast();\n  // 上から下へヒープ化\n  siftDown(0);\n  // ヒープ先頭要素を返す\n  return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nvoid siftDown(int i) {\n  while (true) {\n    // ノード i, l, r のうち値が最大のノードを ma とする\n    int l = _left(i);\n    int r = _right(i);\n    int ma = i;\n    if (l < size() && _maxHeap[l] > _maxHeap[ma]) ma = l;\n    if (r < size() && _maxHeap[r] > _maxHeap[ma]) ma = r;\n    // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n    if (ma == i) break;\n    // 2 つのノードを交換\n    _swap(i, ma);\n    // ループで上から下へヒープ化\n    i = ma;\n  }\n}\n
my_heap.rs
/* 要素をヒープから取り出す */\nfn pop(&mut self) -> i32 {\n    // 空判定の処理\n    if self.is_empty() {\n        panic!(\"index out of bounds\");\n    }\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    self.swap(0, self.size() - 1);\n    // ノードを削除\n    let val = self.max_heap.pop().unwrap();\n    // 上から下へヒープ化\n    self.sift_down(0);\n    // ヒープ先頭要素を返す\n    val\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nfn sift_down(&mut self, mut i: usize) {\n    loop {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let (l, r, mut ma) = (Self::left(i), Self::right(i), i);\n        if l < self.size() && self.max_heap[l] > self.max_heap[ma] {\n            ma = l;\n        }\n        if r < self.size() && self.max_heap[r] > self.max_heap[ma] {\n            ma = r;\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i {\n            break;\n        }\n        // 2 つのノードを交換\n        self.swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n
my_heap.c
/* 要素をヒープから取り出す */\nint pop(MaxHeap *maxHeap) {\n    // 空判定の処理\n    if (isEmpty(maxHeap)) {\n        printf(\"heap is empty!\");\n        return INT_MAX;\n    }\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    swap(maxHeap, 0, size(maxHeap) - 1);\n    // ノードを削除\n    int val = maxHeap->data[maxHeap->size - 1];\n    maxHeap->size--;\n    // 上から下へヒープ化\n    siftDown(maxHeap, 0);\n\n    // ヒープ先頭要素を返す\n    return val;\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nvoid siftDown(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを max とする\n        int l = left(maxHeap, i);\n        int r = right(maxHeap, i);\n        int max = i;\n        if (l < size(maxHeap) && maxHeap->data[l] > maxHeap->data[max]) {\n            max = l;\n        }\n        if (r < size(maxHeap) && maxHeap->data[r] > maxHeap->data[max]) {\n            max = r;\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (max == i) {\n            break;\n        }\n        // 2 つのノードを交換\n        swap(maxHeap, i, max);\n        // ループで上から下へヒープ化\n        i = max;\n    }\n}\n
my_heap.kt
/* 要素をヒープから取り出す */\nfun pop(): Int {\n    // 空判定の処理\n    if (isEmpty()) throw IndexOutOfBoundsException()\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    swap(0, size() - 1)\n    // ノードを削除\n    val _val = maxHeap.removeAt(size() - 1)\n    // 上から下へヒープ化\n    siftDown(0)\n    // ヒープ先頭要素を返す\n    return _val\n}\n\n/* ノード i から始めて、上から下へヒープ化 */\nfun siftDown(it: Int) {\n    // Kotlin の関数引数は不変のため、一時変数を作成する\n    var i = it\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        val l = left(i)\n        val r = right(i)\n        var ma = i\n        if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n        if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i) break\n        // 2 つのノードを交換\n        swap(i, ma)\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n
my_heap.rb
### 要素をヒープから取り出す ###\ndef pop\n  # 空判定の処理\n  raise IndexError, \"ヒープが空です\" if is_empty?\n  # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n  swap(0, size - 1)\n  # ノードを削除\n  val = @max_heap.pop\n  # 上から下へヒープ化\n  sift_down(0)\n  # ヒープ先頭要素を返す\n  val\nend\n\n### ノード i から上から下へヒープ化 ###\ndef sift_down(i)\n  loop do\n    # ノード i, l, r のうち値が最大のノードを ma とする\n    l, r, ma = left(i), right(i), i\n    ma = l if l < size && @max_heap[l] > @max_heap[ma]\n    ma = r if r < size && @max_heap[r] > @max_heap[ma]\n\n    # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n    break if ma == i\n\n    # 2 つのノードを交換\n    swap(i, ma)\n    # ループで上から下へヒープ化\n    i = ma\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/heap/#813","level":2,"title":"8.1.3   ヒープの代表的な応用","text":"
  • 優先度付きキュー:ヒープは、優先度付きキューを実装するための代表的なデータ構造です。キューへの追加と取り出しの時間計算量はいずれも \\(O(\\log n)\\) で、ヒープ構築は \\(O(n)\\) であり、これらの操作はいずれも非常に効率的です。
  • ヒープソート:与えられたデータ群からヒープを構築し、要素の取り出しを繰り返すことで整列済みデータを得られます。ただし、通常はより洗練された方法でヒープソートを実装します。詳しくは「ヒープソート」の章を参照してください。
  • 最大の \\(k\\) 個の要素を取得:これは古典的なアルゴリズム問題であると同時に、典型的な応用でもあります。たとえば、人気上位 10 件のニュースをホットトピックとして選んだり、売上上位 10 件の商品を選んだりする場面です。
","path":["第 8 章   ヒープ","8.1   ヒープ"],"tags":[]},{"location":"chapter_heap/summary/","level":1,"title":"8.4   まとめ","text":"","path":["第 8 章   ヒープ","8.4   まとめ"],"tags":[]},{"location":"chapter_heap/summary/#1","level":3,"title":"1.   重要なポイントの振り返り","text":"
  • ヒープは完全二分木であり、条件の違いによって最大ヒープと最小ヒープに分けられる。最大(最小)ヒープの根の要素は最大値(最小値)である。
  • 優先度付きキューとは、取り出し時に優先度が考慮されるキューであり、通常はヒープを用いて実装される。
  • ヒープの代表的な操作とそれに対応する時間計算量には、要素の挿入 \\(O(\\log n)\\)、根の要素の削除 \\(O(\\log n)\\)、根の要素へのアクセス \\(O(1)\\) などがある。
  • 完全二分木は配列で表現するのに非常に適しているため、通常は配列を使ってヒープを格納する。
  • ヒープ化操作はヒープの性質を保つために用いられ、挿入操作と削除操作の両方で使用される。
  • \\(n\\) 個の要素を入力してヒープを構築する時間計算量は \\(O(n)\\) まで最適化でき、非常に効率的である。
  • Top-k は古典的なアルゴリズム問題であり、ヒープ構造を用いることで効率的に解くことができ、時間計算量は \\(O(n \\log k)\\) である。
","path":["第 8 章   ヒープ","8.4   まとめ"],"tags":[]},{"location":"chapter_heap/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:データ構造の「ヒープ」とメモリ管理の「ヒープ」は同じ概念ですか?

両者は同じ概念ではなく、たまたまどちらも「ヒープ」と呼ばれているだけである。コンピュータシステムのメモリにおけるヒープは動的メモリ割り当ての一部であり、プログラムは実行時にこれを使ってデータを格納できる。プログラムは一定量のヒープメモリを要求し、オブジェクトや配列などの複雑な構造を保存できる。これらのデータが不要になったときは、メモリリークを防ぐためにそのメモリを解放する必要がある。スタックメモリと比べると、ヒープメモリの管理と使用にはより慎重さが求められ、不適切に扱うとメモリリークやダングリングポインタなどの問題を引き起こす可能性がある。

","path":["第 8 章   ヒープ","8.4   まとめ"],"tags":[]},{"location":"chapter_heap/top_k/","level":1,"title":"8.3   Top-k 問題","text":"

Question

長さ \\(n\\) の未整列配列 nums が与えられたとき、配列内で最大の \\(k\\) 個の要素を返してください。

この問題について、まずは発想が比較的直接的な 2 つの解法を紹介し、その後でより効率の高いヒープ解法を紹介します。

","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#831","level":2,"title":"8.3.1   方法一:走査による選択","text":"

以下の図に示すように \\(k\\) 回の走査を行い、各ラウンドでそれぞれ第 \\(1\\)、\\(2\\)、\\(\\dots\\)、\\(k\\) 位の要素を取り出すことができます。時間計算量は \\(O(nk)\\) です。

この方法は \\(k \\ll n\\) の場合にしか適していません。\\(k\\) が \\(n\\) にかなり近いと、時間計算量は \\(O(n^2)\\) に近づき、非常に時間がかかるためです。

図 8-6   走査によって最大の k 個の要素を探す

Tip

\\(k = n\\) のとき、完全な昇順列を得ることができ、この場合は「選択ソート」アルゴリズムと等価になります。

","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#832","level":2,"title":"8.3.2   方法二:ソート","text":"

以下の図に示すように、まず配列 nums をソートし、その後で右端の \\(k\\) 個の要素を返すことができます。時間計算量は \\(O(n \\log n)\\) です。

明らかに、この方法は必要以上の処理を行っています。なぜなら、必要なのは最大の \\(k\\) 個の要素を見つけることだけであり、他の要素をソートする必要はないからです。

図 8-7   ソートによって最大の k 個の要素を探す

","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_heap/top_k/#833","level":2,"title":"8.3.3   方法三:ヒープ","text":"

ヒープを用いることで、Top-k 問題をより効率的に解くことができます。手順は以下の図のとおりです。

  1. 最小ヒープを初期化し、そのヒープ頂点の要素が最小となるようにします。
  2. まず配列の先頭 \\(k\\) 個の要素を順にヒープへ挿入します。
  3. \\(k + 1\\) 番目の要素から開始し、現在の要素がヒープ頂点の要素より大きければ、ヒープ頂点の要素を取り出し、現在の要素をヒープへ挿入します。
  4. 走査が完了した後、ヒープに保持されているのが最大の \\(k\\) 個の要素です。
<1><2><3><4><5><6><7><8><9>

図 8-8   ヒープに基づいて最大の k 個の要素を探す

サンプルコードは以下のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby top_k.py
def top_k_heap(nums: list[int], k: int) -> list[int]:\n    \"\"\"ヒープに基づいて配列中の最大の k 個の要素を探す\"\"\"\n    # 最小ヒープを初期化\n    heap = []\n    # 配列の先頭 k 個の要素をヒープに追加\n    for i in range(k):\n        heapq.heappush(heap, nums[i])\n    # k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for i in range(k, len(nums)):\n        # 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if nums[i] > heap[0]:\n            heapq.heappop(heap)\n            heapq.heappush(heap, nums[i])\n    return heap\n
top_k.cpp
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\npriority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {\n    // 最小ヒープを初期化\n    priority_queue<int, vector<int>, greater<int>> heap;\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (int i = 0; i < k; i++) {\n        heap.push(nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (int i = k; i < nums.size(); i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > heap.top()) {\n            heap.pop();\n            heap.push(nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.java
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nQueue<Integer> topKHeap(int[] nums, int k) {\n    // 最小ヒープを初期化\n    Queue<Integer> heap = new PriorityQueue<Integer>();\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (int i = 0; i < k; i++) {\n        heap.offer(nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (int i = k; i < nums.length; i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > heap.peek()) {\n            heap.poll();\n            heap.offer(nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.cs
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nPriorityQueue<int, int> TopKHeap(int[] nums, int k) {\n    // 最小ヒープを初期化\n    PriorityQueue<int, int> heap = new();\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (int i = 0; i < k; i++) {\n        heap.Enqueue(nums[i], nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (int i = k; i < nums.Length; i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > heap.Peek()) {\n            heap.Dequeue();\n            heap.Enqueue(nums[i], nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.go
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfunc topKHeap(nums []int, k int) *minHeap {\n    // 最小ヒープを初期化\n    h := &minHeap{}\n    heap.Init(h)\n    // 配列の先頭 k 個の要素をヒープに追加\n    for i := 0; i < k; i++ {\n        heap.Push(h, nums[i])\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for i := k; i < len(nums); i++ {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if nums[i] > h.Top().(int) {\n            heap.Pop(h)\n            heap.Push(h, nums[i])\n        }\n    }\n    return h\n}\n
top_k.swift
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfunc topKHeap(nums: [Int], k: Int) -> [Int] {\n    // 最小ヒープを初期化し、先頭 k 個の要素でヒープを構築する\n    var heap = Heap(nums.prefix(k))\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for i in nums.indices.dropFirst(k) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if nums[i] > heap.min()! {\n            _ = heap.removeMin()\n            heap.insert(nums[i])\n        }\n    }\n    return heap.unordered\n}\n
top_k.js
/* 要素をヒープに追加 */\nfunction pushMinHeap(maxHeap, val) {\n    // 要素を反転する\n    maxHeap.push(-val);\n}\n\n/* 要素をヒープから取り出す */\nfunction popMinHeap(maxHeap) {\n    // 要素を反転する\n    return -maxHeap.pop();\n}\n\n/* ヒープ先頭要素にアクセス */\nfunction peekMinHeap(maxHeap) {\n    // 要素を反転する\n    return -maxHeap.peek();\n}\n\n/* ヒープから要素を取り出す */\nfunction getMinHeap(maxHeap) {\n    // 要素を反転する\n    return maxHeap.getMaxHeap().map((num) => -num);\n}\n\n/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfunction topKHeap(nums, k) {\n    // 最小ヒープを初期化する\n    // 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする\n    const maxHeap = new MaxHeap([]);\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (let i = k; i < nums.length; i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // ヒープ内の要素を返す\n    return getMinHeap(maxHeap);\n}\n
top_k.ts
/* 要素をヒープに追加 */\nfunction pushMinHeap(maxHeap: MaxHeap, val: number): void {\n    // 要素を反転する\n    maxHeap.push(-val);\n}\n\n/* 要素をヒープから取り出す */\nfunction popMinHeap(maxHeap: MaxHeap): number {\n    // 要素を反転する\n    return -maxHeap.pop();\n}\n\n/* ヒープ先頭要素にアクセス */\nfunction peekMinHeap(maxHeap: MaxHeap): number {\n    // 要素を反転する\n    return -maxHeap.peek();\n}\n\n/* ヒープから要素を取り出す */\nfunction getMinHeap(maxHeap: MaxHeap): number[] {\n    // 要素を反転する\n    return maxHeap.getMaxHeap().map((num: number) => -num);\n}\n\n/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfunction topKHeap(nums: number[], k: number): number[] {\n    // 最小ヒープを初期化する\n    // 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする\n    const maxHeap = new MaxHeap([]);\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (let i = k; i < nums.length; i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // ヒープ内の要素を返す\n    return getMinHeap(maxHeap);\n}\n
top_k.dart
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nMinHeap topKHeap(List<int> nums, int k) {\n  // 最小ヒープを初期化し、配列の先頭 k 個の要素をヒープに入れる\n  MinHeap heap = MinHeap(nums.sublist(0, k));\n  // k+1 番目の要素から開始し、ヒープ長を k に保つ\n  for (int i = k; i < nums.length; i++) {\n    // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n    if (nums[i] > heap.peek()) {\n      heap.pop();\n      heap.push(nums[i]);\n    }\n  }\n  return heap;\n}\n
top_k.rs
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {\n    // BinaryHeap は最大ヒープであり、Reverse で要素の順序を反転することで最小ヒープを実現する\n    let mut heap = BinaryHeap::<Reverse<i32>>::new();\n    // 配列の先頭 k 個の要素をヒープに追加\n    for &num in nums.iter().take(k) {\n        heap.push(Reverse(num));\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for &num in nums.iter().skip(k) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if num > heap.peek().unwrap().0 {\n            heap.pop();\n            heap.push(Reverse(num));\n        }\n    }\n    heap\n}\n
top_k.c
/* 要素をヒープに追加 */\nvoid pushMinHeap(MaxHeap *maxHeap, int val) {\n    // 要素を反転する\n    push(maxHeap, -val);\n}\n\n/* 要素をヒープから取り出す */\nint popMinHeap(MaxHeap *maxHeap) {\n    // 要素を反転する\n    return -pop(maxHeap);\n}\n\n/* ヒープ先頭要素にアクセス */\nint peekMinHeap(MaxHeap *maxHeap) {\n    // 要素を反転する\n    return -peek(maxHeap);\n}\n\n/* ヒープから要素を取り出す */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // ヒープ内のすべての要素を反転して res 配列に格納\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n/* ヒープから要素を取り出す */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // ヒープ内のすべての要素を反転して res 配列に格納\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n// ヒープに基づいて配列中の最大の k 個の要素を求める関数\nint *topKHeap(int *nums, int sizeNums, int k) {\n    // 最小ヒープを初期化する\n    // 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする\n    int *empty = (int *)malloc(0);\n    MaxHeap *maxHeap = newMaxHeap(empty, 0);\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (int i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (int i = k; i < sizeNums; i++) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    int *res = getMinHeap(maxHeap);\n    // メモリを解放する\n    delMaxHeap(maxHeap);\n    return res;\n}\n
top_k.kt
/* ヒープに基づいて配列中の最大の k 個の要素を探す */\nfun topKHeap(nums: IntArray, k: Int): Queue<Int> {\n    // 最小ヒープを初期化\n    val heap = PriorityQueue<Int>()\n    // 配列の先頭 k 個の要素をヒープに追加\n    for (i in 0..<k) {\n        heap.offer(nums[i])\n    }\n    // k+1 番目の要素から開始し、ヒープ長を k に保つ\n    for (i in k..<nums.size) {\n        // 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n        if (nums[i] > heap.peek()) {\n            heap.poll()\n            heap.offer(nums[i])\n        }\n    }\n    return heap\n}\n
top_k.rb
### ヒープに基づいて配列中の最大 k 個の要素を探す ###\ndef top_k_heap(nums, k)\n  # 最小ヒープを初期化する\n  # 注意: ヒープ内の全要素を反転し、最大ヒープで最小ヒープをシミュレートする\n  max_heap = MaxHeap.new([])\n\n  # 配列の先頭 k 個の要素をヒープに追加\n  for i in 0...k\n    push_min_heap(max_heap, nums[i])\n  end\n\n  # k+1 番目の要素から開始し、ヒープ長を k に保つ\n  for i in k...nums.length\n    # 現在の要素がヒープ先頭より大きければ、ヒープ先頭を取り出して現在の要素を追加する\n    if nums[i] > peek_min_heap(max_heap)\n      pop_min_heap(max_heap)\n      push_min_heap(max_heap, nums[i])\n    end\n  end\n\n  get_min_heap(max_heap)\nend\n
コードの可視化

全画面で見る >

合計で \\(n\\) 回のヒープ挿入と取り出しを行い、ヒープの最大長は \\(k\\) であるため、時間計算量は \\(O(n \\log k)\\) です。この方法は非常に効率が高く、\\(k\\) が小さいときは時間計算量が \\(O(n)\\) に近づき、\\(k\\) が大きいときでも \\(O(n \\log n)\\) を超えることはありません。

さらに、この方法は動的データストリームの利用シーンにも適しています。データが継続的に追加される場合でも、ヒープ内の要素を保ち続けることで、最大の \\(k\\) 個の要素を動的に更新できます。

","path":["第 8 章   ヒープ","8.3   Top-k 問題"],"tags":[]},{"location":"chapter_hello_algo/","level":1,"title":"はじめに","text":"

数年前、私は LeetCode 中国版で「剣指 Offer」シリーズの解説を共有し、多くの読者から励ましと支持をいただきました。読者との交流の中で、最もよく聞かれた質問の一つが「アルゴリズムをどう学び始めればよいか」でした。次第に、私はこの問題に強い関心を抱くようになりました。

手探りでひたすら問題を解くことは、最も人気のある方法のようです。単純で、直接的で、しかも効果的です。しかし問題演習は「マインスイーパー」を遊ぶことに似ており、独学力の高い人は地雷を一つずつうまく取り除ける一方で、基礎が十分でない人は大きな痛手を受け、挫折の中で少しずつ後退してしまいがちです。教材を通読するのもよくある方法ですが、就職を目指す人にとっては、卒業論文、履歴書の提出、筆記試験や面接の準備ですでに大半の力を使い果たしており、分厚い本を読み込むことはしばしば困難な挑戦になってしまいます。

もしあなたも同じような悩みを抱えているなら、この本があなたのもとに“たどり着いた”のは幸運なことです。本書は、この問いに対して私が示す答えです。たとえ最良の解ではなくても、少なくとも前向きな試みではあります。本書だけで直接内定を得られるわけではありませんが、データ構造とアルゴリズムの「知識地図」を探る手助けをし、さまざまな「地雷」の形や大きさ、分布を理解し、いろいろな「地雷除去の方法」を身につけられるよう導きます。こうした力があれば、問題演習や文献読解をより自在に進め、やがて完整な知識体系を築いていけると信じています。

私はファインマン教授の言葉に深く賛同しています。「Knowledge isn't free. You have to pay attention.」この意味において、本書は完全に「無料」ではありません。本書のためにあなたが払ってくれる貴重な「注意」に応えるため、私はできる限りの力を尽くし、最大限の「注意」を注いで本書を書き上げます。

私は自らの学識と力量の浅さをよく承知しています。本書の内容はある程度磨きをかけてきたものの、なお多くの誤りが残っているはずです。先生方、そして学習者の皆さまからのご批判とご指摘を心よりお願いいたします。

Hello、アルゴリズム!

コンピュータの登場は世界に大きな変革をもたらしました。高速な計算能力と優れたプログラム可能性によって、アルゴリズムの実行とデータ処理の理想的な媒体となったのです。ビデオゲームのリアルな映像、自動運転の知的な意思決定、AlphaGo の見事な対局、ChatGPT の自然な対話に至るまで、これらの応用はいずれもコンピュータ上でアルゴリズムが巧みに表現されたものです。

実際には、コンピュータが誕生する以前から、アルゴリズムとデータ構造は世界のいたるところに存在していました。初期のアルゴリズムは比較的単純で、たとえば古代の数え方や道具作りの手順などがそれに当たります。文明の進歩とともに、アルゴリズムは次第により精緻で複雑なものになっていきました。匠の巧みな技から、生産力を解放する工業製品、さらには宇宙の運行を支配する科学法則に至るまで、ほとんどあらゆる平凡なもの、あるいは驚嘆すべきものの背後には、精妙なアルゴリズムの思想が潜んでいます。

同様に、データ構造も至るところに存在します。社会ネットワークのような大きなものから地下鉄路線のような小さなものまで、多くのシステムは「グラフ」としてモデル化できます。国家のような大きな単位から家庭のような小さな単位まで、社会の主な組織形態には「木」の特徴があります。冬服は「スタック」のように、最初に着たものが最後に脱がれます。バドミントンシャトルの筒は「キュー」のように、一方から入れて他方から取り出します。辞書は「ハッシュテーブル」のようなもので、目的の見出し語を素早く探せます。

本書は、わかりやすいアニメーション図解と実行可能なコード例を通じて、読者がアルゴリズムとデータ構造の核心概念を理解し、さらにプログラミングによってそれらを実装できるようになることを目指しています。そのうえで、本書は複雑な世界の中にあるアルゴリズムの生き生きとした現れを明らかにし、アルゴリズムの美しさを示そうとしています。本書があなたの助けになれば幸いです。

","path":["はじめに"],"tags":[]},{"location":"chapter_introduction/","level":1,"title":"第 1 章   アルゴリズム入門","text":"

Abstract

一人の少女が軽やかに舞い、データと織り重なり合いながら、スカートの裾にはアルゴリズムの旋律がたなびいています。

彼女はあなたをこの舞いへと誘います。その足取りに続いて、論理と美しさに満ちたアルゴリズムの世界へ踏み入りましょう。

","path":["第 1 章   アルゴリズムを知る","第 1 章   アルゴリズム入門"],"tags":[]},{"location":"chapter_introduction/#_1","level":2,"title":"章の内容","text":"
  • 1.1   アルゴリズムは至るところにある
  • 1.2   アルゴリズムとは
  • 1.3   まとめ
","path":["第 1 章   アルゴリズムを知る","第 1 章   アルゴリズム入門"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   アルゴリズムは至るところにある","text":"

「アルゴリズム」という言葉を聞くと、自然に数学を思い浮かべます。しかし実際には、多くのアルゴリズムは複雑な数学を必要とせず、むしろ基本的な論理に依存しており、その論理は私たちの日常生活のいたるところで見られます。

アルゴリズムを本格的に議論する前に、ひとつ面白い事実を共有しておきます。あなたはすでに知らず知らずのうちに多くのアルゴリズムを身につけ、それらを日常生活に応用することに慣れているのです。以下では、いくつかの具体例を挙げてこれを示します。

例1:辞書を引く。辞書では、各漢字に対応するピンインがあり、辞書はピンインのアルファベット順に並んでいます。ピンインの先頭文字が \\(r\\) の字を探すと仮定すると、通常は次の図のような方法で行います。

  1. 辞書をおよそ半分のところまで開き、そのページの先頭文字を確認し、先頭文字が \\(m\\) だとします。
  2. ピンインのアルファベット表では \\(r\\) は \\(m\\) の後にあるため、辞書の前半を除外し、探索範囲を後半に絞ります。
  3. ピンインの先頭文字が \\(r\\) のページを見つけるまで、手順 1. と手順 2. を繰り返します。
<1><2><3><4><5>

図 1-1   辞書を引く手順

辞書を引くという小学生の必須スキルは、実は有名な「二分探索」アルゴリズムそのものです。データ構造の観点では、辞書を整列済みの「配列」とみなせます。アルゴリズムの観点では、上記の一連の辞書引きの操作を「二分探索」とみなせます。

例2:トランプを整理する。カードゲームをするとき、毎回手札のトランプを小さい順に並べ替える必要があります。その流れは次の図のとおりです。

  1. トランプを「整列済み」と「未整列」の2つの部分に分け、初期状態では一番左の1枚がすでに整列済みだとします。
  2. 未整列部分から1枚のトランプを取り出し、整列済み部分の正しい位置に挿入します。完了すると、左端の2枚は整列済みになります。
  3. 手順 2. を繰り返し、各ラウンドで未整列部分から1枚を整列済み部分へ挿入し、すべてのトランプが整列済みになるまで続けます。

図 1-2   トランプを並べ替える手順

上記のトランプ整理の方法は、本質的には「挿入ソート」アルゴリズムです。これは小規模なデータ集合を処理する際に非常に効率的で、多くのプログラミング言語のソートライブラリ関数にも挿入ソートが使われています。

例3:お釣りを出す。スーパーで \\(69\\) 元の商品を購入し、店員に \\(100\\) 元渡したとすると、店員は \\(31\\) 元のお釣りを返す必要があります。店員は自然に次の図のような考え方をします。

  1. 選択肢は \\(31\\) 元より小さい額面の貨幣で、\\(1\\) 元、\\(5\\) 元、\\(10\\) 元、\\(20\\) 元があります。
  2. 選択肢の中から最大の \\(20\\) 元を取り出すと、残りは \\(31 - 20 = 11\\) 元です。
  3. 残りの選択肢の中から最大の \\(10\\) 元を取り出すと、残りは \\(11 - 10 = 1\\) 元です。
  4. 残りの選択肢の中から最大の \\(1\\) 元を取り出すと、残りは \\(1 - 1 = 0\\) 元です。
  5. お釣りは完了し、内訳は \\(20 + 10 + 1 = 31\\) 元です。

図 1-3   お釣りの過程

以上の手順では、各ステップでその時点で最善と思われる選択肢を取っています。つまり、できるだけ額面の大きい貨幣を使い、最終的に実行可能なお釣りの方案を得ています。データ構造とアルゴリズムの観点から見ると、この方法は本質的に「貪欲法」です。

料理を一品作ることから星間航行に至るまで、ほとんどあらゆる問題の解決にアルゴリズムは欠かせません。コンピュータの登場によって、プログラミングを通じてデータ構造をメモリに格納し、さらにコードを書いて CPU や GPU にアルゴリズムを実行させることが可能になりました。こうして、生活の中の問題をコンピュータに移し、より効率的な方法でさまざまな複雑な問題を解決できるのです。

Tip

データ構造、アルゴリズム、配列、二分探索といった概念がまだ少し曖昧でも、そのまま読み進めてください。本書がデータ構造とアルゴリズムの知識の世界へと案内します。

","path":["第 1 章   アルゴリズムを知る","1.1   アルゴリズムは至るところにある"],"tags":[]},{"location":"chapter_introduction/summary/","level":1,"title":"1.3   まとめ","text":"","path":["第 1 章   アルゴリズムを知る","1.3   まとめ"],"tags":[]},{"location":"chapter_introduction/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • アルゴリズムは日常生活の至る所にあり、決して手の届かない難解な知識ではありません。実際、私たちは気づかないうちに多くのアルゴリズムを身につけ、生活のさまざまな問題を解決しています。
  • 辞書を引く原理は二分探索アルゴリズムと一致しています。二分探索アルゴリズムは分割統治という重要なアルゴリズム思想を体現しています。
  • トランプを整理する過程は挿入ソートアルゴリズムと非常によく似ています。挿入ソートアルゴリズムは小規模なデータ集合のソートに適しています。
  • 貨幣の釣り銭を求める手順の本質は貪欲アルゴリズムであり、各ステップでその時点で最善と思われる選択を取ります。
  • アルゴリズムとは、限られた時間内に特定の問題を解決するための一連の命令または操作手順であり、データ構造とは、コンピュータ内でデータを組織し保存する方法です。
  • データ構造とアルゴリズムは密接に結びついています。データ構造はアルゴリズムの土台であり、アルゴリズムはデータ構造に生命を吹き込みます。
  • データ構造とアルゴリズムは積み木の組み立てにたとえることができます。積み木はデータを表し、積み木の形や接続方法などはデータ構造を表し、積み木を組み立てる手順がアルゴリズムに対応します。
","path":["第 1 章   アルゴリズムを知る","1.3   まとめ"],"tags":[]},{"location":"chapter_introduction/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:プログラマーとして、私は日常業務でアルゴリズムを使って問題を解決したことがありません。よく使うアルゴリズムはプログラミング言語にすべてカプセル化されており、そのまま使えばよいです。これは、仕事上の問題がまだアルゴリズムを必要とする段階に達していないことを意味するのでしょうか?

具体的な仕事のスキルを武術の「型」にたとえるなら、基礎科目はむしろ「内功」に近いものです。

私は、アルゴリズム(およびその他の基礎科目)を学ぶ意義は、仕事でそれをゼロから実装することではなく、学んだ知識に基づいて問題解決の際に専門的な反応や判断を下せるようになり、その結果として仕事全体の品質を高めることにあると考えています。簡単な例を挙げると、どのプログラミング言語にもソート関数が組み込まれています。

  • もしデータ構造とアルゴリズムを学んでいなければ、どんなデータが与えられても、そのソート関数に任せてしまうかもしれません。問題なく動き、性能も悪くなく、一見すると特に問題はありません。
  • しかしアルゴリズムを学んでいれば、組み込みのソート関数の時間計算量が \\(O(n \\log n)\\) であることを知っています。さらに、与えられたデータが固定桁数の整数(例えば学籍番号)であれば、より効率の高い「基数ソート」を使って、時間計算量を \\(O(nk)\\) に下げることができます。ここで \\(k\\) は桁数です。データ量が非常に大きい場合、節約できた実行時間は大きな価値を生みます(コスト削減、体験向上など)。

工学分野では、多くの問題で最適解に到達することは難しく、少なくない問題は「だいたい」解決されているにすぎません。問題の難しさは、一方では問題そのものの性質に依存し、他方ではそれを観測する人の知識の蓄積にも依存します。知識が充実し、経験が豊富であるほど、問題分析はより深くなり、問題はより洗練された形で解決できるようになります。

","path":["第 1 章   アルゴリズムを知る","1.3   まとめ"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/","level":1,"title":"1.2   アルゴリズムとは","text":"","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#121","level":2,"title":"1.2.1   アルゴリズムの定義","text":"

アルゴリズム(algorithm)とは、限られた時間内に特定の問題を解決するための一連の命令または操作手順であり、次のような特徴を持ちます。

  • 問題が明確であり、入力と出力の定義がはっきりしています。
  • 実行可能であり、有限の手順、時間、メモリ空間で完了できます。
  • 各手順の意味が確定しており、同じ入力と実行条件では常に同じ出力になります。
","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#122","level":2,"title":"1.2.2   データ構造の定義","text":"

データ構造(data structure)とは、データを整理して保存する方式であり、データの内容、データ間の関係、データの操作方法を含み、次のような設計目標があります。

  • 使用する空間をできるだけ少なくし、コンピュータのメモリを節約します。
  • データの操作をできるだけ高速にし、アクセス、追加、削除、更新などを含みます。
  • 簡潔なデータ表現と論理情報を提供し、アルゴリズムが効率よく動作できるようにします。

データ構造の設計はトレードオフに満ちた過程です。ある面を改善したい場合、別の面で妥協が必要になることがよくあります。以下に 2 つの例を示します。

  • 連結リストは配列に比べてデータの追加や削除がしやすい一方で、データアクセス速度を犠牲にしています。
  • グラフは連結リストに比べてより豊富な論理情報を提供しますが、より大きなメモリ空間を必要とします。
","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123","level":2,"title":"1.2.3   データ構造とアルゴリズムの関係","text":"

以下の図のように、データ構造とアルゴリズムは高度に関連し、密接に結び付いており、具体的には次の 3 つの点に表れます。

  • データ構造はアルゴリズムの土台です。データ構造はアルゴリズムに対して、構造化して格納されたデータと、そのデータを操作する方法を提供します。
  • アルゴリズムはデータ構造に命を吹き込みます。データ構造そのものはデータ情報を保存するだけであり、アルゴリズムと組み合わせて初めて特定の問題を解決できます。
  • アルゴリズムは通常、異なるデータ構造に基づいて実装できますが、実行効率が大きく異なる場合があり、適切なデータ構造を選ぶことが重要です。

図 1-4   データ構造とアルゴリズムの関係

データ構造とアルゴリズムは、以下の図に示す組み立てブロックのようなものです。1 セットのブロックには多くの部品が含まれるだけでなく、詳しい組み立て説明書も付いています。説明書に従って一歩ずつ操作すれば、精巧なブロック模型を組み立てられます。

図 1-5   組み立てブロック

両者の詳細な対応関係を次の表に示します。

表 1-1   データ構造とアルゴリズムを組み立てブロックにたとえる

データ構造とアルゴリズム 組み立てブロック 入力データ まだ組み立てていないブロック データ構造 ブロックの構成形式。形状、大きさ、接続方法などを含む アルゴリズム ブロックを目標の形に組み上げる一連の操作手順 出力データ ブロック模型

特筆すべき点として、データ構造とアルゴリズムはプログラミング言語から独立しています。だからこそ、本書では複数のプログラミング言語に基づく実装を提供できます。

慣習的な略称

実際の議論では、私たちは通常「データ構造とアルゴリズム」を略して「アルゴリズム」と呼びます。たとえば広く知られている LeetCode のアルゴリズム問題は、実際にはデータ構造とアルゴリズムの両方の知識を同時に問うています。

","path":["第 1 章   アルゴリズムを知る","1.2   アルゴリズムとは"],"tags":[]},{"location":"chapter_paperbook/","level":1,"title":"紙の書籍","text":"

長い時間をかけて磨き上げた『Hello アルゴリズム』の紙の書籍が、ついに発売されました!今の気持ちは、次の一節で表せます:

風を追い月を追って立ち止まるな、草原の果てには春の山がある。

以下の動画では紙の書籍を紹介しており、私の考えもいくつか含まれています:

  • データ構造とアルゴリズムを学ぶ重要性。
  • なぜ紙の書籍で Python を選んだのか。
  • 知識共有に対する理解。

新人 UP 主ですので、ぜひ応援と高評価・チャンネル登録をお願いします~ありがとうございます!

紙の書籍のスナップショット:

","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_2","level":2,"title":"長所と短所","text":"

紙の書籍ならではの魅力を、簡単にまとめると次のとおりです:

  • フルカラー印刷を採用し、本書の「アニメーション図解」の強みをそのまま活かせます。
  • 紙の素材にもこだわり、色彩を高い精度で再現しつつ、紙の書籍ならではの質感も残しています。
  • 紙の書籍版は Web 版よりも書式が整っており、たとえば図中の数式には斜体を用いています。
  • 価格を上げずに、マインドマップの折り込みページやしおりも付属します。
  • 紙の書籍、Web 版、PDF 版で内容は同期しており、自由に切り替えて読めます。

Tip

紙の書籍と Web 版を同期させるのは難しいため、細かな違いが生じる場合があります。ご了承ください!

もちろん、購入前に検討しておくべき点もいくつかあります:

  • Python 言語を使用しているため、あなたの主言語と合わない可能性があります(Python は疑似コードと捉え、考え方の理解を重視してください)。
  • フルカラー印刷は図解やコードの読みやすさを大きく高める一方で、白黒印刷より価格はやや高くなります。

Tip

「印刷品質」と「価格」は、アルゴリズムにおける「時間効率」と「空間効率」のようなもので、両立は容易ではありません。そして私は、「印刷品質」は「時間効率」に当たるため、より重視すべきだと考えています。

","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_3","level":2,"title":"購入リンク","text":"

紙の書籍に興味があれば、ぜひ一冊ご検討ください。新刊の 5 割引を用意していただきましたので、こちらのリンクをご覧いただくか、以下の QR コードをスキャンしてください:

","path":["紙の書籍"],"tags":[]},{"location":"chapter_paperbook/#_4","level":2,"title":"あとがき","text":"

当初、私は紙の書籍出版に必要な作業量を甘く見ていて、オープンソースプロジェクトをきちんと保守していれば、紙の書籍版も何らかの自動化手段で生成できると思っていました。実際には、紙の書籍の制作フローとオープンソースプロジェクトの更新の仕組みには大きな違いがあり、その間をつなぐには多くの追加作業が必要でした。

一冊の本の初稿と出版基準を満たす完成稿との間には、なお大きな隔たりがあります。出版社(企画、編集、デザイン、マーケティングなど)と著者が力を合わせ、長い時間をかけて磨き上げていく必要があります。ここで、図霊の企画編集者である王軍花さん、そして人民郵電出版社と図霊コミュニティで本書の出版工程に携わってくださったすべての皆さまに感謝いたします!

この本があなたの助けになれば幸いです!

","path":["紙の書籍"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"第 0 章   はじめに","text":"

Abstract

アルゴリズムは美しい交響曲のようであり、コードの一行一行が旋律のように流れていきます。

この本があなたの心の中でそっと響き、独自で深い旋律を残してくれることを願っています。

","path":["第 0 章   前書き","第 0 章   はじめに"],"tags":[]},{"location":"chapter_preface/#_1","level":2,"title":"章の内容","text":"
  • 0.1   本書について
  • 0.2   本書の使い方
  • 0.3   まとめ
","path":["第 0 章   前書き","第 0 章   はじめに"],"tags":[]},{"location":"chapter_preface/about_the_book/","level":1,"title":"0.1   本書について","text":"

本プロジェクトは、オープンソースで無料、かつ初心者にやさしいデータ構造とアルゴリズムの入門書を作ることを目的としています。

  • 全編でアニメーション付きの図解を採用し、内容は明快で理解しやすく、学習曲線もなだらかで、初心者がデータ構造とアルゴリズムの知識地図を探求できるよう導きます。
  • ソースコードはワンクリックで実行でき、読者が演習を通じてプログラミング能力を高め、アルゴリズムの動作原理とデータ構造の内部実装を理解する助けとなります。
  • 読者どうしの助け合いによる学習を推奨しており、コメント欄で質問や見解を共有し、対話と議論を通じてともに成長していくことを歓迎します。
","path":["第 0 章   前書き","0.1   本書について"],"tags":[]},{"location":"chapter_preface/about_the_book/#011","level":2,"title":"0.1.1   対象読者","text":"

もしあなたがアルゴリズム初心者で、これまでアルゴリズムに触れたことがない、あるいはすでに多少の問題演習の経験はあるものの、データ構造とアルゴリズムについてはまだ曖昧な理解にとどまり、できるかできないかの間を行き来しているなら、本書はまさにあなたのために作られています!

もしすでに一定量の問題演習を積み、ほとんどの問題パターンに慣れているなら、本書はアルゴリズム知識体系の復習と整理に役立ちます。リポジトリのソースコードは「問題演習ツール集」や「アルゴリズム辞典」として活用できます。

もしあなたがアルゴリズムの「達人」なら、貴重なご提案をいただけることを楽しみにしています。あるいは一緒に執筆に参加してください。

前提条件

少なくともいずれか一つの言語でのプログラミング基礎があり、簡単なコードを読んだり書いたりできる必要があります。

","path":["第 0 章   前書き","0.1   本書について"],"tags":[]},{"location":"chapter_preface/about_the_book/#012","level":2,"title":"0.1.2   内容構成","text":"

本書の主な内容は以下の図のとおりです。

  • 計算量解析:データ構造とアルゴリズムを評価する観点と方法。時間計算量と空間計算量の求め方、代表的な種類、例など。
  • データ構造:基本データ型とデータ構造の分類方法。配列、連結リスト、スタック、キュー、ハッシュテーブル、木、ヒープ、グラフなどのデータ構造の定義、長所と短所、基本操作、代表的な種類、典型的な応用、実装方法など。
  • アルゴリズム:探索、ソート、分割統治、バックトラッキング、動的計画法、貪欲法などのアルゴリズムの定義、長所と短所、効率、適用場面、問題を解く手順、例題など。

図 0-1   本書の主な内容

","path":["第 0 章   前書き","0.1   本書について"],"tags":[]},{"location":"chapter_preface/about_the_book/#013","level":2,"title":"0.1.3   謝辞","text":"

本書は、オープンソースコミュニティの多くの貢献者による共同の努力のもとで、継続的に改善されています。時間と労力を注いでくださったすべての執筆者の皆さんに感謝します。お名前は次のとおりです(GitHub により自動生成された順序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、IsChristina、xBLACKICEx、guowei-gong、Cathay-Chen、pengchzn、QiLOL、magentaqin、hello-ikun、JoseHung、qualifier1024、thomasq0、sunshinesDL、L-Super、Guanngxu、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、Shyam-Chen、theNefelibatas、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、MolDuM、Nigh、Dr-XYZ、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、beatrix-chan、DullSword、xjr7670、jiaxianhua、qq909244296、iStig、boloboloda、hts0000、gledfish、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、llql1211、fbigm、echo1937、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、GaochaoZhu、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Allen-Scai、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、zhongfq、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Keynman、KeiichiKasai と KawaiiAsh。

本書のコードレビューは coderonion、curtishd、Gonglja、gvenusleo、hpstory、justin-tse、khoaxuantu、krahets、night-cruise、nuomi1、Reanon と rongyi によって行われました(アルファベット順)。彼らが費やしてくれた時間と労力に感謝します。各言語のコードの規範性と統一性が保たれているのは、まさに彼らのおかげです。

本書の繁体字中国語版は Shyam-Chen と Dr-XYZ がレビューし、英語版は yuelinxin、K3v123、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn、thomasq0 と magentaqin がレビューし、日本語版は eltociear がレビューしました。彼らの継続的な貢献があってこそ、本書はより幅広い読者に届けられています。感謝いたします。

本書の ePub 電子書籍生成ツールは zhongfq によって開発されました。彼の貢献に感謝します。読者により自由な読書方法を提供してくれました。

本書の執筆過程で、私は多くの方々の助けを得ました。

  • 会社での私の指導教員である李汐博士に感謝します。ある対話の中で「すぐに行動しよう」と励ましてくださり、この本を書く決意を固めることができました;
  • 私の恋人であり、本書の最初の読者でもある泡泡に感謝します。アルゴリズム初心者の視点から多くの貴重な提案をしてくれたおかげで、本書はより初心者に適したものになりました;
  • 腾宝、琦宝、飞宝が本書に創造性あふれる名前を付けてくれたことに感謝します。みんなが最初のコード行「Hello World!」を書いた美しい記憶を呼び起こしてくれました;
  • 校铨が知的財産の面で専門的な支援をしてくれたことに感謝します。これは本オープンソース書籍の改善に重要な役割を果たしました;
  • 苏潼が本書の美しい表紙と logo をデザインし、私の完璧主義につき合って何度も辛抱強く修正してくれたことに感謝します;
  • @squidfunk が組版に関する助言を提供してくれたこと、そして彼が開発したオープンソースのドキュメントテーマ Material-for-MkDocs に感謝します。

執筆の過程で、私はデータ構造とアルゴリズムに関する多くの教材や記事を読みました。これらの作品は本書に優れた手本を与え、本書の内容の正確性と品質を支えてくれました。ここに、すべての先生方と先人たちの卓越した貢献に感謝します!

本書は手と頭を同時に使う学習方法を提唱しています。この点で私は『手を動かして学ぶ深層学習』から大きな啓発を受けました。ここで読者の皆さんにこの優れた著作を強くお勧めします。

心から両親に感謝します。いつも支え励ましてくれたからこそ、私はこの興味深いことに取り組む機会を得ることができました。

","path":["第 0 章   前書き","0.1   本書について"],"tags":[]},{"location":"chapter_preface/suggestions/","level":1,"title":"0.2   本書の使い方","text":"

Tip

最適な読書体験を得るために、本節の内容を一通り読むことをおすすめします。

","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#021","level":2,"title":"0.2.1   文章スタイルの約束","text":"
  • 見出しの後に * が付いているのは選読章で、内容は比較的難しめです。時間が限られている場合は、先に読み飛ばしてもかまいません。
  • 専門用語は太字(紙書籍版と PDF 版)または下線付き(Web 版)で示します。たとえば配列(array)のようなものです。文献を読む際に役立つため、覚えておくことをおすすめします。
  • 重要な内容やまとめの文は 太字 で示します。これらの文章には特に注意してください。
  • 特定の意味を持つ語句には“引用符”を付け、曖昧さを避けます。
  • プログラミング言語ごとに用語が一致しない場合、本書では Python を基準とします。たとえば、“空”を表すのに None を使います。
  • 本書では、よりコンパクトなレイアウトのために、言語ごとのコメント規約を一部省略しています。コメントは主に3種類あります。タイトルコメント、内容コメント、複数行コメントです。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
\"\"\"タイトルコメント。関数、クラス、テストケースなどを示すために使います\"\"\"\n\n# 内容コメント。コードを詳しく説明するために使います\n\n\"\"\"\n複数行\nコメント\n\"\"\"\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n// 複数行\n// コメント\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
/* タイトルコメント。関数、クラス、テストケースなどを示すために使います */\n\n// 内容コメント。コードを詳しく説明するために使います\n\n/**\n * 複数行\n * コメント\n */\n
### タイトルコメント。関数、クラス、テストケースなどを示すために使います ###\n\n# 内容コメント。コードを詳しく説明するために使います\n\n# 複数行\n# コメント\n
","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#022","level":2,"title":"0.2.2   アニメーション図解で効率よく学ぶ","text":"

文字と比べて、動画や画像は情報密度と構造化の度合いが高く、理解しやすいものです。本書では、重要かつ難解な知識は主にアニメーションによる図解で示し、文章は説明と補足を担います。

本書を読んでいて、ある内容に以下の図のようなアニメーション図解がある場合は、図を主、文章を従として、両方を合わせて理解してください。

図 0-2   アニメーション図解の例

","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#023","level":2,"title":"0.2.3   コード実践で理解を深める","text":"

本書のサンプルコードは GitHub リポジトリ で管理されています。以下の図のように、ソースコードにはテストケースが付いており、ワンクリックで実行できます。

時間に余裕があれば、コードを見ながら自分で一度書いてみることをおすすめします。学習時間が限られている場合でも、少なくともすべてのコードに目を通し、実行してください。

コードを読むのに比べて、書く過程のほうが得られるものは多いものです。手を動かしてこそ、本当に学んだことになります。

図 0-3   コード実行例

コードを実行する前準備は主に3ステップです。

第1ステップ:ローカルのプログラミング環境をインストールする。付録のチュートリアルを参照してインストールしてください。すでにインストール済みであれば、この手順は省略できます。

第2ステップ:コードリポジトリをクローンまたはダウンロードする。 GitHub リポジトリ にアクセスしてください。すでに Git をインストールしている場合は、次のコマンドでこのリポジトリをクローンできます:

git clone https://github.com/krahets/hello-algo.git\n

もちろん、以下の図に示す場所で“Download ZIP”ボタンをクリックし、コードの圧縮ファイルを直接ダウンロードしてローカルで展開することもできます。

図 0-4   リポジトリのクローンとコードのダウンロード

第3ステップ:ソースコードを実行する。以下の図のように、上部にファイル名が表示されているコードブロックについては、リポジトリの codes フォルダ内に対応するソースコードファイルがあります。ソースコードファイルはワンクリックで実行できるため、不要なデバッグ時間を減らし、学習内容に集中できます。

図 0-5   コードブロックと対応するソースコードファイル

ローカルでコードを実行するだけでなく、Web 版では Python コードの可視化実行にも対応しています(pythontutor を利用)。以下の図のように、コードブロックの下にある“可視化実行”をクリックすると表示を展開し、アルゴリズムコードの実行過程を観察できます。また、“全画面表示”をクリックすると、より見やすい閲覧体験が得られます。

図 0-6   Python コードの可視化実行

","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#024","level":2,"title":"0.2.4   質問と議論を通じてともに成長する","text":"

本書を読んでいて、理解できていない知識点を安易に読み飛ばさないでください。コメント欄で気軽に質問してください。私と仲間たちが誠意をもって回答し、通常は 2 日以内に返信します。

以下の図のように、Web 版では各章の下部にコメント欄があります。ぜひコメント欄の内容にも目を通してください。一方では、みんなが直面した問題を知ることで知識の抜けを補い、より深い思考を促せます。もう一方では、ほかの仲間の質問にも積極的に答え、見解を共有し、互いの成長を助けてほしいと思います。

図 0-7   コメント欄の例

","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/suggestions/#025","level":2,"title":"0.2.5   アルゴリズム学習ロードマップ","text":"

全体として見ると、データ構造とアルゴリズムの学習過程は 3 つの段階に分けられます。

  1. 第 1 段階:アルゴリズム入門。さまざまなデータ構造の特徴と使い方に慣れ、異なるアルゴリズムの原理、流れ、用途、効率などを学ぶ必要があります。
  2. 第 2 段階:アルゴリズム問題を解く。まずは人気の高い問題から取り組み、少なくとも 100 問は蓄積して、主流のアルゴリズム問題に慣れることをおすすめします。最初のうちは、“知識の忘却”が課題になるかもしれませんが、心配はいりません。これはごく自然なことです。“エビングハウスの忘却曲線”に沿って問題を復習すれば、通常は 3~5 回繰り返すことでしっかり記憶に定着します。おすすめの問題リストと学習計画は、この GitHub リポジトリ を参照してください。
  3. 第 3 段階:知識体系を構築する。学習面では、アルゴリズムの連載記事、解法フレームワーク、教材などを読むことで、知識体系を継続的に充実させられます。問題演習の面では、トピック別分類、1 問多解、1 解多題といった発展的な戦略も試せます。関連する学習ノウハウは各コミュニティで見つけられます。

以下の図のように、本書の内容は主に“第 1 段階”を扱っており、第 2 段階と第 3 段階の学習をより効率的に進める助けとなることを目的としています。

図 0-8   アルゴリズム学習ロードマップ

","path":["第 0 章   前書き","0.2   本書の使い方"],"tags":[]},{"location":"chapter_preface/summary/","level":1,"title":"0.3   まとめ","text":"","path":["第 0 章   前書き","0.3   まとめ"],"tags":[]},{"location":"chapter_preface/summary/#1","level":3,"title":"1.   重要ポイントの振り返り","text":"
  • 本書の主な対象読者はアルゴリズム初学者です。すでにある程度の基礎がある場合でも、本書はアルゴリズム知識を体系的に振り返る助けとなり、書中のソースコードは「問題演習用ツール集」としても利用できます。
  • 本書の内容は主に計算量解析、データ構造、アルゴリズムの三部からなり、この分野の大部分のテーマを網羅しています。
  • アルゴリズム初心者にとって、学習初期の段階で入門書を読むことは非常に重要であり、多くの遠回りを避けられます。
  • 本書のアニメーション図解は通常、重要な知識や難しい知識を紹介するために用いられます。本書を読む際は、これらの内容により多く注意を払うべきです。
  • 実践はプログラミングを学ぶ最良の方法です。ソースコードを実行し、実際に自分でコードを書くことを強く勧めます。
  • 本書のWeb版の各章にはコメント欄が設けられており、疑問や見解をいつでも共有することを歓迎します。
","path":["第 0 章   前書き","0.3   まとめ"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"参考文献","text":"

[1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition).

[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).

[3] Robert Sedgewick, et al. Algorithms (4th Edition).

[4] 严蔚敏. データ構造(C 言語版).

[5] 邓俊辉. データ構造(C++ 言語版、第3版).

[6] マーク・アレン・ワイス著,陈越訳. データ構造とアルゴリズム解析:Java言語による記述(第3版).

[7] 程杰. データ構造の話.

[8] 王争. データ構造とアルゴリズムの美.

[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

[10] Aston Zhang, et al. Dive into Deep Learning.

","path":["参考文献"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"第 10 章   探索","text":"

Abstract

探索は未知の冒険であり、私たちは神秘的な空間の隅々まで歩き回る必要があるかもしれず、あるいは素早く目標を特定できるかもしれません。

この探索の旅において、すべての探求が思いもよらなかった答えをもたらすかもしれません。

","path":["第 10 章   探索"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"章の内容","text":"
  • 10.1   二分探索
  • 10.2   二分探索の挿入位置
  • 10.3   二分探索の境界
  • 10.4   ハッシュによる最適化戦略
  • 10.5   探索アルゴリズム再考
  • 10.6   まとめ
","path":["第 10 章   探索"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   二分探索","text":"

二分探索(binary search)は分割統治法に基づく効率的な探索アルゴリズムです。データが整列済みである性質を利用し、各ラウンドで探索範囲を半分に縮小し、目標要素を見つけるか探索区間が空になるまで続けます。

Question

長さ \\(n\\) の配列 nums が与えられます。要素は小さい順に並んでおり、重複しません。要素 target がこの配列内にある場合はそのインデックスを返し、含まれない場合は \\(-1\\) を返してください。例を次の図に示します。

図 10-1   二分探索の例

次の図に示すように、まずポインタ \\(i = 0\\) と \\(j = n - 1\\) を初期化し、それぞれ配列の先頭要素と末尾要素を指すようにして、探索区間 \\([0, n - 1]\\) を表します。角括弧は閉区間を表し、境界値自体を含むことに注意してください。

次に、以下の 2 つの手順を繰り返します。

  1. 中央のインデックス \\(m = \\lfloor {(i + j) / 2} \\rfloor\\) を計算します。ここで \\(\\lfloor \\: \\rfloor\\) は切り捨てを表します。
  2. nums[m]target の大小関係を判定し、次の 3 つの場合に分かれます。
    1. nums[m] < target のとき、target は区間 \\([m + 1, j]\\) にあるため、\\(i = m + 1\\) を実行します。
    2. nums[m] > target のとき、target は区間 \\([i, m - 1]\\) にあるため、\\(j = m - 1\\) を実行します。
    3. nums[m] = target のとき、target が見つかったので、インデックス \\(m\\) を返します。

配列に目標要素が含まれない場合、探索区間は最終的に空まで縮小されます。このとき \\(-1\\) を返します。

<1><2><3><4><5><6><7>

図 10-2   二分探索の流れ

注意すべき点として、\\(i\\) と \\(j\\) はどちらも int 型であるため、\\(i + j\\) が int 型の範囲を超える可能性があります。大きな数によるオーバーフローを避けるため、通常は式 \\(m = \\lfloor {i + (j - i) / 2} \\rfloor\\) を用いて中点を計算します。

コードは次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
def binary_search(nums: list[int], target: int) -> int:\n    \"\"\"二分探索(両閉区間)\"\"\"\n    # 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    i, j = 0, len(nums) - 1\n    # ループし、探索区間が空になったら終了する(i > j で空)\n    while i <= j:\n        # 理論上、Python の数値は無限に大きくできるため(メモリ容量に依存)、大きな数のオーバーフローを考慮する必要はない\n        m = (i + j) // 2  # 中点インデックス m を計算\n        if nums[m] < target:\n            i = m + 1  # この場合、target は区間 [m+1, j] にある\n        elif nums[m] > target:\n            j = m - 1  # この場合、target は区間 [i, m-1] にある\n        else:\n            return m  # 目標要素が見つかったらそのインデックスを返す\n    return -1  # 目標要素が見つからなければ -1 を返す\n
binary_search.cpp
/* 二分探索(両閉区間) */\nint binarySearch(vector<int> &nums, int target) {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    int i = 0, j = nums.size() - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target)    // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.java
/* 二分探索(両閉区間) */\nint binarySearch(int[] nums, int target) {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    int i = 0, j = nums.length - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.cs
/* 二分探索(両閉区間) */\nint BinarySearch(int[] nums, int target) {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    int i = 0, j = nums.Length - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        int m = i + (j - i) / 2;   // 中点インデックス m を計算\n        if (nums[m] < target)      // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        else                       // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.go
/* 二分探索(両閉区間) */\nfunc binarySearch(nums []int, target int) int {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    i, j := 0, len(nums)-1\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    for i <= j {\n        m := i + (j-i)/2      // 中点インデックス m を計算\n        if nums[m] < target { // この場合、target は区間 [m+1, j] にある\n            i = m + 1\n        } else if nums[m] > target { // この場合、target は区間 [i, m-1] にある\n            j = m - 1\n        } else { // 目標要素が見つかったらそのインデックスを返す\n            return m\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.swift
/* 二分探索(両閉区間) */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while i <= j {\n        let m = i + (j - i) / 2 // 中点インデックス m を計算\n        if nums[m] < target { // この場合、target は区間 [m+1, j] にある\n            i = m + 1\n        } else if nums[m] > target { // この場合、target は区間 [i, m-1] にある\n            j = m - 1\n        } else { // 目標要素が見つかったらそのインデックスを返す\n            return m\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.js
/* 二分探索(両閉区間) */\nfunction binarySearch(nums, target) {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    let i = 0,\n        j = nums.length - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        // 中点インデックス `m` を計算し、`parseInt()` で切り捨てる\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        else if (nums[m] > target)\n            // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        else return m; // 目標要素が見つかったらそのインデックスを返す\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.ts
/* 二分探索(両閉区間) */\nfunction binarySearch(nums: number[], target: number): number {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    let i = 0,\n        j = nums.length - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        // 中点インデックス m を計算\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        } else {\n            // 目標要素が見つかったらそのインデックスを返す\n            return m;\n        }\n    }\n    return -1; // 目標要素が見つからなければ -1 を返す\n}\n
binary_search.dart
/* 二分探索(両閉区間) */\nint binarySearch(List<int> nums, int target) {\n  // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n  int i = 0, j = nums.length - 1;\n  // ループし、探索区間が空になったら終了する(i > j で空)\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // 中点インデックス m を計算\n    if (nums[m] < target) {\n      // この場合、target は区間 [m+1, j] にある\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // この場合、target は区間 [i, m-1] にある\n      j = m - 1;\n    } else {\n      // 目標要素が見つかったらそのインデックスを返す\n      return m;\n    }\n  }\n  // 目標要素が見つからなければ -1 を返す\n  return -1;\n}\n
binary_search.rs
/* 二分探索(両閉区間) */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    let mut i = 0;\n    let mut j = nums.len() as i32 - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while i <= j {\n        let m = i + (j - i) / 2; // 中点インデックス m を計算\n        if nums[m as usize] < target {\n            // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        } else {\n            // 目標要素が見つかったらそのインデックスを返す\n            return m;\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.c
/* 二分探索(両閉区間) */\nint binarySearch(int *nums, int len, int target) {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    int i = 0, j = len - 1;\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target)    // この場合、target は区間 [m+1, j] にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある\n            j = m - 1;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.kt
/* 二分探索(両閉区間) */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    // 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n    var i = 0\n    var j = nums.size - 1\n    // ループし、探索区間が空になったら終了する(i > j で空)\n    while (i <= j) {\n        val m = i + (j - i) / 2 // 中点インデックス m を計算\n        if (nums[m] < target) // この場合、target は区間 [m+1, j] にある\n            i = m + 1\n        else if (nums[m] > target) // この場合、target は区間 [i, m-1] にある\n            j = m - 1\n        else  // 目標要素が見つかったらそのインデックスを返す\n            return m\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.rb
### 二分探索(両閉区間) ###\ndef binary_search(nums, target)\n  # 両閉区間 [0, n-1] を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素を指す\n  i, j = 0, nums.length - 1\n\n  # ループし、探索区間が空になったら終了する(i > j で空)\n  while i <= j\n    # 理論上、Ruby の数値は無限に大きくできるため(メモリ容量に依存)、大きな数のオーバーフローを考慮する必要はない\n    m = (i + j) / 2   # 中点インデックス m を計算\n\n    if nums[m] < target\n      i = m + 1 # この場合、target は区間 [m+1, j] にある\n    elsif nums[m] > target\n      j = m - 1 # この場合、target は区間 [i, m-1] にある\n    else\n      return m  # 目標要素が見つかったらそのインデックスを返す\n    end\n  end\n\n  -1  # 目標要素が見つからなければ -1 を返す\nend\n
コードの可視化

全画面で見る >

時間計算量は \\(O(\\log n)\\) :二分探索のループでは各ラウンドで区間が半分になるため、ループ回数は \\(\\log_2 n\\) です。

空間計算量は \\(O(1)\\) :ポインタ \\(i\\) と \\(j\\) に必要なのは定数サイズの空間だけです。

","path":["第 10 章   探索","10.1   二分探索"],"tags":[]},{"location":"chapter_searching/binary_search/#1011","level":2,"title":"10.1.1   区間の表し方","text":"

上記の両閉区間のほかに、一般的な区間表現として「左閉右開」区間があり、\\([0, n)\\) と定義されます。つまり左端は含み、右端は含みません。この表現では、区間 \\([i, j)\\) は \\(i = j\\) のとき空です。

この表現に基づいて、同じ機能を持つ二分探索アルゴリズムを実装できます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
def binary_search_lcro(nums: list[int], target: int) -> int:\n    \"\"\"二分探索(左閉右開区間)\"\"\"\n    # 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    i, j = 0, len(nums)\n    # ループし、探索区間が空になったら終了する(i = j で空)\n    while i < j:\n        m = (i + j) // 2  # 中点インデックス m を計算\n        if nums[m] < target:\n            i = m + 1  # この場合、target は区間 [m+1, j) にある\n        elif nums[m] > target:\n            j = m  # この場合、target は区間 [i, m) にある\n        else:\n            return m  # 目標要素が見つかったらそのインデックスを返す\n    return -1  # 目標要素が見つからなければ -1 を返す\n
binary_search.cpp
/* 二分探索(左閉右開区間) */\nint binarySearchLCRO(vector<int> &nums, int target) {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    int i = 0, j = nums.size();\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target)    // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m) にある\n            j = m;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.java
/* 二分探索(左閉右開区間) */\nint binarySearchLCRO(int[] nums, int target) {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    int i = 0, j = nums.length;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m) にある\n            j = m;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.cs
/* 二分探索(左閉右開区間) */\nint BinarySearchLCRO(int[] nums, int target) {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    int i = 0, j = nums.Length;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        int m = i + (j - i) / 2;   // 中点インデックス m を計算\n        if (nums[m] < target)      // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m) にある\n            j = m;\n        else                       // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.go
/* 二分探索(左閉右開区間) */\nfunc binarySearchLCRO(nums []int, target int) int {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    i, j := 0, len(nums)\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    for i < j {\n        m := i + (j-i)/2      // 中点インデックス m を計算\n        if nums[m] < target { // この場合、target は区間 [m+1, j) にある\n            i = m + 1\n        } else if nums[m] > target { // この場合、target は区間 [i, m) にある\n            j = m\n        } else { // 目標要素が見つかったらそのインデックスを返す\n            return m\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.swift
/* 二分探索(左閉右開区間) */\nfunc binarySearchLCRO(nums: [Int], target: Int) -> Int {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    var i = nums.startIndex\n    var j = nums.endIndex\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while i < j {\n        let m = i + (j - i) / 2 // 中点インデックス m を計算\n        if nums[m] < target { // この場合、target は区間 [m+1, j) にある\n            i = m + 1\n        } else if nums[m] > target { // この場合、target は区間 [i, m) にある\n            j = m\n        } else { // 目標要素が見つかったらそのインデックスを返す\n            return m\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.js
/* 二分探索(左閉右開区間) */\nfunction binarySearchLCRO(nums, target) {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    let i = 0,\n        j = nums.length;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        // 中点インデックス `m` を計算し、`parseInt()` で切り捨てる\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        else if (nums[m] > target)\n            // この場合、target は区間 [i, m) にある\n            j = m;\n        // 目標要素が見つかったらそのインデックスを返す\n        else return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.ts
/* 二分探索(左閉右開区間) */\nfunction binarySearchLCRO(nums: number[], target: number): number {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    let i = 0,\n        j = nums.length;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        // 中点インデックス m を計算\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // この場合、target は区間 [i, m) にある\n            j = m;\n        } else {\n            // 目標要素が見つかったらそのインデックスを返す\n            return m;\n        }\n    }\n    return -1; // 目標要素が見つからなければ -1 を返す\n}\n
binary_search.dart
/* 二分探索(左閉右開区間) */\nint binarySearchLCRO(List<int> nums, int target) {\n  // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n  int i = 0, j = nums.length;\n  // ループし、探索区間が空になったら終了する(i = j で空)\n  while (i < j) {\n    int m = i + (j - i) ~/ 2; // 中点インデックス m を計算\n    if (nums[m] < target) {\n      // この場合、target は区間 [m+1, j) にある\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // この場合、target は区間 [i, m) にある\n      j = m;\n    } else {\n      // 目標要素が見つかったらそのインデックスを返す\n      return m;\n    }\n  }\n  // 目標要素が見つからなければ -1 を返す\n  return -1;\n}\n
binary_search.rs
/* 二分探索(左閉右開区間) */\nfn binary_search_lcro(nums: &[i32], target: i32) -> i32 {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    let mut i = 0;\n    let mut j = nums.len() as i32;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while i < j {\n        let m = i + (j - i) / 2; // 中点インデックス m を計算\n        if nums[m as usize] < target {\n            // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // この場合、target は区間 [i, m) にある\n            j = m;\n        } else {\n            // 目標要素が見つかったらそのインデックスを返す\n            return m;\n        }\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.c
/* 二分探索(左閉右開区間) */\nint binarySearchLCRO(int *nums, int len, int target) {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    int i = 0, j = len;\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target)    // この場合、target は区間 [m+1, j) にある\n            i = m + 1;\n        else if (nums[m] > target) // この場合、target は区間 [i, m) にある\n            j = m;\n        else // 目標要素が見つかったらそのインデックスを返す\n            return m;\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1;\n}\n
binary_search.kt
/* 二分探索(左閉右開区間) */\nfun binarySearchLCRO(nums: IntArray, target: Int): Int {\n    // 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n    var i = 0\n    var j = nums.size\n    // ループし、探索区間が空になったら終了する(i = j で空)\n    while (i < j) {\n        val m = i + (j - i) / 2 // 中点インデックス m を計算\n        if (nums[m] < target) // この場合、target は区間 [m+1, j) にある\n            i = m + 1\n        else if (nums[m] > target) // この場合、target は区間 [i, m) にある\n            j = m\n        else  // 目標要素が見つかったらそのインデックスを返す\n            return m\n    }\n    // 目標要素が見つからなければ -1 を返す\n    return -1\n}\n
binary_search.rb
### 二分探索(左閉右開区間) ###\ndef binary_search_lcro(nums, target)\n  # 左閉右開区間 [0, n) を初期化する。つまり i, j はそれぞれ配列の先頭要素と末尾要素+1を指す\n  i, j = 0, nums.length\n\n  # ループし、探索区間が空になったら終了する(i = j で空)\n  while i < j\n    # 中点インデックス m を計算\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # この場合、target は区間 [m+1, j) にある\n    elsif nums[m] > target\n      j = m - 1 # この場合、target は区間 [i, m) にある\n    else\n      return m  # 目標要素が見つかったらそのインデックスを返す\n    end\n  end\n\n  -1  # 目標要素が見つからなければ -1 を返す\nend\n
コードの可視化

全画面で見る >

次の図に示すように、2 種類の区間表現では、二分探索アルゴリズムの初期化、ループ条件、区間の縮小操作がそれぞれ異なります。

「両閉区間」の表現では左右の境界がどちらも閉区間として定義されるため、ポインタ \\(i\\) とポインタ \\(j\\) による区間縮小の操作も対称になります。このほうがミスをしにくいため、一般には「両閉区間」の書き方を推奨します。

図 10-3   2 種類の区間定義

","path":["第 10 章   探索","10.1   二分探索"],"tags":[]},{"location":"chapter_searching/binary_search/#1012","level":2,"title":"10.1.2   利点と限界","text":"

二分探索は時間と空間の両面で優れた性能を持ちます。

  • 二分探索は時間効率が高いです。データ量が大きい場合、対数時間計算量は大きな優位性を持ちます。たとえば、データサイズ \\(n = 2^{20}\\) のとき、線形探索では \\(2^{20} = 1048576\\) 回のループが必要ですが、二分探索では \\(\\log_2 2^{20} = 20\\) 回で済みます。
  • 二分探索は追加の空間を必要としません。追加領域を要する探索アルゴリズム(たとえばハッシュ探索)と比べて、二分探索はより省メモリです。

しかし、二分探索があらゆる状況に適しているわけではなく、主な理由は次のとおりです。

  • 二分探索は整列済みデータにしか適用できません。入力データが無秩序な場合、二分探索を使うためだけにソートするのは割に合いません。ソートアルゴリズムの時間計算量は通常 \\(O(n \\log n)\\) であり、線形探索や二分探索よりも高いからです。要素を頻繁に挿入する場面では、配列の整列性を保つために特定位置へ挿入する必要があり、その時間計算量は \\(O(n)\\) と高コストです。
  • 二分探索は配列にしか適していません。二分探索では要素へ飛び飛びにアクセスする必要がありますが、連結リストでそのようなアクセスを行う効率は低いため、連結リストやそれを基に実装されたデータ構造には向きません。
  • データ量が小さい場合は線形探索のほうが高性能です。線形探索では各ラウンドで 1 回の比較だけで済みますが、二分探索では 1 回の加算、1 回の除算、1 ~ 3 回の比較、1 回の加算(減算)が必要で、合計 4 ~ 6 個の基本操作になります。したがって、データ量 \\(n\\) が小さいときは、線形探索のほうがかえって速くなります。
","path":["第 10 章   探索","10.1   二分探索"],"tags":[]},{"location":"chapter_searching/binary_search_edge/","level":1,"title":"10.3   二分探索の境界","text":"","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1031","level":2,"title":"10.3.1   左端境界を探す","text":"

Question

長さ \\(n\\) のソート済み配列 nums が与えられ、その中には重複要素が含まれる可能性があります。配列内で最も左にある要素 target のインデックスを返してください。配列にこの要素が含まれない場合は、\\(-1\\) を返します。

二分探索で挿入位置を求める方法を思い出すと、探索完了後に \\(i\\) は最も左にある target を指します。したがって、挿入位置を探すことの本質は、最も左にある target のインデックスを探すことです。

挿入位置を探す関数を使って左端境界を求めることを考えます。なお、配列に target が含まれない場合があり、そのときは次の 2 つの結果が起こりえます。

  • 挿入位置のインデックス \\(i\\) が範囲外になる。
  • 要素 nums[i]target と等しくない。

上の 2 つの状況に当てはまる場合は、直接 \\(-1\\) を返せば十分です。コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
def binary_search_left_edge(nums: list[int], target: int) -> int:\n    \"\"\"最も左の target を二分探索\"\"\"\n    # target の挿入位置を探すのと等価\n    i = binary_search_insertion(nums, target)\n    # target が見つからなければ、-1 を返す\n    if i == len(nums) or nums[i] != target:\n        return -1\n    # target が見つかったら、インデックス i を返す\n    return i\n
binary_search_edge.cpp
/* 最も左の target を二分探索 */\nint binarySearchLeftEdge(vector<int> &nums, int target) {\n    // target の挿入位置を探すのと等価\n    int i = binarySearchInsertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if (i == nums.size() || nums[i] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.java
/* 最も左の target を二分探索 */\nint binarySearchLeftEdge(int[] nums, int target) {\n    // target の挿入位置を探すのと等価\n    int i = binary_search_insertion.binarySearchInsertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if (i == nums.length || nums[i] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.cs
/* 最も左の target を二分探索 */\nint BinarySearchLeftEdge(int[] nums, int target) {\n    // target の挿入位置を探すのと等価\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if (i == nums.Length || nums[i] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.go
/* 最も左の target を二分探索 */\nfunc binarySearchLeftEdge(nums []int, target int) int {\n    // target の挿入位置を探すのと等価\n    i := binarySearchInsertion(nums, target)\n    // target が見つからなければ、-1 を返す\n    if i == len(nums) || nums[i] != target {\n        return -1\n    }\n    // target が見つかったら、インデックス i を返す\n    return i\n}\n
binary_search_edge.swift
/* 最も左の target を二分探索 */\nfunc binarySearchLeftEdge(nums: [Int], target: Int) -> Int {\n    // target の挿入位置を探すのと等価\n    let i = binarySearchInsertion(nums: nums, target: target)\n    // target が見つからなければ、-1 を返す\n    if i == nums.endIndex || nums[i] != target {\n        return -1\n    }\n    // target が見つかったら、インデックス i を返す\n    return i\n}\n
binary_search_edge.js
/* 最も左の target を二分探索 */\nfunction binarySearchLeftEdge(nums, target) {\n    // target の挿入位置を探すのと等価\n    const i = binarySearchInsertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.ts
/* 最も左の target を二分探索 */\nfunction binarySearchLeftEdge(nums: Array<number>, target: number): number {\n    // target の挿入位置を探すのと等価\n    const i = binarySearchInsertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.dart
/* 最も左の target を二分探索 */\nint binarySearchLeftEdge(List<int> nums, int target) {\n  // target の挿入位置を探すのと等価\n  int i = binarySearchInsertion(nums, target);\n  // target が見つからなければ、-1 を返す\n  if (i == nums.length || nums[i] != target) {\n    return -1;\n  }\n  // target が見つかったら、インデックス i を返す\n  return i;\n}\n
binary_search_edge.rs
/* 最も左の target を二分探索 */\nfn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {\n    // target の挿入位置を探すのと等価\n    let i = binary_search_insertion(nums, target);\n    // target が見つからなければ、-1 を返す\n    if i == nums.len() as i32 || nums[i as usize] != target {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    i\n}\n
binary_search_edge.c
/* 最も左の target を二分探索 */\nint binarySearchLeftEdge(int *nums, int numSize, int target) {\n    // target の挿入位置を探すのと等価\n    int i = binarySearchInsertion(nums, numSize, target);\n    // target が見つからなければ、-1 を返す\n    if (i == numSize || nums[i] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス i を返す\n    return i;\n}\n
binary_search_edge.kt
/* 最も左の target を二分探索 */\nfun binarySearchLeftEdge(nums: IntArray, target: Int): Int {\n    // target の挿入位置を探すのと等価\n    val i = binarySearchInsertion(nums, target)\n    // target が見つからなければ、-1 を返す\n    if (i == nums.size || nums[i] != target) {\n        return -1\n    }\n    // target が見つかったら、インデックス i を返す\n    return i\n}\n
binary_search_edge.rb
### target の最左位置を二分探索 ###\ndef binary_search_left_edge(nums, target)\n  # target の挿入位置を探すのと等価\n  i = binary_search_insertion(nums, target)\n\n  # target が見つからなければ、-1 を返す\n  return -1 if i == nums.length || nums[i] != target\n\n  i # target が見つかったら、インデックス i を返す\nend\n
コードの可視化

全画面で見る >

","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1032","level":2,"title":"10.3.2   右端境界を探す","text":"

では、最も右にある target はどのように探せるでしょうか。最も直接的な方法はコードを修正し、nums[m] == target の場合のポインタの縮小操作を置き換えることです。ここではコードを省略するので、興味があれば自分で実装してみてください。

ここでは、より巧妙な 2 つの方法を紹介します。

","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1","level":3,"title":"1.   左端境界探索を再利用する","text":"

実際には、最も左の要素を探す関数を利用して最も右の要素を探せます。具体的には、最も右にある target を探すことを、最も左にある target + 1 を探すことに変換します。

下図のように、探索完了後、ポインタ \\(i\\) は最も左にある target + 1(存在する場合)を指し、\\(j\\) は最も右にある target を指します。したがって \\(j\\) を返せばよいです。

図 10-7   右端境界の探索を左端境界の探索に変換する

返される挿入位置は \\(i\\) なので、そこから \\(1\\) を引いて \\(j\\) を得る必要があることに注意してください:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
def binary_search_right_edge(nums: list[int], target: int) -> int:\n    \"\"\"最も右の target を二分探索\"\"\"\n    # 最左の target + 1 を探す問題に変換する\n    i = binary_search_insertion(nums, target + 1)\n    # j は最も右の target を指し、i は target より大きい最初の要素を指す\n    j = i - 1\n    # target が見つからなければ、-1 を返す\n    if j == -1 or nums[j] != target:\n        return -1\n    # target が見つかったら、インデックス j を返す\n    return j\n
binary_search_edge.cpp
/* 最も右の target を二分探索 */\nint binarySearchRightEdge(vector<int> &nums, int target) {\n    // 最左の target + 1 を探す問題に変換する\n    int i = binarySearchInsertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    int j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.java
/* 最も右の target を二分探索 */\nint binarySearchRightEdge(int[] nums, int target) {\n    // 最左の target + 1 を探す問題に変換する\n    int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    int j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.cs
/* 最も右の target を二分探索 */\nint BinarySearchRightEdge(int[] nums, int target) {\n    // 最左の target + 1 を探す問題に変換する\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    int j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.go
/* 最も右の target を二分探索 */\nfunc binarySearchRightEdge(nums []int, target int) int {\n    // 最左の target + 1 を探す問題に変換する\n    i := binarySearchInsertion(nums, target+1)\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    j := i - 1\n    // target が見つからなければ、-1 を返す\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // target が見つかったら、インデックス j を返す\n    return j\n}\n
binary_search_edge.swift
/* 最も右の target を二分探索 */\nfunc binarySearchRightEdge(nums: [Int], target: Int) -> Int {\n    // 最左の target + 1 を探す問題に変換する\n    let i = binarySearchInsertion(nums: nums, target: target + 1)\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    let j = i - 1\n    // target が見つからなければ、-1 を返す\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // target が見つかったら、インデックス j を返す\n    return j\n}\n
binary_search_edge.js
/* 最も右の target を二分探索 */\nfunction binarySearchRightEdge(nums, target) {\n    // 最左の target + 1 を探す問題に変換する\n    const i = binarySearchInsertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    const j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.ts
/* 最も右の target を二分探索 */\nfunction binarySearchRightEdge(nums: Array<number>, target: number): number {\n    // 最左の target + 1 を探す問題に変換する\n    const i = binarySearchInsertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    const j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.dart
/* 最も右の target を二分探索 */\nint binarySearchRightEdge(List<int> nums, int target) {\n  // 最左の target + 1 を探す問題に変換する\n  int i = binarySearchInsertion(nums, target + 1);\n  // j は最も右の target を指し、i は target より大きい最初の要素を指す\n  int j = i - 1;\n  // target が見つからなければ、-1 を返す\n  if (j == -1 || nums[j] != target) {\n    return -1;\n  }\n  // target が見つかったら、インデックス j を返す\n  return j;\n}\n
binary_search_edge.rs
/* 最も右の target を二分探索 */\nfn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {\n    // 最左の target + 1 を探す問題に変換する\n    let i = binary_search_insertion(nums, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    let j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if j == -1 || nums[j as usize] != target {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    j\n}\n
binary_search_edge.c
/* 最も右の target を二分探索 */\nint binarySearchRightEdge(int *nums, int numSize, int target) {\n    // 最左の target + 1 を探す問題に変換する\n    int i = binarySearchInsertion(nums, numSize, target + 1);\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    int j = i - 1;\n    // target が見つからなければ、-1 を返す\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // target が見つかったら、インデックス j を返す\n    return j;\n}\n
binary_search_edge.kt
/* 最も右の target を二分探索 */\nfun binarySearchRightEdge(nums: IntArray, target: Int): Int {\n    // 最左の target + 1 を探す問題に変換する\n    val i = binarySearchInsertion(nums, target + 1)\n    // j は最も右の target を指し、i は target より大きい最初の要素を指す\n    val j = i - 1\n    // target が見つからなければ、-1 を返す\n    if (j == -1 || nums[j] != target) {\n        return -1\n    }\n    // target が見つかったら、インデックス j を返す\n    return j\n}\n
binary_search_edge.rb
### target の最右位置を二分探索 ###\ndef binary_search_right_edge(nums, target)\n  # 最左の target + 1 を探す問題に変換する\n  i = binary_search_insertion(nums, target + 1)\n\n  # j は最も右の target を指し、i は target より大きい最初の要素を指す\n  j = i - 1\n\n  # target が見つからなければ、-1 を返す\n  return -1 if j == -1 || nums[j] != target\n\n  j # target が見つかったら、インデックス j を返す\nend\n
コードの可視化

全画面で見る >

","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#2","level":3,"title":"2.   要素探索に変換する","text":"

配列に target が含まれない場合、最終的に \\(i\\) と \\(j\\) はそれぞれ target より大きい最初の要素と、target より小さい最初の要素を指すことになります。

したがって、下図のように、配列中に存在しない要素を構成して、それを使って左右の境界を探せます。

  • 最も左にある target の探索:target - 0.5 を探すことに変換でき、ポインタ \\(i\\) を返します。
  • 最も右にある target の探索:target + 0.5 を探すことに変換でき、ポインタ \\(j\\) を返します。

図 10-8   境界の探索を要素の探索に変換する

ここではコードを省略しますが、次の 2 点に注意が必要です。

  • 与えられた配列には小数が含まれないため、等しい場合をどう処理するかを気にする必要はありません。
  • この方法では小数を導入するため、関数内の変数 target を浮動小数点数型に変更する必要があります(Python は変更不要です)。
","path":["第 10 章   探索","10.3   二分探索の境界"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/","level":1,"title":"10.2   二分探索の挿入位置","text":"

二分探索は目標要素の検索だけでなく、目標要素の挿入位置を探すなど、多くの派生問題の解決にも利用できます。

","path":["第 10 章   探索","10.2   二分探索の挿入位置"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1021","level":2,"title":"10.2.1   重複要素がない場合","text":"

Question

長さ \\(n\\) の整列済み配列 nums と要素 target が与えられます。配列には重複要素は存在しません。ここで target を配列 nums に挿入し、その順序を保ちます。配列中にすでに要素 target が存在する場合は、その左側に挿入します。挿入後の配列における target のインデックスを返してください。例を以下の図に示します。

図 10-4   二分探索の挿入位置の例データ

前節の二分探索コードを再利用したい場合は、次の二つの問題に答える必要があります。

問題 1:配列に target が含まれる場合、挿入位置のインデックスはその要素のインデックスですか?

問題では target を等しい要素の左側に挿入するよう求めているため、新しく挿入された target は元の target の位置に入ります。つまり、配列に target が含まれる場合、挿入位置のインデックスはその target のインデックスです。

問題 2:配列に target が存在しない場合、挿入位置はどの要素のインデックスですか?

二分探索の過程をさらに考えると、nums[m] < target のときは \\(i\\) が移動します。これは、ポインタ \\(i\\) が target 以上の要素へ近づいていることを意味します。同様に、ポインタ \\(j\\) は常に target 以下の要素へ近づいています。

したがって二分探索の終了時には、\\(i\\) は最初の target より大きい要素を指し、\\(j\\) は最初の target より小さい要素を指します。よって、配列に target が含まれない場合、挿入インデックスは \\(i\\) です。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
def binary_search_insertion_simple(nums: list[int], target: int) -> int:\n    \"\"\"二分探索で挿入位置を探す(重複要素なし)\"\"\"\n    i, j = 0, len(nums) - 1  # 両閉区間 [0, n-1] を初期化\n    while i <= j:\n        m = (i + j) // 2  # 中点インデックス m を計算\n        if nums[m] < target:\n            i = m + 1  # target は区間 [m+1, j] にある\n        elif nums[m] > target:\n            j = m - 1  # target は区間 [i, m-1] にある\n        else:\n            return m  # target が見つかったら、挿入位置 m を返す\n    # target が見つからなければ、挿入位置 i を返す\n    return i\n
binary_search_insertion.cpp
/* 二分探索で挿入位置を探す(重複要素なし) */\nint binarySearchInsertionSimple(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.java
/* 二分探索で挿入位置を探す(重複要素なし) */\nint binarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.cs
/* 二分探索で挿入位置を探す(重複要素なし) */\nint BinarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.go
/* 二分探索で挿入位置を探す(重複要素なし) */\nfunc binarySearchInsertionSimple(nums []int, target int) int {\n    // 両閉区間 [0, n-1] を初期化\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // 中点インデックス m を計算\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target は区間 [m+1, j] にある\n            i = m + 1\n        } else if nums[m] > target {\n            // target は区間 [i, m-1] にある\n            j = m - 1\n        } else {\n            // target が見つかったら、挿入位置 m を返す\n            return m\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.swift
/* 二分探索で挿入位置を探す(重複要素なし) */\nfunc binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {\n    // 両閉区間 [0, n-1] を初期化\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // 中点インデックス m を計算\n        if nums[m] < target {\n            i = m + 1 // target は区間 [m+1, j] にある\n        } else if nums[m] > target {\n            j = m - 1 // target は区間 [i, m-1] にある\n        } else {\n            return m // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.js
/* 二分探索で挿入位置を探す(重複要素なし) */\nfunction binarySearchInsertionSimple(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.ts
/* 二分探索で挿入位置を探す(重複要素なし) */\nfunction binarySearchInsertionSimple(\n    nums: Array<number>,\n    target: number\n): number {\n    let i = 0,\n        j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.dart
/* 二分探索で挿入位置を探す(重複要素なし) */\nint binarySearchInsertionSimple(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // 中点インデックス m を計算\n    if (nums[m] < target) {\n      i = m + 1; // target は区間 [m+1, j] にある\n    } else if (nums[m] > target) {\n      j = m - 1; // target は区間 [i, m-1] にある\n    } else {\n      return m; // target が見つかったら、挿入位置 m を返す\n    }\n  }\n  // target が見つからなければ、挿入位置 i を返す\n  return i;\n}\n
binary_search_insertion.rs
/* 二分探索で挿入位置を探す(重複要素なし) */\nfn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // 両閉区間 [0, n-1] を初期化\n    while i <= j {\n        let m = i + (j - i) / 2; // 中点インデックス m を計算\n        if nums[m as usize] < target {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if nums[m as usize] > target {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m;\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    i\n}\n
binary_search_insertion.c
/* 二分探索で挿入位置を探す(重複要素なし) */\nint binarySearchInsertionSimple(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            return m; // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.kt
/* 二分探索で挿入位置を探す(重複要素なし) */\nfun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        val m = i + (j - i) / 2 // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1 // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1 // target は区間 [i, m-1] にある\n        } else {\n            return m // target が見つかったら、挿入位置 m を返す\n        }\n    }\n    // target が見つからなければ、挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.rb
### 二分探索の挿入位置(重複要素なし) ###\ndef binary_search_insertion_simple(nums, target)\n  # 両閉区間 [0, n-1] を初期化\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # 中点インデックス m を計算\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target は区間 [m+1, j] にある\n    elsif nums[m] > target\n      j = m - 1 # target は区間 [i, m-1] にある\n    else\n      return m  # target が見つかったら、挿入位置 m を返す\n    end\n  end\n\n  i # target が見つからなければ、挿入位置 i を返す\nend\n
コードの可視化

全画面で見る >

","path":["第 10 章   探索","10.2   二分探索の挿入位置"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1022","level":2,"title":"10.2.2   重複要素がある場合","text":"

Question

前問を踏まえ、配列には重複要素が含まれる可能性があるものとし、それ以外の条件は変わりません。

配列中に複数の target が存在する場合、通常の二分探索ではそのうち一つの target のインデックスしか返せず、その要素の左側と右側にあといくつ target があるかは分かりません。

問題では目標要素を最も左に挿入する必要があるため、配列中で最も左にある target のインデックスを探す必要があります。まずは以下の図に示す手順で実現することを考えます。

  1. 二分探索を実行し、任意の target のインデックスを得て、これを \\(k\\) とします。
  2. インデックス \\(k\\) から始めて左へ線形探索し、最も左の target を見つけたら返します。

図 10-5   線形探索による重複要素の挿入位置

この方法は使用できますが、線形探索を含むため、時間計算量は \\(O(n)\\) です。配列中に重複した target が多い場合、この方法の効率は低くなります。

次に、二分探索のコードを拡張することを考えます。以下の図に示すように、全体の流れは変えず、各反復でまず中点インデックス \\(m\\) を計算し、その後 targetnums[m] の大小関係を判定して、次のいくつかの状況に分けます。

  • nums[m] < target または nums[m] > target のときは、まだ target を見つけていないことを意味するため、通常の二分探索と同じ区間縮小を行い、ポインタ \\(i\\) と \\(j\\) を target に近づけます。
  • nums[m] == target のときは、target より小さい要素が区間 \\([i, m - 1]\\) にあることを意味するため、\\(j = m - 1\\) として区間を縮小し、ポインタ \\(j\\) を target より小さい要素に近づけます。

ループ終了後、\\(i\\) は最も左の target を指し、\\(j\\) は最初の target より小さい要素を指すため、インデックス \\(i\\) が挿入位置です。

<1><2><3><4><5><6><7><8>

図 10-6   重複要素に対する二分探索の挿入位置の手順

以下のコードを観察すると、分岐 nums[m] > targetnums[m] == target の処理は同じであるため、両者はまとめることができます。

それでも、判定条件を分けたままにしておくことは可能であり、そのほうがロジックがより明確で、可読性も高くなります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
def binary_search_insertion(nums: list[int], target: int) -> int:\n    \"\"\"二分探索で挿入位置を探す(重複要素あり)\"\"\"\n    i, j = 0, len(nums) - 1  # 両閉区間 [0, n-1] を初期化\n    while i <= j:\n        m = (i + j) // 2  # 中点インデックス m を計算\n        if nums[m] < target:\n            i = m + 1  # target は区間 [m+1, j] にある\n        elif nums[m] > target:\n            j = m - 1  # target は区間 [i, m-1] にある\n        else:\n            j = m - 1  # target より小さい最初の要素は区間 [i, m-1] にある\n    # 挿入位置 i を返す\n    return i\n
binary_search_insertion.cpp
/* 二分探索で挿入位置を探す(重複要素あり) */\nint binarySearchInsertion(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.java
/* 二分探索で挿入位置を探す(重複要素あり) */\nint binarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.cs
/* 二分探索で挿入位置を探す(重複要素あり) */\nint BinarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.go
/* 二分探索で挿入位置を探す(重複要素あり) */\nfunc binarySearchInsertion(nums []int, target int) int {\n    // 両閉区間 [0, n-1] を初期化\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // 中点インデックス m を計算\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target は区間 [m+1, j] にある\n            i = m + 1\n        } else if nums[m] > target {\n            // target は区間 [i, m-1] にある\n            j = m - 1\n        } else {\n            // target より小さい最初の要素は区間 [i, m-1] にある\n            j = m - 1\n        }\n    }\n    // 挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.swift
/* 二分探索で挿入位置を探す(重複要素あり) */\nfunc binarySearchInsertion(nums: [Int], target: Int) -> Int {\n    // 両閉区間 [0, n-1] を初期化\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // 中点インデックス m を計算\n        if nums[m] < target {\n            i = m + 1 // target は区間 [m+1, j] にある\n        } else if nums[m] > target {\n            j = m - 1 // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1 // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.js
/* 二分探索で挿入位置を探す(重複要素あり) */\nfunction binarySearchInsertion(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.ts
/* 二分探索で挿入位置を探す(重複要素あり) */\nfunction binarySearchInsertion(nums: Array<number>, target: number): number {\n    let i = 0,\n        j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 中点インデックス m を計算し、Math.floor() で切り捨てる\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.dart
/* 二分探索で挿入位置を探す(重複要素あり) */\nint binarySearchInsertion(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // 両閉区間 [0, n-1] を初期化\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // 中点インデックス m を計算\n    if (nums[m] < target) {\n      i = m + 1; // target は区間 [m+1, j] にある\n    } else if (nums[m] > target) {\n      j = m - 1; // target は区間 [i, m-1] にある\n    } else {\n      j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n    }\n  }\n  // 挿入位置 i を返す\n  return i;\n}\n
binary_search_insertion.rs
/* 二分探索で挿入位置を探す(重複要素あり) */\npub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // 両閉区間 [0, n-1] を初期化\n    while i <= j {\n        let m = i + (j - i) / 2; // 中点インデックス m を計算\n        if nums[m as usize] < target {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if nums[m as usize] > target {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    i\n}\n
binary_search_insertion.c
/* 二分探索で挿入位置を探す(重複要素あり) */\nint binarySearchInsertion(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1; // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1; // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1; // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i;\n}\n
binary_search_insertion.kt
/* 二分探索で挿入位置を探す(重複要素あり) */\nfun binarySearchInsertion(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // 両閉区間 [0, n-1] を初期化\n    while (i <= j) {\n        val m = i + (j - i) / 2 // 中点インデックス m を計算\n        if (nums[m] < target) {\n            i = m + 1 // target は区間 [m+1, j] にある\n        } else if (nums[m] > target) {\n            j = m - 1 // target は区間 [i, m-1] にある\n        } else {\n            j = m - 1 // target より小さい最初の要素は区間 [i, m-1] にある\n        }\n    }\n    // 挿入位置 i を返す\n    return i\n}\n
binary_search_insertion.rb
### 二分探索の挿入位置(重複要素あり) ###\ndef binary_search_insertion(nums, target)\n  # 両閉区間 [0, n-1] を初期化\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # 中点インデックス m を計算\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target は区間 [m+1, j] にある\n    elsif nums[m] > target\n      j = m - 1 # target は区間 [i, m-1] にある\n    else\n      j = m - 1 # target より小さい最初の要素は区間 [i, m-1] にある\n    end\n  end\n\n  i # 挿入位置 i を返す\nend\n
コードの可視化

全画面で見る >

Tip

本節のコードはすべて「両閉区間」の書き方です。興味のある読者は「左閉右開」の書き方を自分で実装してみてください。

要するに、二分探索とはポインタ \\(i\\) と \\(j\\) にそれぞれ探索目標を設定することにほかなりません。目標は具体的な要素(たとえば target)である場合もあれば、要素の範囲(たとえば target より小さい要素)である場合もあります。

繰り返される二分のループの中で、ポインタ \\(i\\) と \\(j\\) はどちらも事前に定めた目標へ徐々に近づいていきます。最終的に、それらは答えを見つけるか、境界を越えたところで停止します。

","path":["第 10 章   探索","10.2   二分探索の挿入位置"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/","level":1,"title":"10.4   ハッシュによる最適化戦略","text":"

アルゴリズムの問題では,線形探索をハッシュ探索に置き換えることでアルゴリズムの時間計算量を下げることがよくあります。ここでは,あるアルゴリズム問題を通じて理解を深めましょう。

Question

整数配列 nums と目標要素 target が与えられたとき,配列内から和が target となる 2 つの要素を探索し,それらの配列インデックスを返してください。任意の 1 つの解を返せば十分です。

","path":["第 10 章   探索","10.4   ハッシュによる最適化戦略"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041","level":2,"title":"10.4.1   線形探索:時間と引き換えに空間を節約","text":"

考えられるすべての組み合わせを直接走査することを考えます。次の図に示すように,2 重ループを開始し,各ラウンドで 2 つの整数の和が target であるかを判定します。そうであれば,それらのインデックスを返します。

図 10-9   線形探索で 2 数の和を求める

コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n    \"\"\"方法 1:総当たり列挙\"\"\"\n    # 2重ループのため、時間計算量は O(n^2)\n    for i in range(len(nums) - 1):\n        for j in range(i + 1, len(nums)):\n            if nums[i] + nums[j] == target:\n                return [i, j]\n    return []\n
two_sum.cpp
/* 方法 1:総当たり列挙 */\nvector<int> twoSumBruteForce(vector<int> &nums, int target) {\n    int size = nums.size();\n    // 2重ループのため、時間計算量は O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return {i, j};\n        }\n    }\n    return {};\n}\n
two_sum.java
/* 方法 1:総当たり列挙 */\nint[] twoSumBruteForce(int[] nums, int target) {\n    int size = nums.length;\n    // 2重ループのため、時間計算量は O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return new int[] { i, j };\n        }\n    }\n    return new int[0];\n}\n
two_sum.cs
/* 方法 1:総当たり列挙 */\nint[] TwoSumBruteForce(int[] nums, int target) {\n    int size = nums.Length;\n    // 2重ループのため、時間計算量は O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return [i, j];\n        }\n    }\n    return [];\n}\n
two_sum.go
/* 方法 1:総当たり列挙 */\nfunc twoSumBruteForce(nums []int, target int) []int {\n    size := len(nums)\n    // 2重ループのため、時間計算量は O(n^2)\n    for i := 0; i < size-1; i++ {\n        for j := i + 1; j < size; j++ {\n            if nums[i]+nums[j] == target {\n                return []int{i, j}\n            }\n        }\n    }\n    return nil\n}\n
two_sum.swift
/* 方法 1:総当たり列挙 */\nfunc twoSumBruteForce(nums: [Int], target: Int) -> [Int] {\n    // 2重ループのため、時間計算量は O(n^2)\n    for i in nums.indices.dropLast() {\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[i] + nums[j] == target {\n                return [i, j]\n            }\n        }\n    }\n    return [0]\n}\n
two_sum.js
/* 方法 1:総当たり列挙 */\nfunction twoSumBruteForce(nums, target) {\n    const n = nums.length;\n    // 2重ループのため、時間計算量は O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
two_sum.ts
/* 方法 1:総当たり列挙 */\nfunction twoSumBruteForce(nums: number[], target: number): number[] {\n    const n = nums.length;\n    // 2重ループのため、時間計算量は O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
two_sum.dart
/* 方法1: 総当たり列挙 */\nList<int> twoSumBruteForce(List<int> nums, int target) {\n  int size = nums.length;\n  // 2重ループのため、時間計算量は O(n^2)\n  for (var i = 0; i < size - 1; i++) {\n    for (var j = i + 1; j < size; j++) {\n      if (nums[i] + nums[j] == target) return [i, j];\n    }\n  }\n  return [0];\n}\n
two_sum.rs
/* 方法 1:総当たり列挙 */\npub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    let size = nums.len();\n    // 2重ループのため、時間計算量は O(n^2)\n    for i in 0..size - 1 {\n        for j in i + 1..size {\n            if nums[i] + nums[j] == target {\n                return Some(vec![i as i32, j as i32]);\n            }\n        }\n    }\n    None\n}\n
two_sum.c
/* 方法 1:総当たり列挙 */\nint *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {\n    for (int i = 0; i < numsSize; ++i) {\n        for (int j = i + 1; j < numsSize; ++j) {\n            if (nums[i] + nums[j] == target) {\n                int *res = malloc(sizeof(int) * 2);\n                res[0] = i, res[1] = j;\n                *returnSize = 2;\n                return res;\n            }\n        }\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
two_sum.kt
/* 方法 1:総当たり列挙 */\nfun twoSumBruteForce(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // 2重ループのため、時間計算量は O(n^2)\n    for (i in 0..<size - 1) {\n        for (j in i + 1..<size) {\n            if (nums[i] + nums[j] == target) return intArrayOf(i, j)\n        }\n    }\n    return IntArray(0)\n}\n
two_sum.rb
### 方法1:総当たり列挙 ###\ndef two_sum_brute_force(nums, target)\n  # 2重ループのため、時間計算量は O(n^2)\n  for i in 0...(nums.length - 1)\n    for j in (i + 1)...nums.length\n      return [i, j] if nums[i] + nums[j] == target\n    end\n  end\n\n  []\nend\n
コードの可視化

全画面で見る >

この方法の時間計算量は \\(O(n^2)\\) ,空間計算量は \\(O(1)\\) であり,大規模データでは非常に時間がかかります。

","path":["第 10 章   探索","10.4   ハッシュによる最適化戦略"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1042","level":2,"title":"10.4.2   ハッシュ探索:空間と引き換えに時間を節約","text":"

ハッシュテーブルを利用し,キーと値をそれぞれ配列要素と要素のインデックスにします。配列をループで走査し,各ラウンドで次の図に示す手順を実行します。

  1. 数値 target - nums[i] がハッシュテーブル内にあるかを判定します。あれば,この 2 つの要素のインデックスを直接返します。
  2. キーと値の組 nums[i] とインデックス i をハッシュテーブルに追加します。
<1><2><3>

図 10-10   補助ハッシュテーブルで 2 数の和を求める

実装コードは次のとおりで,単一ループだけで済みます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
def two_sum_hash_table(nums: list[int], target: int) -> list[int]:\n    \"\"\"方法 2:補助ハッシュテーブル\"\"\"\n    # 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    dic = {}\n    # 単一ループで、時間計算量は O(n)\n    for i in range(len(nums)):\n        if target - nums[i] in dic:\n            return [dic[target - nums[i]], i]\n        dic[nums[i]] = i\n    return []\n
two_sum.cpp
/* 方法 2:補助ハッシュテーブル */\nvector<int> twoSumHashTable(vector<int> &nums, int target) {\n    int size = nums.size();\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    unordered_map<int, int> dic;\n    // 単一ループで、時間計算量は O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.find(target - nums[i]) != dic.end()) {\n            return {dic[target - nums[i]], i};\n        }\n        dic.emplace(nums[i], i);\n    }\n    return {};\n}\n
two_sum.java
/* 方法 2:補助ハッシュテーブル */\nint[] twoSumHashTable(int[] nums, int target) {\n    int size = nums.length;\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    Map<Integer, Integer> dic = new HashMap<>();\n    // 単一ループで、時間計算量は O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.containsKey(target - nums[i])) {\n            return new int[] { dic.get(target - nums[i]), i };\n        }\n        dic.put(nums[i], i);\n    }\n    return new int[0];\n}\n
two_sum.cs
/* 方法 2:補助ハッシュテーブル */\nint[] TwoSumHashTable(int[] nums, int target) {\n    int size = nums.Length;\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    Dictionary<int, int> dic = [];\n    // 単一ループで、時間計算量は O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.ContainsKey(target - nums[i])) {\n            return [dic[target - nums[i]], i];\n        }\n        dic.Add(nums[i], i);\n    }\n    return [];\n}\n
two_sum.go
/* 方法 2:補助ハッシュテーブル */\nfunc twoSumHashTable(nums []int, target int) []int {\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    hashTable := map[int]int{}\n    // 単一ループで、時間計算量は O(n)\n    for idx, val := range nums {\n        if preIdx, ok := hashTable[target-val]; ok {\n            return []int{preIdx, idx}\n        }\n        hashTable[val] = idx\n    }\n    return nil\n}\n
two_sum.swift
/* 方法 2:補助ハッシュテーブル */\nfunc twoSumHashTable(nums: [Int], target: Int) -> [Int] {\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    var dic: [Int: Int] = [:]\n    // 単一ループで、時間計算量は O(n)\n    for i in nums.indices {\n        if let j = dic[target - nums[i]] {\n            return [j, i]\n        }\n        dic[nums[i]] = i\n    }\n    return [0]\n}\n
two_sum.js
/* 方法 2:補助ハッシュテーブル */\nfunction twoSumHashTable(nums, target) {\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    let m = {};\n    // 単一ループで、時間計算量は O(n)\n    for (let i = 0; i < nums.length; i++) {\n        if (m[target - nums[i]] !== undefined) {\n            return [m[target - nums[i]], i];\n        } else {\n            m[nums[i]] = i;\n        }\n    }\n    return [];\n}\n
two_sum.ts
/* 方法 2:補助ハッシュテーブル */\nfunction twoSumHashTable(nums: number[], target: number): number[] {\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    let m: Map<number, number> = new Map();\n    // 単一ループで、時間計算量は O(n)\n    for (let i = 0; i < nums.length; i++) {\n        let index = m.get(target - nums[i]);\n        if (index !== undefined) {\n            return [index, i];\n        } else {\n            m.set(nums[i], i);\n        }\n    }\n    return [];\n}\n
two_sum.dart
/* 方法2: 補助ハッシュテーブル */\nList<int> twoSumHashTable(List<int> nums, int target) {\n  int size = nums.length;\n  // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n  Map<int, int> dic = HashMap();\n  // 単一ループで、時間計算量は O(n)\n  for (var i = 0; i < size; i++) {\n    if (dic.containsKey(target - nums[i])) {\n      return [dic[target - nums[i]]!, i];\n    }\n    dic.putIfAbsent(nums[i], () => i);\n  }\n  return [0];\n}\n
two_sum.rs
/* 方法 2:補助ハッシュテーブル */\npub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    let mut dic = HashMap::new();\n    // 単一ループで、時間計算量は O(n)\n    for (i, num) in nums.iter().enumerate() {\n        match dic.get(&(target - num)) {\n            Some(v) => return Some(vec![*v as i32, i as i32]),\n            None => dic.insert(num, i as i32),\n        };\n    }\n    None\n}\n
two_sum.c
/* ハッシュテーブル */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // uthash.h を用いて実装\n} HashTable;\n\n/* ハッシュテーブルを検索する */\nHashTable *find(HashTable *h, int key) {\n    HashTable *tmp;\n    HASH_FIND_INT(h, &key, tmp);\n    return tmp;\n}\n\n/* ハッシュテーブルに要素を挿入する */\nvoid insert(HashTable **h, int key, int val) {\n    HashTable *t = find(*h, key);\n    if (t == NULL) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = key, tmp->val = val;\n        HASH_ADD_INT(*h, key, tmp);\n    } else {\n        t->val = val;\n    }\n}\n\n/* 方法 2:補助ハッシュテーブル */\nint *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {\n    HashTable *hashtable = NULL;\n    for (int i = 0; i < numsSize; i++) {\n        HashTable *t = find(hashtable, target - nums[i]);\n        if (t != NULL) {\n            int *res = malloc(sizeof(int) * 2);\n            res[0] = t->val, res[1] = i;\n            *returnSize = 2;\n            return res;\n        }\n        insert(&hashtable, nums[i], i);\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
two_sum.kt
/* 方法 2:補助ハッシュテーブル */\nfun twoSumHashTable(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // 補助ハッシュテーブルを使用し、空間計算量は O(n)\n    val dic = HashMap<Int, Int>()\n    // 単一ループで、時間計算量は O(n)\n    for (i in 0..<size) {\n        if (dic.containsKey(target - nums[i])) {\n            return intArrayOf(dic[target - nums[i]]!!, i)\n        }\n        dic[nums[i]] = i\n    }\n    return IntArray(0)\n}\n
two_sum.rb
### 方法2:補助ハッシュテーブル ###\ndef two_sum_hash_table(nums, target)\n  # 補助ハッシュテーブルを使用し、空間計算量は O(n)\n  dic = {}\n  # 単一ループで、時間計算量は O(n)\n  for i in 0...nums.length\n    return [dic[target - nums[i]], i] if dic.has_key?(target - nums[i])\n\n    dic[nums[i]] = i\n  end\n\n  []\nend\n
コードの可視化

全画面で見る >

この方法ではハッシュ探索によって時間計算量を \\(O(n^2)\\) から \\(O(n)\\) に下げ,実行効率を大幅に向上させます。

追加のハッシュテーブルを維持する必要があるため,空間計算量は \\(O(n)\\) です。それでも,この方法は全体として時間と空間の効率のバランスがより良く,本問の最適解です。

","path":["第 10 章   探索","10.4   ハッシュによる最適化戦略"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/","level":1,"title":"10.5   探索アルゴリズム再考","text":"

探索アルゴリズム(searching algorithm)は、データ構造(配列、連結リスト、木、グラフなど)の中から、特定の条件を満たす 1 つまたは複数の要素を探索するために用いられます。

探索アルゴリズムは、実装の考え方に応じて次の 2 種類に分けられます。

  • データ構造を走査して目標要素を特定する方法。配列、連結リスト、木、グラフの走査などがこれに当たります。
  • データの構成やデータに含まれる事前情報を利用して、要素を効率よく探す方法。二分探索、ハッシュ探索、二分探索木による探索などがこれに当たります。

これらのトピックはすでに前の章で扱っているため、探索アルゴリズムは私たちにとって見慣れたものです。本節では、より体系的な視点から探索アルゴリズムをあらためて見直します。

","path":["第 10 章   探索","10.5   探索アルゴリズム再考"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051","level":2,"title":"10.5.1   総当たり探索","text":"

総当たり探索は、データ構造の各要素を順に調べて目標要素を特定します。

  • “線形探索”は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から始めて、要素を 1 つずつ調べ、目標要素が見つかるか、もう一方の端に達しても見つからないまで続けます。
  • “幅優先探索”と“深さ優先探索”は、グラフと木における 2 つの走査戦略です。幅優先探索は初期ノードから始めて層ごとに探索し、近いところから遠いところへ各ノードを訪れます。深さ優先探索は初期ノードから始めて 1 本の経路を最後までたどり、その後でバックトラックしてほかの経路を試し、データ構造全体を走査し終えるまで続けます。

総当たり探索の利点は、単純で汎用性が高く、**データの前処理や追加のデータ構造を必要としない**ことです。

しかし、この種のアルゴリズムの時間計算量は \\(O(n)\\) です。ここで \\(n\\) は要素数であり、そのためデータ量が大きい場合は性能が低くなります。

","path":["第 10 章   探索","10.5   探索アルゴリズム再考"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1052","level":2,"title":"10.5.2   適応的な探索","text":"

適応的な探索は、データが持つ固有の性質(整列性など)を利用して探索過程を最適化し、目標要素をより効率よく特定します。

  • “二分探索”は、データの順序性を利用して効率的な探索を行う方法で、配列にしか適用できません。
  • “ハッシュ探索”は、ハッシュ表を用いて探索対象のデータと目標データをキーと値の対応にし、問い合わせ操作を実現します。
  • “木探索”は、特定の木構造(たとえば二分探索木)の中で、ノード値の比較に基づいて不要なノードをすばやく除外し、目標要素を特定します。

この種のアルゴリズムの利点は効率が高く、**時間計算量が \\(O(\\log n)\\) あるいは \\(O(1)\\) に達する**ことです。

しかし、これらのアルゴリズムを使うには、たいていデータの前処理が必要です。たとえば、二分探索では事前に配列をソートする必要があり、ハッシュ探索と木探索では追加のデータ構造が必要です。これらのデータ構造を維持するにも、追加の時間と空間のコストがかかります。

Tip

適応的な探索アルゴリズムは、しばしば検索アルゴリズムとも呼ばれ、主に特定のデータ構造の中で目標要素を高速に取得するために用いられます。

","path":["第 10 章   探索","10.5   探索アルゴリズム再考"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1053","level":2,"title":"10.5.3   探索手法の選択","text":"

大きさ \\(n\\) のデータ集合が与えられたとき、線形探索、二分探索、木探索、ハッシュ探索など、さまざまな方法で目標要素を探索できます。各手法の動作原理を下図に示します。

図 10-11   複数の探索戦略

上記のいくつかの手法について、操作効率と特性を次の表に示します。

表 10-1   探索アルゴリズムの効率比較

線形探索 二分探索 木探索 ハッシュ探索 要素探索 \\(O(n)\\) \\(O(\\log n)\\) \\(O(\\log n)\\) \\(O(1)\\) 要素挿入 \\(O(1)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) 要素削除 \\(O(n)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) 追加領域 \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\) データ前処理 / ソート \\(O(n \\log n)\\) 木構築 \\(O(n \\log n)\\) ハッシュ表構築 \\(O(n)\\) データの順序性 なし あり あり なし

探索アルゴリズムの選択は、規模、探索性能の要求、データの問い合わせ頻度や更新頻度などにも左右されます。

線形探索

  • 汎用性が高く、データの前処理をまったく必要としません。データを 1 回だけ問い合わせればよい場合、ほか 3 つの手法では前処理にかかる時間のほうが、線形探索そのものより長くなることがあります。
  • 規模の小さいデータに適しています。この場合、時間計算量が効率に与える影響は比較的小さいです。
  • データ更新頻度が高い場面に適しています。この手法では、データに対する追加の保守が不要だからです。

二分探索

  • 大規模データに適しており、効率が安定しています。最悪時間計算量は \\(O(\\log n)\\) です。
  • データ量が大きすぎる場合には向きません。配列の格納には連続したメモリ領域が必要だからです。
  • 頻繁な挿入・削除がある場面には向きません。整列配列を維持するコストが高いためです。

ハッシュ探索

  • 問い合わせ性能への要求が高い場面に適しており、平均時間計算量は \\(O(1)\\) です。
  • 順序付きデータや範囲探索が必要な場面には向きません。ハッシュ表ではデータの順序性を維持できないからです。
  • ハッシュ関数とハッシュ衝突処理戦略への依存度が高く、性能劣化のリスクが大きいです。
  • データ量が大きすぎる場合には向きません。ハッシュ表は衝突をできるだけ減らして良好な問い合わせ性能を出すために、追加の空間を必要とするからです。

木探索

  • 巨大データに適しています。木ノードはメモリ上に分散して格納されるためです。
  • 順序付きデータの維持や範囲探索が必要な場面に適しています。
  • ノードの挿入・削除を続ける過程で、二分探索木は偏ることがあり、時間計算量は \\(O(n)\\) まで劣化する可能性があります。
  • AVL 木や赤黒木を使えば、各種操作を \\(O(\\log n)\\) の効率で安定して実行できますが、木の平衡を保つ処理による追加コストが発生します。
","path":["第 10 章   探索","10.5   探索アルゴリズム再考"],"tags":[]},{"location":"chapter_searching/summary/","level":1,"title":"10.6   まとめ","text":"","path":["第 10 章   探索","10.6   まとめ"],"tags":[]},{"location":"chapter_searching/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • 二分探索はデータの順序性に依存し、ループによって探索区間を半分ずつ縮小しながら探索を行う。入力データがソート済みであることを前提とし、配列または配列ベースで実装されたデータ構造にのみ適用できる。
  • 総当たり探索はデータ構造を走査してデータを特定する。線形探索は配列と連結リストに適しており、幅優先探索と深さ優先探索はグラフと木に適している。この種のアルゴリズムは汎用性が高く、データの前処理を必要としないが、時間計算量 \\(O(n)\\) は高い。
  • ハッシュ探索、木探索、二分探索は高効率な探索手法であり、特定のデータ構造内で目的の要素を高速に特定できる。この種のアルゴリズムは効率が高く、時間計算量は \\(O(\\log n)\\) あるいは \\(O(1)\\) に達するが、通常は追加のデータ構造を必要とする。
  • 実際には、データ規模、探索性能の要件、データの問い合わせ頻度や更新頻度などの要因を具体的に分析し、そのうえで適切な探索手法を選択する必要がある。
  • 線形探索は小規模または頻繁に更新されるデータに適している。二分探索は大規模でソート済みのデータに適している。ハッシュ探索は問い合わせ効率への要求が高く、範囲検索を必要としないデータに適している。木探索は順序の維持と範囲検索のサポートが必要な大規模動的データに適している。
  • ハッシュ探索で線形探索を置き換えることは、実行時間を最適化するための一般的な戦略であり、時間計算量を \\(O(n)\\) から \\(O(1)\\) へと下げられる。
","path":["第 10 章   探索","10.6   まとめ"],"tags":[]},{"location":"chapter_sorting/","level":1,"title":"第 11 章   ソート","text":"

Abstract

ソートは、混沌を秩序へと変える魔法の鍵のようなものであり、データをより効率的に理解し処理することを可能にします。

単純な昇順であれ、複雑な分類配列であれ、ソートはデータの調和のとれた美しさを私たちに示してくれます。

","path":["第 11 章   ソート"],"tags":[]},{"location":"chapter_sorting/#_1","level":2,"title":"章の内容","text":"
  • 11.1   ソートアルゴリズム
  • 11.2   選択ソート
  • 11.3   バブルソート
  • 11.4   挿入ソート
  • 11.5   クイックソート
  • 11.6   マージソート
  • 11.7   ヒープソート
  • 11.8   バケットソート
  • 11.9   計数ソート
  • 11.10   基数ソート
  • 11.11   まとめ
","path":["第 11 章   ソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   バブルソート","text":"

バブルソート(bubble sort)は、隣接する要素を繰り返し比較して交換することで整列を行います。この過程が泡のように下から上へ浮かび上がることから、バブルソートと呼ばれます。

次の図に示すように、バブル処理は要素の交換操作によってシミュレートできます。配列の最も左の端から右へ走査し、隣接する要素の大小を順に比較して、「左要素 > 右要素」であれば両者を交換します。走査が終わると、最大の要素は配列の最も右端へ移動します。

<1><2><3><4><5><6><7>

図 11-4   要素の交換操作でバブル処理をシミュレート

","path":["第 11 章   ソート","11.3   バブルソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1131","level":2,"title":"11.3.1   アルゴリズムの流れ","text":"

配列の長さを \\(n\\) とすると、バブルソートの手順は次の図のとおりです。

  1. まず、\\(n\\) 個の要素に対して「バブル処理」を行い、配列中の最大要素を正しい位置へ交換します。
  2. 次に、残りの \\(n - 1\\) 個の要素に対して「バブル処理」を行い、2 番目に大きい要素を正しい位置へ交換します。
  3. このようにして、\\(n - 1\\) 回の「バブル処理」を終えると、大きいほうから \\(n - 1\\) 個の要素がすべて正しい位置へ交換されます。
  4. 残った 1 つの要素は必ず最小要素なので、並べ替える必要はなく、これで配列のソートが完了します。

図 11-5   バブルソートの流れ

コード例は次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
def bubble_sort(nums: list[int]):\n    \"\"\"バブルソート\"\"\"\n    n = len(nums)\n    # 外側のループ:未ソート区間は [0, i]\n    for i in range(n - 1, 0, -1):\n        # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # nums[j] と nums[j + 1] を交換\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n
bubble_sort.cpp
/* バブルソート */\nvoid bubbleSort(vector<int> &nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換する\n                // ここでは std::swap() 関数を使用する\n                swap(nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
bubble_sort.java
/* バブルソート */\nvoid bubbleSort(int[] nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.cs
/* バブルソート */\nvoid BubbleSort(int[] nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
bubble_sort.go
/* バブルソート */\nfunc bubbleSort(nums []int) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // nums[j] と nums[j + 1] を交換\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n            }\n        }\n    }\n}\n
bubble_sort.swift
/* バブルソート */\nfunc bubbleSort(nums: inout [Int]) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                nums.swapAt(j, j + 1)\n            }\n        }\n    }\n}\n
bubble_sort.js
/* バブルソート */\nfunction bubbleSort(nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.ts
/* バブルソート */\nfunction bubbleSort(nums: number[]): void {\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.dart
/* バブルソート */\nvoid bubbleSort(List<int> nums) {\n  // 外側のループ:未ソート区間は [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // nums[j] と nums[j + 1] を交換\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n      }\n    }\n  }\n}\n
bubble_sort.rs
/* バブルソート */\nfn bubble_sort(nums: &mut [i32]) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i in (1..nums.len()).rev() {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                nums.swap(j, j + 1);\n            }\n        }\n    }\n}\n
bubble_sort.c
/* バブルソート */\nvoid bubbleSort(int nums[], int size) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n            }\n        }\n    }\n}\n
bubble_sort.kt
/* バブルソート */\nfun bubbleSort(nums: IntArray) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n            }\n        }\n    }\n}\n
bubble_sort.rb
### バブルソート ###\ndef bubble_sort(nums)\n  n = nums.length\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (n - 1).downto(1)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.3   バブルソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1132","level":2,"title":"11.3.2   効率の最適化","text":"

ある回の「バブル処理」で交換操作が一度も行われなければ、配列はすでにソート済みであり、結果をそのまま返せることがわかります。したがって、この状況を検出するためのフラグ flag を追加し、発生した時点で直ちに返すようにできます。

最適化後も、バブルソートの最悪時間計算量と平均時間計算量は依然として \\(O(n^2)\\) です。ただし、入力配列が完全に整列済みであれば、最良時間計算量は \\(O(n)\\) に達します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
def bubble_sort_with_flag(nums: list[int]):\n    \"\"\"バブルソート(フラグ最適化)\"\"\"\n    n = len(nums)\n    # 外側のループ:未ソート区間は [0, i]\n    for i in range(n - 1, 0, -1):\n        flag = False  # フラグを初期化する\n        # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # nums[j] と nums[j + 1] を交換\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n                flag = True  # 交換する要素を記録\n        if not flag:\n            break  # このバブル処理で要素交換が一度もなければそのまま終了\n
bubble_sort.cpp
/* バブルソート(フラグ最適化) */\nvoid bubbleSortWithFlag(vector<int> &nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        bool flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換する\n                // ここでは std::swap() 関数を使用する\n                swap(nums[j], nums[j + 1]);\n                flag = true; // 交換する要素を記録\n            }\n        }\n        if (!flag)\n            break; // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.java
/* バブルソート(フラグ最適化) */\nvoid bubbleSortWithFlag(int[] nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        boolean flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // 交換する要素を記録\n            }\n        }\n        if (!flag)\n            break; // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.cs
/* バブルソート(フラグ最適化) */\nvoid BubbleSortWithFlag(int[] nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        bool flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                flag = true;  // 交換する要素を記録\n            }\n        }\n        if (!flag) break;     // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.go
/* バブルソート(フラグ最適化) */\nfunc bubbleSortWithFlag(nums []int) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        flag := false // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // nums[j] と nums[j + 1] を交換\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n                flag = true // 交換する要素を記録\n            }\n        }\n        if flag == false { // このバブル処理で要素交換が一度もなければそのまま終了\n            break\n        }\n    }\n}\n
bubble_sort.swift
/* バブルソート(フラグ最適化) */\nfunc bubbleSortWithFlag(nums: inout [Int]) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        var flag = false // フラグを初期化する\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                nums.swapAt(j, j + 1)\n                flag = true // 交換する要素を記録\n            }\n        }\n        if !flag { // このバブル処理で要素交換が一度もなければそのまま終了\n            break\n        }\n    }\n}\n
bubble_sort.js
/* バブルソート(フラグ最適化) */\nfunction bubbleSortWithFlag(nums) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // 交換する要素を記録\n            }\n        }\n        if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.ts
/* バブルソート(フラグ最適化) */\nfunction bubbleSortWithFlag(nums: number[]): void {\n    // 外側のループ:未ソート区間は [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // 交換する要素を記録\n            }\n        }\n        if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.dart
/* バブルソート(フラグ最適化) */\nvoid bubbleSortWithFlag(List<int> nums) {\n  // 外側のループ:未ソート区間は [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    bool flag = false; // フラグを初期化する\n    // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // nums[j] と nums[j + 1] を交換\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        flag = true; // 交換する要素を記録\n      }\n    }\n    if (!flag) break; // このバブル処理で要素交換が一度もなければそのまま終了\n  }\n}\n
bubble_sort.rs
/* バブルソート(フラグ最適化) */\nfn bubble_sort_with_flag(nums: &mut [i32]) {\n    // 外側のループ:未ソート区間は [0, i]\n    for i in (1..nums.len()).rev() {\n        let mut flag = false; // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // nums[j] と nums[j + 1] を交換\n                nums.swap(j, j + 1);\n                flag = true; // 交換する要素を記録\n            }\n        }\n        if !flag {\n            break; // このバブル処理で要素交換が一度もなければそのまま終了\n        };\n    }\n}\n
bubble_sort.c
/* バブルソート(フラグ最適化) */\nvoid bubbleSortWithFlag(int nums[], int size) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        bool flag = false;\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n                flag = true;\n            }\n        }\n        if (!flag)\n            break;\n    }\n}\n
bubble_sort.kt
/* バブルソート(フラグ最適化) */\nfun bubbleSortWithFlag(nums: IntArray) {\n    // 外側のループ:未ソート区間は [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        var flag = false // フラグを初期化する\n        // 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // nums[j] と nums[j + 1] を交換\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                flag = true // 交換する要素を記録\n            }\n        }\n        if (!flag) break // このバブル処理で要素交換が一度もなければそのまま終了\n    }\n}\n
bubble_sort.rb
### バブルソート ###\ndef bubble_sort(nums)\n  n = nums.length\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (n - 1).downto(1)\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n\n# ## バブルソート(フラグ最適化)###\ndef bubble_sort_with_flag(nums)\n  n = nums.length\n  # 外側のループ:未ソート区間は [0, i]\n  for i in (n - 1).downto(1)\n    flag = false # フラグを初期化する\n\n    # 内側のループ:未ソート区間 [0, i] の最大要素をその区間の最右端へ交換\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # nums[j] と nums[j + 1] を交換\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n        flag = true # 交換する要素を記録\n      end\n    end\n\n    break unless flag # このバブル処理で要素交換が一度もなければそのまま終了\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.3   バブルソート"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1133","level":2,"title":"11.3.3   アルゴリズムの特徴","text":"
  • 時間計算量は \\(O(n^2)\\)、適応的ソート:各回の「バブル処理」で走査する配列の長さは順に \\(n - 1\\)、\\(n - 2\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) であり、その総和は \\((n - 1) n / 2\\) です。flag による最適化を導入すると、最良時間計算量は \\(O(n)\\) に達します。
  • 空間計算量は \\(O(1)\\)、インプレースソート:ポインタ \\(i\\) と \\(j\\) は定数サイズの追加領域しか使用しません。
  • 安定ソート:「バブル処理」では等しい要素に出会っても交換しないためです。
","path":["第 11 章   ソート","11.3   バブルソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/","level":1,"title":"11.8   バケットソート","text":"

前述のいくつかのソートアルゴリズムは、いずれも「比較ベースのソートアルゴリズム」に属し、要素間の大小を比較することで整列を実現します。この種のソートアルゴリズムの時間計算量は \\(O(n \\log n)\\) を超えられません。続いて、時間計算量が線形オーダーに達しうる「非比較ソートアルゴリズム」をいくつか見ていきます。

バケットソート(bucket sort)は分割統治戦略の典型的な応用です。大小関係をもつ複数のバケットを用意し、各バケットがあるデータ範囲に対応するようにして、データを各バケットへ均等に分配します。その後、各バケット内でそれぞれソートを行い、最後にバケットの順序に従ってすべてのデータを結合します。

","path":["第 11 章   ソート","11.8   バケットソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1181","level":2,"title":"11.8.1   アルゴリズムの流れ","text":"

長さ \\(n\\) の配列を考え、その要素は範囲 \\([0, 1)\\) の浮動小数点数であるとします。バケットソートの流れを以下の図に示します。

  1. \\(k\\) 個のバケットを初期化し、\\(n\\) 個の要素を \\(k\\) 個のバケットに分配します。
  2. 各バケットに対してそれぞれソートを実行します(ここではプログラミング言語の組み込みソート関数を用います)。
  3. バケットを小さい順にたどって結果を結合します。

図 11-13   バケットソートの流れ

コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bucket_sort.py
def bucket_sort(nums: list[float]):\n    \"\"\"バケットソート\"\"\"\n    # k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    k = len(nums) // 2\n    buckets = [[] for _ in range(k)]\n    # 1. 配列要素を各バケットに振り分ける\n    for num in nums:\n        # 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        i = int(num * k)\n        # num をバケット i に追加\n        buckets[i].append(num)\n    # 2. 各バケットをソートする\n    for bucket in buckets:\n        # 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.sort()\n    # 3. バケットを走査して結果を結合\n    i = 0\n    for bucket in buckets:\n        for num in bucket:\n            nums[i] = num\n            i += 1\n
bucket_sort.cpp
/* バケットソート */\nvoid bucketSort(vector<float> &nums) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    int k = nums.size() / 2;\n    vector<vector<float>> buckets(k);\n    // 1. 配列要素を各バケットに振り分ける\n    for (float num : nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        int i = num * k;\n        // num をバケット bucket_idx に追加\n        buckets[i].push_back(num);\n    }\n    // 2. 各バケットをソートする\n    for (vector<float> &bucket : buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        sort(bucket.begin(), bucket.end());\n    }\n    // 3. バケットを走査して結果を結合\n    int i = 0;\n    for (vector<float> &bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.java
/* バケットソート */\nvoid bucketSort(float[] nums) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    int k = nums.length / 2;\n    List<List<Float>> buckets = new ArrayList<>();\n    for (int i = 0; i < k; i++) {\n        buckets.add(new ArrayList<>());\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for (float num : nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        int i = (int) (num * k);\n        // num をバケット i に追加\n        buckets.get(i).add(num);\n    }\n    // 2. 各バケットをソートする\n    for (List<Float> bucket : buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        Collections.sort(bucket);\n    }\n    // 3. バケットを走査して結果を結合\n    int i = 0;\n    for (List<Float> bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.cs
/* バケットソート */\nvoid BucketSort(float[] nums) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    int k = nums.Length / 2;\n    List<List<float>> buckets = [];\n    for (int i = 0; i < k; i++) {\n        buckets.Add([]);\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    foreach (float num in nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        int i = (int)(num * k);\n        // num をバケット i に追加\n        buckets[i].Add(num);\n    }\n    // 2. 各バケットをソートする\n    foreach (List<float> bucket in buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.Sort();\n    }\n    // 3. バケットを走査して結果を結合\n    int j = 0;\n    foreach (List<float> bucket in buckets) {\n        foreach (float num in bucket) {\n            nums[j++] = num;\n        }\n    }\n}\n
bucket_sort.go
/* バケットソート */\nfunc bucketSort(nums []float64) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    k := len(nums) / 2\n    buckets := make([][]float64, k)\n    for i := 0; i < k; i++ {\n        buckets[i] = make([]float64, 0)\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for _, num := range nums {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        i := int(num * float64(k))\n        // num をバケット i に追加\n        buckets[i] = append(buckets[i], num)\n    }\n    // 2. 各バケットをソートする\n    for i := 0; i < k; i++ {\n        // 組み込みのスライスソート関数を使う。ほかのソートアルゴリズムに置き換えてもよい\n        sort.Float64s(buckets[i])\n    }\n    // 3. バケットを走査して結果を結合\n    i := 0\n    for _, bucket := range buckets {\n        for _, num := range bucket {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
bucket_sort.swift
/* バケットソート */\nfunc bucketSort(nums: inout [Double]) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    let k = nums.count / 2\n    var buckets = (0 ..< k).map { _ in [Double]() }\n    // 1. 配列要素を各バケットに振り分ける\n    for num in nums {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        let i = Int(num * Double(k))\n        // num をバケット i に追加\n        buckets[i].append(num)\n    }\n    // 2. 各バケットをソートする\n    for i in buckets.indices {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        buckets[i].sort()\n    }\n    // 3. バケットを走査して結果を結合\n    var i = nums.startIndex\n    for bucket in buckets {\n        for num in bucket {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
bucket_sort.js
/* バケットソート */\nfunction bucketSort(nums) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    const k = nums.length / 2;\n    const buckets = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for (const num of nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        const i = Math.floor(num * k);\n        // num をバケット i に追加\n        buckets[i].push(num);\n    }\n    // 2. 各バケットをソートする\n    for (const bucket of buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. バケットを走査して結果を結合\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.ts
/* バケットソート */\nfunction bucketSort(nums: number[]): void {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    const k = nums.length / 2;\n    const buckets: number[][] = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for (const num of nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        const i = Math.floor(num * k);\n        // num をバケット i に追加\n        buckets[i].push(num);\n    }\n    // 2. 各バケットをソートする\n    for (const bucket of buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. バケットを走査して結果を結合\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.dart
/* バケットソート */\nvoid bucketSort(List<double> nums) {\n  // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n  int k = nums.length ~/ 2;\n  List<List<double>> buckets = List.generate(k, (index) => []);\n\n  // 1. 配列要素を各バケットに振り分ける\n  for (double _num in nums) {\n    // 入力データの範囲は [0, 1) であり、_num * k を用いてインデックス範囲 [0, k-1] に写像する\n    int i = (_num * k).toInt();\n    // _num をバケット bucket_idx に追加\n    buckets[i].add(_num);\n  }\n  // 2. 各バケットをソートする\n  for (List<double> bucket in buckets) {\n    bucket.sort();\n  }\n  // 3. バケットを走査して結果を結合\n  int i = 0;\n  for (List<double> bucket in buckets) {\n    for (double _num in bucket) {\n      nums[i++] = _num;\n    }\n  }\n}\n
bucket_sort.rs
/* バケットソート */\nfn bucket_sort(nums: &mut [f64]) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    let k = nums.len() / 2;\n    let mut buckets = vec![vec![]; k];\n    // 1. 配列要素を各バケットに振り分ける\n    for &num in nums.iter() {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        let i = (num * k as f64) as usize;\n        // num をバケット i に追加\n        buckets[i].push(num);\n    }\n    // 2. 各バケットをソートする\n    for bucket in &mut buckets {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());\n    }\n    // 3. バケットを走査して結果を結合\n    let mut i = 0;\n    for bucket in buckets.iter() {\n        for &num in bucket.iter() {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
bucket_sort.c
/* バケットソート */\nvoid bucketSort(float nums[], int n) {\n    int k = n / 2;                                 // k = n/2 個のバケットを初期化する\n    int *sizes = malloc(k * sizeof(int));          // 各バケットのサイズを記録する\n    float **buckets = malloc(k * sizeof(float *)); // 動的配列の配列(バケット)\n    // 各バケットに十分な容量を事前確保する\n    for (int i = 0; i < k; ++i) {\n        buckets[i] = (float *)malloc(n * sizeof(float));\n        sizes[i] = 0;\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for (int i = 0; i < n; ++i) {\n        int idx = (int)(nums[i] * k);\n        buckets[idx][sizes[idx]++] = nums[i];\n    }\n    // 2. 各バケットをソートする\n    for (int i = 0; i < k; ++i) {\n        qsort(buckets[i], sizes[i], sizeof(float), compare);\n    }\n    // 3. ソート済みのバケットを結合する\n    int idx = 0;\n    for (int i = 0; i < k; ++i) {\n        for (int j = 0; j < sizes[i]; ++j) {\n            nums[idx++] = buckets[i][j];\n        }\n        // メモリを解放する\n        free(buckets[i]);\n    }\n}\n
bucket_sort.kt
/* バケットソート */\nfun bucketSort(nums: FloatArray) {\n    // k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n    val k = nums.size / 2\n    val buckets = mutableListOf<MutableList<Float>>()\n    for (i in 0..<k) {\n        buckets.add(mutableListOf())\n    }\n    // 1. 配列要素を各バケットに振り分ける\n    for (num in nums) {\n        // 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n        val i = (num * k).toInt()\n        // num をバケット i に追加\n        buckets[i].add(num)\n    }\n    // 2. 各バケットをソートする\n    for (bucket in buckets) {\n        // 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n        bucket.sort()\n    }\n    // 3. バケットを走査して結果を結合\n    var i = 0\n    for (bucket in buckets) {\n        for (num in bucket) {\n            nums[i++] = num\n        }\n    }\n}\n
bucket_sort.rb
### バケットソート ###\ndef bucket_sort(nums)\n  # k = n/2 個のバケットを初期化し、各バケットに 2 要素ずつ割り当てる想定とする\n  k = nums.length / 2\n  buckets = Array.new(k) { [] }\n\n  # 1. 配列要素を各バケットに振り分ける\n  nums.each do |num|\n    # 入力データの範囲は [0, 1) であり、num * k を用いてインデックス範囲 [0, k-1] に写像する\n    i = (num * k).to_i\n    # num をバケット i に追加\n    buckets[i] << num\n  end\n\n  # 2. 各バケットをソートする\n  buckets.each do |bucket|\n    # 組み込みのソート関数を使う。他のソートアルゴリズムに置き換えてもよい\n    bucket.sort!\n  end\n\n  # 3. バケットを走査して結果を結合\n  i = 0\n  buckets.each do |bucket|\n    bucket.each do |num|\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.8   バケットソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1182","level":2,"title":"11.8.2   アルゴリズムの特性","text":"

バケットソートは、非常に大規模なデータの処理に適しています。たとえば、入力データに 100 万個の要素が含まれ、空間の制約によりシステムメモリへすべてのデータを一度に読み込めない場合です。このとき、データを 1000 個のバケットに分け、それぞれのバケットを個別にソートしてから、最後に結果を結合できます。

  • 時間計算量は \\(O(n + k)\\) :要素が各バケット内に平均的に分布していると仮定すると、各バケット内の要素数は \\(\\frac{n}{k}\\) です。1 つのバケットをソートするのに \\(O(\\frac{n}{k} \\log\\frac{n}{k})\\) の時間がかかるなら、すべてのバケットのソートには \\(O(n \\log\\frac{n}{k})\\) の時間がかかります。バケット数 \\(k\\) が十分大きいとき、時間計算量は \\(O(n)\\) に近づきます。結果を結合する際には、すべてのバケットと要素を走査する必要があり、\\(O(n + k)\\) の時間を要します。最悪の場合、すべてのデータが 1 つのバケットに割り当てられ、そのバケットのソートに \\(O(n^2)\\) の時間がかかります。
  • 空間計算量は \\(O(n + k)\\)、非インプレースソート:\\(k\\) 個のバケットと合計 \\(n\\) 個の要素ぶんの追加領域が必要です。
  • バケットソートが安定かどうかは、バケット内要素のソートに用いるアルゴリズムが安定かどうかに依存します。
","path":["第 11 章   ソート","11.8   バケットソート"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1183","level":2,"title":"11.8.3   均等な分配を実現するには","text":"

バケットソートの時間計算量は理論上 \\(O(n)\\) に達しますが、鍵は要素を各バケットへ均等に分配すること にあります。実際のデータは均一に分布していないことが多いからです。たとえば、Taobao 上のすべての商品を価格帯ごとに 10 個のバケットへ均等に分けたいとしても、商品の価格分布は偏っており、100 元未満は非常に多く、1000 元超は非常に少ないかもしれません。価格区間を単純に 10 等分すると、各バケットの商品数には大きな差が生じます。

均等な分配を実現するために、まず大まかな境界線を設定し、データをひとまず 3 個のバケットに粗く振り分けます。分配後は、商品数の多いバケットをさらに 3 個のバケットに分割し、すべてのバケット内の要素数がおおむね等しくなるまでこれを続けます。

以下の図に示すように、この方法の本質は再帰木を構築することにあり、目標は葉ノードの値をできるだけ均等にすることです。もちろん、毎回データを 3 個のバケットに分割する必要はなく、具体的な分け方はデータの特徴に応じて柔軟に選べます。

図 11-14   再帰的にバケットを分割

商品価格の確率分布をあらかじめ把握しているなら、データの確率分布に基づいて各バケットの価格境界を設定できます。なお、データ分布は必ずしも特別に統計を取る必要はなく、データの特徴に応じて何らかの確率モデルで近似することもできます。

以下の図に示すように、商品価格が正規分布に従うと仮定すれば、価格区間を合理的に設定でき、それによって商品を各バケットへ均等に分配できます。

図 11-15   確率分布に基づいてバケットを分割

","path":["第 11 章   ソート","11.8   バケットソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/","level":1,"title":"11.9   計数ソート","text":"

計数ソート(counting sort)は要素数を集計することでソートを実現し、通常は整数配列に適用されます。

","path":["第 11 章   ソート","11.9   計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1191","level":2,"title":"11.9.1   単純な実装","text":"

まず簡単な例を見てみましょう。長さ \\(n\\) の配列 nums が与えられ、その要素はすべて「非負整数」であるとします。計数ソートの全体的な流れを以下の図に示します。

  1. 配列を走査し、その中の最大値を見つけて \\(m\\) とし、続いて長さ \\(m + 1\\) の補助配列 counter を作成します。
  2. counter を用いて nums 内の各数値の出現回数を集計します。ここで counter[num] は数値 num の出現回数に対応します。集計方法は非常に簡単で、nums を走査し(現在の数値を num とする)、各回で counter[num] を \\(1\\) 増やせばよいです。
  3. counter の各インデックスは自然に順序づけられているため、すべての数値はすでに整列された状態とみなせます。続いて counter を走査し、各数値の出現回数に応じて小さい順に nums へ書き戻せば完了です。

図 11-16   計数ソートの流れ

コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
def counting_sort_naive(nums: list[int]):\n    \"\"\"計数ソート\"\"\"\n    # 簡易版。オブジェクトのソートには使えない\n    # 1. 配列の最大要素 m を求める\n    m = max(nums)\n    # 2. 各数値の出現回数を数える\n    # counter[num] は num の出現回数を表す\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    i = 0\n    for num in range(m + 1):\n        for _ in range(counter[num]):\n            nums[i] = num\n            i += 1\n
counting_sort.cpp
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nvoid countingSortNaive(vector<int> &nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.java
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nvoid countingSortNaive(int[] nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.cs
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nvoid CountingSortNaive(int[] nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.go
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfunc countingSortNaive(nums []int) {\n    // 1. 配列の最大要素 m を求める\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    for i, num := 0, 0; num < m+1; num++ {\n        for j := 0; j < counter[num]; j++ {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
counting_sort.swift
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfunc countingSortNaive(nums: inout [Int]) {\n    // 1. 配列の最大要素 m を求める\n    let m = nums.max()!\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    var i = 0\n    for num in 0 ..< m + 1 {\n        for _ in 0 ..< counter[num] {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
counting_sort.js
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfunction countingSortNaive(nums) {\n    // 1. 配列の最大要素 m を求める\n    let m = Math.max(...nums);\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.ts
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfunction countingSortNaive(nums: number[]): void {\n    // 1. 配列の最大要素 m を求める\n    let m: number = Math.max(...nums);\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.dart
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nvoid countingSortNaive(List<int> nums) {\n  // 1. 配列の最大要素 m を求める\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. 各数値の出現回数を数える\n  // counter[_num] は _num の出現回数を表す\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n  int i = 0;\n  for (int _num = 0; _num < m + 1; _num++) {\n    for (int j = 0; j < counter[_num]; j++, i++) {\n      nums[i] = _num;\n    }\n  }\n}\n
counting_sort.rs
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfn counting_sort_naive(nums: &mut [i32]) {\n    // 1. 配列の最大要素 m を求める\n    let m = *nums.iter().max().unwrap();\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    let mut counter = vec![0; m as usize + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    let mut i = 0;\n    for num in 0..m + 1 {\n        for _ in 0..counter[num as usize] {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
counting_sort.c
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nvoid countingSortNaive(int nums[], int size) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int *counter = calloc(m + 1, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n    // 4. メモリを解放する\n    free(counter);\n}\n
counting_sort.kt
/* 計数ソート */\n// 簡易実装のため、オブジェクトのソートには使えない\nfun countingSortNaive(nums: IntArray) {\n    // 1. 配列の最大要素 m を求める\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. counter を走査し、各要素を元の配列 nums に書き戻す\n    var i = 0\n    for (num in 0..<m + 1) {\n        var j = 0\n        while (j < counter[num]) {\n            nums[i] = num\n            j++\n            i++\n        }\n    }\n}\n
counting_sort.rb
### 計数ソート ###\ndef counting_sort_naive(nums)\n  # 簡易版。オブジェクトのソートには使えない\n  # 1. 配列の最大要素 m を求める\n  m = 0\n  nums.each { |num| m = [m, num].max }\n  # 2. 各数値の出現回数を数える\n  # counter[num] は num の出現回数を表す\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. counter を走査し、各要素を元の配列 nums に書き戻す\n  i = 0\n  for num in 0...(m + 1)\n    (0...counter[num]).each do\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
コードの可視化

全画面で見る >

計数ソートとバケットソートの関係

バケットソートの観点から見ると、計数ソートにおける計数配列 counter の各インデックスを 1 つのバケットとみなし、個数を数える過程を各要素を対応するバケットへ振り分ける操作とみなせます。本質的には、計数ソートは整数データにおけるバケットソートの特殊な一例です。

","path":["第 11 章   ソート","11.9   計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1192","level":2,"title":"11.9.2   完全な実装","text":"

注意深い読者なら、**入力データがオブジェクトである場合、上記の手順 3. は機能しない**ことに気づくかもしれません。入力データが商品オブジェクトであり、商品価格(クラスのメンバ変数)に基づいて商品をソートしたいとします。しかし上記のアルゴリズムが返せるのは価格のソート結果だけです。

では、元のデータのソート結果を得るにはどうすればよいのでしょうか。まず counter の「累積和」を計算します。名前のとおり、インデックス i における累積和 prefix[i] は、配列の先頭から i 番目までの要素の総和に等しくなります:

\\[ \\text{prefix}[i] = \\sum_{j=0}^i \\text{counter[j]} \\]

累積和には明確な意味があり、prefix[num] - 1 は要素 num が結果配列 res に最後に現れるインデックスを表します。この情報は非常に重要で、各要素が結果配列のどの位置に現れるべきかを示してくれます。続いて元の配列 nums を逆順に走査し、各要素 num に対して各反復で次の 2 つの手順を行います。

  1. num を配列 res のインデックス prefix[num] - 1 に格納します。
  2. 累積和 prefix[num] を \\(1\\) 減らし、次に num を配置するインデックスを得ます。

走査が完了すると、配列 res にソート済みの結果が格納されます。最後に res で元の配列 nums を上書きすれば完了です。以下の図は完全な計数ソートの流れを示しています。

<1><2><3><4><5><6><7><8>

図 11-17   計数ソートの手順

計数ソートの実装コードは以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
def counting_sort(nums: list[int]):\n    \"\"\"計数ソート\"\"\"\n    # 完全版。オブジェクトをソートでき、かつ安定ソートである\n    # 1. 配列の最大要素 m を求める\n    m = max(nums)\n    # 2. 各数値の出現回数を数える\n    # counter[num] は num の出現回数を表す\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    # つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for i in range(m):\n        counter[i + 1] += counter[i]\n    # 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    # 結果を記録するための配列 res を初期化\n    n = len(nums)\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        num = nums[i]\n        res[counter[num] - 1] = num  # num を対応するインデックスに配置\n        counter[num] -= 1  # 累積和を 1 減らして、次に num を配置するインデックスを得る\n    # 結果配列 res で元の配列 nums を上書きする\n    for i in range(n):\n        nums[i] = res[i]\n
counting_sort.cpp
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nvoid countingSort(vector<int> &nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    int n = nums.size();\n    vector<int> res(n);\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--;              // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    nums = res;\n}\n
counting_sort.java
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nvoid countingSort(int[] nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    int n = nums.length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.cs
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nvoid CountingSort(int[] nums) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    int n = nums.Length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.go
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfunc countingSort(nums []int) {\n    // 1. 配列の最大要素 m を求める\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for i := 0; i < m; i++ {\n        counter[i+1] += counter[i]\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    n := len(nums)\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        num := nums[i]\n        // num を対応するインデックスに配置\n        res[counter[num]-1] = num\n        // 累積和を 1 減らして、次に num を配置するインデックスを得る\n        counter[num]--\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    copy(nums, res)\n}\n
counting_sort.swift
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfunc countingSort(nums: inout [Int]) {\n    // 1. 配列の最大要素 m を求める\n    let m = nums.max()!\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for i in 0 ..< m {\n        counter[i + 1] += counter[i]\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let num = nums[i]\n        res[counter[num] - 1] = num // num を対応するインデックスに配置\n        counter[num] -= 1 // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n
counting_sort.js
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfunction countingSort(nums) {\n    // 1. 配列の最大要素 m を求める\n    let m = Math.max(...nums);\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    const n = nums.length;\n    const res = new Array(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.ts
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfunction countingSort(nums: number[]): void {\n    // 1. 配列の最大要素 m を求める\n    let m: number = Math.max(...nums);\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    const n = nums.length;\n    const res: number[] = new Array<number>(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--; // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.dart
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nvoid countingSort(List<int> nums) {\n  // 1. 配列の最大要素 m を求める\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. 各数値の出現回数を数える\n  // counter[_num] は _num の出現回数を表す\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. counter の累積和を求め、「出現回数」を「末尾インデックス」に変換する\n  // つまり counter[_num]-1 は、res において _num が最後に出現する位置のインデックスである\n  for (int i = 0; i < m; i++) {\n    counter[i + 1] += counter[i];\n  }\n  // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n  // 結果を記録するための配列 res を初期化\n  int n = nums.length;\n  List<int> res = List.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int _num = nums[i];\n    res[counter[_num] - 1] = _num; // _num を対応する添字に配置\n    counter[_num]--; // 累積和を 1 減らし、次に _num を配置するインデックスを得る\n  }\n  // 結果配列 res で元の配列 nums を上書きする\n  nums.setAll(0, res);\n}\n
counting_sort.rs
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfn counting_sort(nums: &mut [i32]) {\n    // 1. 配列の最大要素 m を求める\n    let m = *nums.iter().max().unwrap() as usize;\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    let mut counter = vec![0; m + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for i in 0..m {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    let n = nums.len();\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let num = nums[i];\n        res[counter[num as usize] - 1] = num; // num を対応するインデックスに配置\n        counter[num as usize] -= 1; // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    nums.copy_from_slice(&res)\n}\n
counting_sort.c
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nvoid countingSort(int nums[], int size) {\n    // 1. 配列の最大要素 m を求める\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    int *counter = calloc(m, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    int *res = malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // num を対応するインデックスに配置\n        counter[num]--;              // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    memcpy(nums, res, size * sizeof(int));\n    // 5. メモリを解放する\n    free(res);\n    free(counter);\n}\n
counting_sort.kt
/* 計数ソート */\n// 完全な実装で、オブジェクトをソートでき、かつ安定ソートである\nfun countingSort(nums: IntArray) {\n    // 1. 配列の最大要素 m を求める\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. 各数値の出現回数を数える\n    // counter[num] は num の出現回数を表す\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n    // つまり counter[num]-1 は、num が res に最後に現れるインデックス\n    for (i in 0..<m) {\n        counter[i + 1] += counter[i]\n    }\n    // 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n    // 結果を記録するための配列 res を初期化\n    val n = nums.size\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val num = nums[i]\n        res[counter[num] - 1] = num // num を対応するインデックスに配置\n        counter[num]-- // 累積和を 1 減らして、次に num を配置するインデックスを得る\n    }\n    // 結果配列 res で元の配列 nums を上書きする\n    for (i in 0..<n) {\n        nums[i] = res[i]\n    }\n}\n
counting_sort.rb
### 計数ソート ###\ndef counting_sort(nums)\n  # 完全版。オブジェクトをソートでき、かつ安定ソートである\n  # 1. 配列の最大要素 m を求める\n  m = nums.max\n  # 2. 各数値の出現回数を数える\n  # counter[num] は num の出現回数を表す\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. counter の累積和を求めて、「出現回数」を「末尾インデックス」に変換する\n  # つまり counter[num]-1 は、num が res に最後に現れるインデックス\n  (0...m).each { |i| counter[i + 1] += counter[i] }\n  # 4. nums を逆順に走査し、各要素を結果配列 res に格納する\n  # 結果を記録するための配列 res を初期化する\n  n = nums.length\n  res = Array.new(n, 0)\n  (n - 1).downto(0).each do |i|\n    num = nums[i]\n    res[counter[num] - 1] = num # num を対応するインデックスに配置\n    counter[num] -= 1 # 累積和を 1 減らして、次に num を配置するインデックスを得る\n  end\n  # 結果配列 res で元の配列 nums を上書きする\n  (0...n).each { |i| nums[i] = res[i] }\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.9   計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1193","level":2,"title":"11.9.3   アルゴリズムの特性","text":"
  • 時間計算量は \\(O(n + m)\\)、非適応ソート :nums の走査と counter の走査が含まれ、いずれも線形時間です。一般には \\(n \\gg m\\) であり、時間計算量は \\(O(n)\\) に近づきます。
  • 空間計算量は \\(O(n + m)\\)、非インプレースソート:長さがそれぞれ \\(n\\) と \\(m\\) の配列 rescounter を利用します。
  • 安定ソート:res に要素を埋める順序が「右から左」であるため、nums を逆順に走査することで等しい要素どうしの相対位置が変化するのを防ぎ、安定ソートを実現できます。実際には、nums を順方向に走査しても正しいソート結果は得られますが、その結果は安定ではありません。
","path":["第 11 章   ソート","11.9   計数ソート"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1194","level":2,"title":"11.9.4   制約","text":"

ここまで読むと、計数ソートは非常に巧妙で、個数を数えるだけで効率的なソートを実現できると感じるかもしれません。しかし、計数ソートを利用するための前提条件は比較的厳格です。

計数ソートは非負整数にしか適用できません。ほかの型のデータに適用したい場合は、それらのデータを非負整数に変換でき、かつ変換の過程で要素間の相対的な大小関係が変わらないことを保証する必要があります。たとえば、負数を含む整数配列に対しては、すべての数値に定数を加えて正数へ変換し、ソート後に元へ戻すことができます。

計数ソートはデータ量が多く、値域が小さい場合に適しています。たとえば上記の例では \\(m\\) が大きすぎてはならず、そうでないと過剰な空間を消費します。また、\\(n \\ll m\\) のとき、計数ソートは \\(O(m)\\) 時間を要するため、\\(O(n \\log n)\\) のソートアルゴリズムより遅くなる可能性があります。

","path":["第 11 章   ソート","11.9   計数ソート"],"tags":[]},{"location":"chapter_sorting/heap_sort/","level":1,"title":"11.7   ヒープソート","text":"

Tip

本節を読む前に、「ヒープ」の章を学習済みであることを確認してください。

ヒープソート(heap sort)は、ヒープデータ構造に基づいて実装される効率的なソートアルゴリズムです。すでに学んだ「ヒープ構築操作」と「要素の取り出し操作」を利用してヒープソートを実現できます。

  1. 配列を入力して最小ヒープを構築すると、このとき最小要素はヒープの頂点にあります。
  2. 取り出し操作を繰り返し実行し、取り出された要素を順に記録すれば、昇順に並んだ列が得られます。

以上の方法でも実行できますが、取り出した要素を保存するために追加の配列が必要となり、空間をやや無駄にします。実際には、通常はより洗練された実装方法を用います。

","path":["第 11 章   ソート","11.7   ヒープソート"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1171","level":2,"title":"11.7.1   アルゴリズムの流れ","text":"

配列の長さを \\(n\\) とすると、ヒープソートの流れは次図のとおりです。

  1. 配列を入力して最大ヒープを構築します。完了後、最大要素はヒープの頂点にあります。
  2. ヒープ頂点の要素(最初の要素)とヒープ末尾の要素(最後の要素)を交換します。交換後、ヒープの長さは \\(1\\) 減少し、整列済み要素数は \\(1\\) 増加します。
  3. ヒープ頂点の要素から始めて、上から下へヒープ化操作(sift down)を実行します。ヒープ化が完了すると、ヒープの性質が回復します。
  4. 2. ステップと第 3. ステップを繰り返し実行します。これを \\(n - 1\\) 回繰り返すと、配列の整列が完了します。

Tip

実際には、要素の取り出し操作にも第 2. ステップと第 3. ステップが含まれており、要素を取り出す処理が 1 つ加わるだけです。

<1><2><3><4><5><6><7><8><9><10><11><12>

図 11-12   ヒープソートの手順

コード実装では、「ヒープ」の章と同じ上から下へのヒープ化 sift_down() 関数を使用します。注意すべき点として、ヒープの長さは最大要素を取り出すたびに短くなるため、sift_down() 関数に長さパラメータ \\(n\\) を追加し、ヒープの現在の有効な長さを指定する必要があります。コードは以下のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap_sort.py
def sift_down(nums: list[int], n: int, i: int):\n    \"\"\"ヒープの長さは n。ノード i から下方向にヒープ化\"\"\"\n    while True:\n        # ノード i, l, r のうち値が最大のノードを ma とする\n        l = 2 * i + 1\n        r = 2 * i + 2\n        ma = i\n        if l < n and nums[l] > nums[ma]:\n            ma = l\n        if r < n and nums[r] > nums[ma]:\n            ma = r\n        # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i:\n            break\n        # 2 つのノードを交換\n        nums[i], nums[ma] = nums[ma], nums[i]\n        # ループで上から下へヒープ化\n        i = ma\n\ndef heap_sort(nums: list[int]):\n    \"\"\"ヒープソート\"\"\"\n    # ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for i in range(len(nums) // 2 - 1, -1, -1):\n        sift_down(nums, len(nums), i)\n    # ヒープから最大要素を取り出し、n-1 回繰り返す\n    for i in range(len(nums) - 1, 0, -1):\n        # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        nums[0], nums[i] = nums[i], nums[0]\n        # 根ノードを起点に、上から下へヒープ化\n        sift_down(nums, i, 0)\n
heap_sort.cpp
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nvoid siftDown(vector<int> &nums, int n, int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i) {\n            break;\n        }\n        // 2 つのノードを交換\n        swap(nums[i], nums[ma]);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid heapSort(vector<int> &nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (int i = nums.size() / 2 - 1; i >= 0; --i) {\n        siftDown(nums, nums.size(), i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (int i = nums.size() - 1; i > 0; --i) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        swap(nums[0], nums[i]);\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.java
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nvoid siftDown(int[] nums, int n, int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i)\n            break;\n        // 2 つのノードを交換\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid heapSort(int[] nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (int i = nums.length / 2 - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (int i = nums.length - 1; i > 0; i--) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.cs
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nvoid SiftDown(int[] nums, int n, int i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i)\n            break;\n        // 2 つのノードを交換\n        (nums[ma], nums[i]) = (nums[i], nums[ma]);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid HeapSort(int[] nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (int i = nums.Length / 2 - 1; i >= 0; i--) {\n        SiftDown(nums, nums.Length, i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        (nums[i], nums[0]) = (nums[0], nums[i]);\n        // 根ノードを起点に、上から下へヒープ化\n        SiftDown(nums, i, 0);\n    }\n}\n
heap_sort.go
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfunc siftDown(nums *[]int, n, i int) {\n    for true {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        l := 2*i + 1\n        r := 2*i + 2\n        ma := i\n        if l < n && (*nums)[l] > (*nums)[ma] {\n            ma = l\n        }\n        if r < n && (*nums)[r] > (*nums)[ma] {\n            ma = r\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i {\n            break\n        }\n        // 2 つのノードを交換\n        (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n\n/* ヒープソート */\nfunc heapSort(nums *[]int) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for i := len(*nums)/2 - 1; i >= 0; i-- {\n        siftDown(nums, len(*nums), i)\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for i := len(*nums) - 1; i > 0; i-- {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0)\n    }\n}\n
heap_sort.swift
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfunc siftDown(nums: inout [Int], n: Int, i: Int) {\n    var i = i\n    while true {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let l = 2 * i + 1\n        let r = 2 * i + 2\n        var ma = i\n        if l < n, nums[l] > nums[ma] {\n            ma = l\n        }\n        if r < n, nums[r] > nums[ma] {\n            ma = r\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i {\n            break\n        }\n        // 2 つのノードを交換\n        nums.swapAt(i, ma)\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n\n/* ヒープソート */\nfunc heapSort(nums: inout [Int]) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {\n        siftDown(nums: &nums, n: nums.count, i: i)\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for i in nums.indices.dropFirst().reversed() {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        nums.swapAt(0, i)\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums: &nums, n: i, i: 0)\n    }\n}\n
heap_sort.js
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfunction siftDown(nums, n, i) {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma === i) {\n            break;\n        }\n        // 2 つのノードを交換\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfunction heapSort(nums) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.ts
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfunction siftDown(nums: number[], n: number, i: number): void {\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma === i) {\n            break;\n        }\n        // 2 つのノードを交換\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfunction heapSort(nums: number[]): void {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.dart
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nvoid siftDown(List<int> nums, int n, int i) {\n  while (true) {\n    // ノード i, l, r のうち値が最大のノードを ma とする\n    int l = 2 * i + 1;\n    int r = 2 * i + 2;\n    int ma = i;\n    if (l < n && nums[l] > nums[ma]) ma = l;\n    if (r < n && nums[r] > nums[ma]) ma = r;\n    // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n    if (ma == i) break;\n    // 2 つのノードを交換\n    int temp = nums[i];\n    nums[i] = nums[ma];\n    nums[ma] = temp;\n    // ループで上から下へヒープ化\n    i = ma;\n  }\n}\n\n/* ヒープソート */\nvoid heapSort(List<int> nums) {\n  // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n  for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {\n    siftDown(nums, nums.length, i);\n  }\n  // ヒープから最大要素を取り出し、n-1 回繰り返す\n  for (int i = nums.length - 1; i > 0; i--) {\n    // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    int tmp = nums[0];\n    nums[0] = nums[i];\n    nums[i] = tmp;\n    // 根ノードを起点に、上から下へヒープ化\n    siftDown(nums, i, 0);\n  }\n}\n
heap_sort.rs
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfn sift_down(nums: &mut [i32], n: usize, mut i: usize) {\n    loop {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let mut ma = i;\n        if l < n && nums[l] > nums[ma] {\n            ma = l;\n        }\n        if r < n && nums[r] > nums[ma] {\n            ma = r;\n        }\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if ma == i {\n            break;\n        }\n        // 2 つのノードを交換\n        nums.swap(i, ma);\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nfn heap_sort(nums: &mut [i32]) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for i in (0..nums.len() / 2).rev() {\n        sift_down(nums, nums.len(), i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for i in (1..nums.len()).rev() {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        nums.swap(0, i);\n        // 根ノードを起点に、上から下へヒープ化\n        sift_down(nums, i, 0);\n    }\n}\n
heap_sort.c
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nvoid siftDown(int nums[], int n, int i) {\n    while (1) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i) {\n            break;\n        }\n        // 2 つのノードを交換\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // ループで上から下へヒープ化\n        i = ma;\n    }\n}\n\n/* ヒープソート */\nvoid heapSort(int nums[], int n) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (int i = n / 2 - 1; i >= 0; --i) {\n        siftDown(nums, n, i);\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (int i = n - 1; i > 0; --i) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.kt
/* ヒープの長さは n。ノード i から下方向にヒープ化 */\nfun siftDown(nums: IntArray, n: Int, li: Int) {\n    var i = li\n    while (true) {\n        // ノード i, l, r のうち値が最大のノードを ma とする\n        val l = 2 * i + 1\n        val r = 2 * i + 2\n        var ma = i\n        if (l < n && nums[l] > nums[ma]) \n            ma = l\n        if (r < n && nums[r] > nums[ma]) \n            ma = r\n        // ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n        if (ma == i) \n            break\n        // 2 つのノードを交換\n        val temp = nums[i]\n        nums[i] = nums[ma]\n        nums[ma] = temp\n        // ループで上から下へヒープ化\n        i = ma\n    }\n}\n\n/* ヒープソート */\nfun heapSort(nums: IntArray) {\n    // ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n    for (i in nums.size / 2 - 1 downTo 0) {\n        siftDown(nums, nums.size, i)\n    }\n    // ヒープから最大要素を取り出し、n-1 回繰り返す\n    for (i in nums.size - 1 downTo 1) {\n        // 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n        val temp = nums[0]\n        nums[0] = nums[i]\n        nums[i] = temp\n        // 根ノードを起点に、上から下へヒープ化\n        siftDown(nums, i, 0)\n    }\n}\n
heap_sort.rb
### ヒープ長 n で、ノード i から上から下へヒープ化 ###\ndef sift_down(nums, n, i)\n  while true\n    # ノード i, l, r のうち値が最大のノードを ma とする\n    l = 2 * i + 1\n    r = 2 * i + 2\n    ma = i\n    ma = l if l < n && nums[l] > nums[ma]\n    ma = r if r < n && nums[r] > nums[ma]\n    # ノード i が最大、またはインデックス l, r が範囲外なら、ヒープ化は不要なので抜ける\n    break if ma == i\n    # 2 つのノードを交換\n    nums[i], nums[ma] = nums[ma], nums[i]\n    # ループで上から下へヒープ化\n    i = ma\n  end\nend\n\n### ヒープソート ###\ndef heap_sort(nums)\n  # ヒープ構築:葉ノード以外のすべてのノードをヒープ化する\n  (nums.length / 2 - 1).downto(0) do |i|\n    sift_down(nums, nums.length, i)\n  end\n  # ヒープから最大要素を取り出し、n-1 回繰り返す\n  (nums.length - 1).downto(1) do |i|\n    # 根ノードと最も右の葉ノードを交換(先頭要素と末尾要素を交換)\n    nums[0], nums[i] = nums[i], nums[0]\n    # 根ノードを起点に、上から下へヒープ化\n    sift_down(nums, i, 0)\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.7   ヒープソート"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1172","level":2,"title":"11.7.2   アルゴリズムの特性","text":"
  • 時間計算量は \\(O(n \\log n)\\)、非適応ソート:ヒープ構築操作には \\(O(n)\\) の時間がかかります。ヒープから最大要素を取り出す時間計算量は \\(O(\\log n)\\) であり、これを合計 \\(n - 1\\) 回繰り返します。
  • 空間計算量は \\(O(1)\\)、インプレースソート:いくつかのポインタ変数が使う空間は \\(O(1)\\) です。要素の交換とヒープ化操作はいずれも元の配列上で行われます。
  • 非安定ソート:ヒープ頂点の要素とヒープ末尾の要素を交換する際、等しい要素どうしの相対位置が変化する可能性があります。
","path":["第 11 章   ソート","11.7   ヒープソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/","level":1,"title":"11.4   挿入ソート","text":"

挿入ソート(insertion sort)は単純なソートアルゴリズムであり、その動作原理は手作業でトランプの山を整える過程と非常によく似ています。

具体的には、未ソート区間から基準要素を 1 つ選び、その要素を左側の整列済み区間の要素と 1 つずつ比較し、正しい位置に挿入します。

以下の図は、配列に要素を挿入する操作の流れを示しています。基準要素を base とすると、目的のインデックスから base までのすべての要素を 1 つずつ右に移動し、その後 base を目的のインデックスに代入する必要があります。

図 11-6   1 回の挿入操作

","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141","level":2,"title":"11.4.1   アルゴリズムの流れ","text":"

挿入ソート全体の流れを以下の図に示します。

  1. 初期状態では、配列の 1 番目の要素はすでに整列済みです。
  2. 配列の 2 番目の要素を base として選び、正しい位置に挿入すると、**配列の先頭 2 要素が整列済み**になります。
  3. 3 番目の要素を base として選び、正しい位置に挿入すると、**配列の先頭 3 要素が整列済み**になります。
  4. このように繰り返し、最後のラウンドで最後の要素を base として選んで正しい位置に挿入すると、**すべての要素が整列済み**になります。

図 11-7   挿入ソートの流れ

コード例は以下のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby insertion_sort.py
def insertion_sort(nums: list[int]):\n    \"\"\"挿入ソート\"\"\"\n    # 外側ループ:整列済み区間は [0, i-1]\n    for i in range(1, len(nums)):\n        base = nums[i]\n        j = i - 1\n        # 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while j >= 0 and nums[j] > base:\n            nums[j + 1] = nums[j]  # nums[j] を 1 つ右へ移動する\n            j -= 1\n        nums[j + 1] = base  # base を正しい位置に配置する\n
insertion_sort.cpp
/* 挿入ソート */\nvoid insertionSort(vector<int> &nums) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (int i = 1; i < nums.size(); i++) {\n        int base = nums[i], j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n            j--;\n        }\n        nums[j + 1] = base; // base を正しい位置に配置する\n    }\n}\n
insertion_sort.java
/* 挿入ソート */\nvoid insertionSort(int[] nums) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (int i = 1; i < nums.length; i++) {\n        int base = nums[i], j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n            j--;\n        }\n        nums[j + 1] = base;        // base を正しい位置に配置する\n    }\n}\n
insertion_sort.cs
/* 挿入ソート */\nvoid InsertionSort(int[] nums) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (int i = 1; i < nums.Length; i++) {\n        int bas = nums[i], j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > bas) {\n            nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n            j--;\n        }\n        nums[j + 1] = bas;         // base を正しい位置に配置する\n    }\n}\n
insertion_sort.go
/* 挿入ソート */\nfunc insertionSort(nums []int) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for i := 1; i < len(nums); i++ {\n        base := nums[i]\n        j := i - 1\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        for j >= 0 && nums[j] > base {\n            nums[j+1] = nums[j] // nums[j] を 1 つ右へ移動する\n            j--\n        }\n        nums[j+1] = base // base を正しい位置に配置する\n    }\n}\n
insertion_sort.swift
/* 挿入ソート */\nfunc insertionSort(nums: inout [Int]) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for i in nums.indices.dropFirst() {\n        let base = nums[i]\n        var j = i - 1\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while j >= 0, nums[j] > base {\n            nums[j + 1] = nums[j] // nums[j] を 1 つ右へ移動する\n            j -= 1\n        }\n        nums[j + 1] = base // base を正しい位置に配置する\n    }\n}\n
insertion_sort.js
/* 挿入ソート */\nfunction insertionSort(nums) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        let base = nums[i],\n            j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n            j--;\n        }\n        nums[j + 1] = base; // base を正しい位置に配置する\n    }\n}\n
insertion_sort.ts
/* 挿入ソート */\nfunction insertionSort(nums: number[]): void {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        const base = nums[i];\n        let j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n            j--;\n        }\n        nums[j + 1] = base; // base を正しい位置に配置する\n    }\n}\n
insertion_sort.dart
/* 挿入ソート */\nvoid insertionSort(List<int> nums) {\n  // 外側ループ:整列済み区間は [0, i-1]\n  for (int i = 1; i < nums.length; i++) {\n    int base = nums[i], j = i - 1;\n    // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n    while (j >= 0 && nums[j] > base) {\n      nums[j + 1] = nums[j]; // nums[j] を 1 つ右へ移動する\n      j--;\n    }\n    nums[j + 1] = base; // base を正しい位置に配置する\n  }\n}\n
insertion_sort.rs
/* 挿入ソート */\nfn insertion_sort(nums: &mut [i32]) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for i in 1..nums.len() {\n        let (base, mut j) = (nums[i], (i - 1) as i32);\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while j >= 0 && nums[j as usize] > base {\n            nums[(j + 1) as usize] = nums[j as usize]; // nums[j] を 1 つ右へ移動する\n            j -= 1;\n        }\n        nums[(j + 1) as usize] = base; // base を正しい位置に配置する\n    }\n}\n
insertion_sort.c
/* 挿入ソート */\nvoid insertionSort(int nums[], int size) {\n    // 外側ループ:整列済み区間は [0, i-1]\n    for (int i = 1; i < size; i++) {\n        int base = nums[i], j = i - 1;\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            // nums[j] を 1 つ右へ移動する\n            nums[j + 1] = nums[j];\n            j--;\n        }\n        // base を正しい位置に配置する\n        nums[j + 1] = base;\n    }\n}\n
insertion_sort.kt
/* 挿入ソート */\nfun insertionSort(nums: IntArray) {\n    // 外側ループ: ソート済み要素は 1, 2, ..., n\n    for (i in nums.indices) {\n        val base = nums[i]\n        var j = i - 1\n        // 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j] // nums[j] を 1 つ右へ移動する\n            j--\n        }\n        nums[j + 1] = base        // base を正しい位置に配置する\n    }\n}\n
insertion_sort.rb
### 挿入ソート ###\ndef insertion_sort(nums)\n  n = nums.length\n  # 外側ループ:整列済み区間は [0, i-1]\n  for i in 1...n\n    base = nums[i]\n    j = i - 1\n    # 内側ループ: base をソート済み区間 [0, i-1] の正しい位置に挿入する\n    while j >= 0 && nums[j] > base\n      nums[j + 1] = nums[j] # nums[j] を 1 つ右へ移動する\n      j -= 1\n    end\n    nums[j + 1] = base # base を正しい位置に配置する\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1142","level":2,"title":"11.4.2   アルゴリズムの特徴","text":"
  • 計算量は \\(O(n^2)\\)、適応的ソート:最悪の場合、各挿入操作ではそれぞれ \\(n - 1\\)、\\(n-2\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) 回のループが必要であり、合計は \\((n - 1) n / 2\\) となるため、時間計算量は \\(O(n^2)\\) です。データが整列済みであれば、挿入操作は早期に終了します。入力配列が完全に整列済みである場合、挿入ソートは最良の時間計算量 \\(O(n)\\) に達します。
  • 空間計算量は \\(O(1)\\)、インプレースソート:ポインタ \\(i\\) と \\(j\\) は定数サイズの追加領域しか使用しません。
  • 安定ソート:挿入操作の過程では、要素を等しい要素の右側に挿入するため、それらの順序は変化しません。
","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1143","level":2,"title":"11.4.3   挿入ソートの利点","text":"

挿入ソートの時間計算量は \\(O(n^2)\\) であり、これから学ぶクイックソートの時間計算量は \\(O(n \\log n)\\) です。挿入ソートの時間計算量のほうが大きいにもかかわらず、**データ量が小さい場合には、挿入ソートのほうが通常は高速**です。

この結論は、線形探索と二分探索の適用条件に関する結論と似ています。クイックソートのような \\(O(n \\log n)\\) のアルゴリズムは分割統治法に基づくソートアルゴリズムであり、一般により多くの基本演算を含みます。一方、データ量が小さい場合は、\\(n^2\\) と \\(n \\log n\\) の値は比較的近く、計算量が支配的ではなくなり、各ラウンドにおける基本演算の回数が決定的な役割を果たします。

実際、多くのプログラミング言語(たとえば Java)の組み込みソート関数では挿入ソートが採用されており、その大まかな考え方は次のとおりです。長い配列にはクイックソートなどの分割統治法に基づくソートアルゴリズムを使い、短い配列には直接挿入ソートを使います。

バブルソート、選択ソート、挿入ソートはいずれも時間計算量が \\(O(n^2)\\) ですが、実際には、挿入ソートはバブルソートや選択ソートよりもはるかに高い頻度で使われます。主な理由は次のとおりです。

  • バブルソートは要素の交換によって実装され、1 つの一時変数を必要とするため、合計で 3 回の基本演算が関わります。これに対して、挿入ソートは要素の代入に基づいており、必要な基本演算は 1 回だけです。したがって、バブルソートの計算コストは通常、挿入ソートより高くなります。
  • 選択ソートの時間計算量はどのような場合でも \\(O(n^2)\\) です。**部分的に整列されたデータが与えられた場合、挿入ソートは通常、選択ソートより効率的**です。
  • 選択ソートは安定ではないため、多段ソートには適用できません。
","path":["第 11 章   ソート","11.4   挿入ソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   マージソート","text":"

マージソート(merge sort)は分割統治戦略に基づくソートアルゴリズムであり、以下の図に示す「分割」と「マージ」の段階から構成されます。

  1. 分割段階:再帰によって配列を中点で繰り返し分割し、長い配列のソート問題を短い配列のソート問題へ変換します。
  2. マージ段階:部分配列の長さが 1 になったら分割を終了し、マージを開始して、左右 2 つの短いソート済み配列をより長いソート済み配列へと繰り返しマージしていきます。

図 11-10   マージソートの分割とマージの段階

","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161","level":2,"title":"11.6.1   アルゴリズムの流れ","text":"

以下の図に示すように、「分割段階」では配列を上から下へ再帰的に中点で 2 つの部分配列へ分割します。

  1. 配列の中点 mid を計算し、左部分配列(区間 [left, mid] )と右部分配列(区間 [mid + 1, right] )を再帰的に分割します。
  2. 手順 1. を再帰的に実行し、部分配列区間の長さが 1 になった時点で終了します。

「マージ段階」では左部分配列と右部分配列を下から上へとマージし、1 つのソート済み配列にします。長さ 1 の部分配列からマージを始めるため、この段階の各部分配列はすでに整列されています。

<1><2><3><4><5><6><7><8><9><10>

図 11-11   マージソートの手順

観察すると、マージソートの再帰順序は二分木の後順走査と一致していることがわかります。

  • 後順走査:まず左部分木を再帰し、次に右部分木を再帰し、最後に根ノードを処理します。
  • マージソート:まず左部分配列を再帰し、次に右部分配列を再帰し、最後にマージを処理します。

マージソートの実装を以下のコードに示します。注意として、nums のマージ対象区間は [left, right] であり、tmp の対応区間は [0, right - left] です。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby merge_sort.py
def merge(nums: list[int], left: int, mid: int, right: int):\n    \"\"\"左部分配列と右部分配列をマージ\"\"\"\n    # 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    # マージ結果を格納する一時配列 tmp を作成\n    tmp = [0] * (right - left + 1)\n    # 左右の部分配列の開始インデックスを初期化する\n    i, j, k = left, mid + 1, 0\n    # 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while i <= mid and j <= right:\n        if nums[i] <= nums[j]:\n            tmp[k] = nums[i]\n            i += 1\n        else:\n            tmp[k] = nums[j]\n            j += 1\n        k += 1\n    # 左右の部分配列の残り要素を一時配列にコピーする\n    while i <= mid:\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    while j <= right:\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    # 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for k in range(0, len(tmp)):\n        nums[left + k] = tmp[k]\n\ndef merge_sort(nums: list[int], left: int, right: int):\n    \"\"\"マージソート\"\"\"\n    # 終了条件\n    if left >= right:\n        return  # 部分配列の長さが 1 になったら再帰を終了\n    # 分割フェーズ\n    mid = (left + right) // 2 # 中点を計算\n    merge_sort(nums, left, mid)  # 左部分配列を再帰処理\n    merge_sort(nums, mid + 1, right)  # 右部分配列を再帰処理\n    # マージフェーズ\n    merge(nums, left, mid, right)\n
merge_sort.cpp
/* 左部分配列と右部分配列をマージ */\nvoid merge(vector<int> &nums, int left, int mid, int right) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    vector<int> tmp(right - left + 1);\n    // 左右の部分配列の開始インデックスを初期化する\n    int i = left, j = mid + 1, k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmp.size(); k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nvoid mergeSort(vector<int> &nums, int left, int right) {\n    // 終了条件\n    if (left >= right)\n        return; // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    int mid = left + (right - left) / 2;    // 中点を計算\n    mergeSort(nums, left, mid);      // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.java
/* 左部分配列と右部分配列をマージ */\nvoid merge(int[] nums, int left, int mid, int right) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    int[] tmp = new int[right - left + 1];\n    // 左右の部分配列の開始インデックスを初期化する\n    int i = left, j = mid + 1, k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nvoid mergeSort(int[] nums, int left, int right) {\n    // 終了条件\n    if (left >= right)\n        return; // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    int mid = left + (right - left) / 2; // 中点を計算\n    mergeSort(nums, left, mid); // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.cs
/* 左部分配列と右部分配列をマージ */\nvoid Merge(int[] nums, int left, int mid, int right) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    int[] tmp = new int[right - left + 1];\n    // 左右の部分配列の開始インデックスを初期化する\n    int i = left, j = mid + 1, k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmp.Length; ++k) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nvoid MergeSort(int[] nums, int left, int right) {\n    // 終了条件\n    if (left >= right) return;       // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    int mid = left + (right - left) / 2;    // 中点を計算\n    MergeSort(nums, left, mid);      // 左部分配列を再帰処理\n    MergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    Merge(nums, left, mid, right);\n}\n
merge_sort.go
/* 左部分配列と右部分配列をマージ */\nfunc merge(nums []int, left, mid, right int) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    tmp := make([]int, right-left+1)\n    // 左右の部分配列の開始インデックスを初期化する\n    i, j, k := left, mid+1, 0\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    for i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i++\n        } else {\n            tmp[k] = nums[j]\n            j++\n        }\n        k++\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    for i <= mid {\n        tmp[k] = nums[i]\n        i++\n        k++\n    }\n    for j <= right {\n        tmp[k] = nums[j]\n        j++\n        k++\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for k := 0; k < len(tmp); k++ {\n        nums[left+k] = tmp[k]\n    }\n}\n\n/* マージソート */\nfunc mergeSort(nums []int, left, right int) {\n    // 終了条件\n    if left >= right {\n        return\n    }\n    // 分割フェーズ\n    mid := left + (right - left) / 2\n    mergeSort(nums, left, mid)\n    mergeSort(nums, mid+1, right)\n    // マージフェーズ\n    merge(nums, left, mid, right)\n}\n
merge_sort.swift
/* 左部分配列と右部分配列をマージ */\nfunc merge(nums: inout [Int], left: Int, mid: Int, right: Int) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    var tmp = Array(repeating: 0, count: right - left + 1)\n    // 左右の部分配列の開始インデックスを初期化する\n    var i = left, j = mid + 1, k = 0\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while i <= mid, j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i += 1\n        } else {\n            tmp[k] = nums[j]\n            j += 1\n        }\n        k += 1\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while i <= mid {\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    }\n    while j <= right {\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for k in tmp.indices {\n        nums[left + k] = tmp[k]\n    }\n}\n\n/* マージソート */\nfunc mergeSort(nums: inout [Int], left: Int, right: Int) {\n    // 終了条件\n    if left >= right { // 部分配列の長さが 1 になったら再帰を終了\n        return\n    }\n    // 分割フェーズ\n    let mid = left + (right - left) / 2 // 中点を計算\n    mergeSort(nums: &nums, left: left, right: mid) // 左部分配列を再帰処理\n    mergeSort(nums: &nums, left: mid + 1, right: right) // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums: &nums, left: left, mid: mid, right: right)\n}\n
merge_sort.js
/* 左部分配列と右部分配列をマージ */\nfunction merge(nums, left, mid, right) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    const tmp = new Array(right - left + 1);\n    // 左右の部分配列の開始インデックスを初期化する\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nfunction mergeSort(nums, left, right) {\n    // 終了条件\n    if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    let mid = Math.floor(left + (right - left) / 2); // 中点を計算\n    mergeSort(nums, left, mid); // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.ts
/* 左部分配列と右部分配列をマージ */\nfunction merge(nums: number[], left: number, mid: number, right: number): void {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    const tmp = new Array(right - left + 1);\n    // 左右の部分配列の開始インデックスを初期化する\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nfunction mergeSort(nums: number[], left: number, right: number): void {\n    // 終了条件\n    if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    let mid = Math.floor(left + (right - left) / 2); // 中点を計算\n    mergeSort(nums, left, mid); // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.dart
/* 左部分配列と右部分配列をマージ */\nvoid merge(List<int> nums, int left, int mid, int right) {\n  // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n  // マージ結果を格納する一時配列 tmp を作成\n  List<int> tmp = List.filled(right - left + 1, 0);\n  // 左右の部分配列の開始インデックスを初期化する\n  int i = left, j = mid + 1, k = 0;\n  // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n  while (i <= mid && j <= right) {\n    if (nums[i] <= nums[j])\n      tmp[k++] = nums[i++];\n    else\n      tmp[k++] = nums[j++];\n  }\n  // 左右の部分配列の残り要素を一時配列にコピーする\n  while (i <= mid) {\n    tmp[k++] = nums[i++];\n  }\n  while (j <= right) {\n    tmp[k++] = nums[j++];\n  }\n  // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n  for (k = 0; k < tmp.length; k++) {\n    nums[left + k] = tmp[k];\n  }\n}\n\n/* マージソート */\nvoid mergeSort(List<int> nums, int left, int right) {\n  // 終了条件\n  if (left >= right) return; // 部分配列の長さが 1 になったら再帰を終了\n  // 分割フェーズ\n  int mid = left + (right - left) ~/ 2; // 中点を計算\n  mergeSort(nums, left, mid); // 左部分配列を再帰処理\n  mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n  // マージフェーズ\n  merge(nums, left, mid, right);\n}\n
merge_sort.rs
/* 左部分配列と右部分配列をマージ */\nfn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    let tmp_size = right - left + 1;\n    let mut tmp = vec![0; tmp_size];\n    // 左右の部分配列の開始インデックスを初期化する\n    let (mut i, mut j, mut k) = (left, mid + 1, 0);\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i];\n            i += 1;\n        } else {\n            tmp[k] = nums[j];\n            j += 1;\n        }\n        k += 1;\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while i <= mid {\n        tmp[k] = nums[i];\n        k += 1;\n        i += 1;\n    }\n    while j <= right {\n        tmp[k] = nums[j];\n        k += 1;\n        j += 1;\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for k in 0..tmp_size {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* マージソート */\nfn merge_sort(nums: &mut [i32], left: usize, right: usize) {\n    // 終了条件\n    if left >= right {\n        return; // 部分配列の長さが 1 になったら再帰を終了\n    }\n\n    // 分割フェーズ\n    let mid = left + (right - left) / 2; // 中点を計算\n    merge_sort(nums, left, mid); // 左部分配列を再帰処理\n    merge_sort(nums, mid + 1, right); // 右部分配列を再帰処理\n\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.c
/* 左部分配列と右部分配列をマージ */\nvoid merge(int *nums, int left, int mid, int right) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    int tmpSize = right - left + 1;\n    int *tmp = (int *)malloc(tmpSize * sizeof(int));\n    // 左右の部分配列の開始インデックスを初期化する\n    int i = left, j = mid + 1, k = 0;\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (k = 0; k < tmpSize; ++k) {\n        nums[left + k] = tmp[k];\n    }\n    // メモリを解放する\n    free(tmp);\n}\n\n/* マージソート */\nvoid mergeSort(int *nums, int left, int right) {\n    // 終了条件\n    if (left >= right)\n        return; // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    int mid = left + (right - left) / 2;    // 中点を計算\n    mergeSort(nums, left, mid);      // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right); // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right);\n}\n
merge_sort.kt
/* 左部分配列と右部分配列をマージ */\nfun merge(nums: IntArray, left: Int, mid: Int, right: Int) {\n    // 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n    // マージ結果を格納する一時配列 tmp を作成\n    val tmp = IntArray(right - left + 1)\n    // 左右の部分配列の開始インデックスを初期化する\n    var i = left\n    var j = mid + 1\n    var k = 0\n    // 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++]\n        else\n            tmp[k++] = nums[j++]\n    }\n    // 左右の部分配列の残り要素を一時配列にコピーする\n    while (i <= mid) {\n        tmp[k++] = nums[i++]\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++]\n    }\n    // 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n    for (l in tmp.indices) {\n        nums[left + l] = tmp[l]\n    }\n}\n\n/* マージソート */\nfun mergeSort(nums: IntArray, left: Int, right: Int) {\n    // 終了条件\n    if (left >= right) return  // 部分配列の長さが 1 になったら再帰を終了\n    // 分割フェーズ\n    val mid = left + (right - left) / 2 // 中点を計算\n    mergeSort(nums, left, mid) // 左部分配列を再帰処理\n    mergeSort(nums, mid + 1, right) // 右部分配列を再帰処理\n    // マージフェーズ\n    merge(nums, left, mid, right)\n}\n
merge_sort.rb
### 左部分配列と右部分配列をマージ ###\ndef merge(nums, left, mid, right)\n  # 左部分配列の区間は [left, mid]、右部分配列の区間は [mid+1, right]\n  # マージ結果を格納する一時配列 tmp を作成\n  tmp = Array.new(right - left + 1, 0)\n  # 左右の部分配列の開始インデックスを初期化する\n  i, j, k = left, mid + 1, 0\n  # 左右の部分配列にまだ要素がある間は比較し、小さいほうを一時配列にコピーする\n  while i <= mid && j <= right\n    if nums[i] <= nums[j]\n      tmp[k] = nums[i]\n      i += 1\n    else\n      tmp[k] = nums[j]\n      j += 1\n    end\n    k += 1\n  end\n  # 左右の部分配列の残り要素を一時配列にコピーする\n  while i <= mid\n    tmp[k] = nums[i]\n    i += 1\n    k += 1\n  end\n  while j <= right\n    tmp[k] = nums[j]\n    j += 1\n    k += 1\n  end\n  # 一時配列 tmp の要素を元の配列 nums の対応区間にコピーする\n  (0...tmp.length).each do |k|\n    nums[left + k] = tmp[k]\n  end\nend\n\n### マージソート ###\ndef merge_sort(nums, left, right)\n  # 終了条件\n  # 部分配列の長さが 1 になったら再帰を終了する\n  return if left >= right\n  # 分割フェーズ\n  mid = left + (right - left) / 2 # 中点を計算\n  merge_sort(nums, left, mid) # 左部分配列を再帰処理\n  merge_sort(nums, mid + 1, right) # 右部分配列を再帰処理\n  # マージフェーズ\n  merge(nums, left, mid, right)\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1162","level":2,"title":"11.6.2   アルゴリズムの特性","text":"
  • 時間計算量は \\(O(n \\log n)\\)、非適応型ソート:分割によって高さ \\(\\log n\\) の再帰木が生成され、各層でのマージ操作の総数は \\(n\\) であるため、全体の時間計算量は \\(O(n \\log n)\\) です。
  • 空間計算量は \\(O(n)\\)、インプレースではないソート:再帰の深さは \\(\\log n\\) であり、サイズ \\(O(\\log n)\\) のスタックフレーム領域を使用します。マージ操作は補助配列を用いて実装する必要があり、サイズ \\(O(n)\\) の追加領域を使用します。
  • 安定ソート:マージの過程では、等しい要素の順序は変化しません。
","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1163","level":2,"title":"11.6.3   連結リストのソート","text":"

連結リストに対しては、マージソートは他のソートアルゴリズムと比べて顕著な利点があり、連結リストのソート問題の空間計算量を \\(O(1)\\) まで最適化できます 。

  • 分割段階:連結リストの分割は「再帰」の代わりに「反復」で実装できるため、再帰で使用するスタックフレーム領域を省けます。
  • マージ段階:連結リストでは、ノードの追加や削除は参照(ポインタ)を変更するだけで実現できるため、マージ段階(2 つの短いソート済み連結リストを 1 つの長いソート済み連結リストにマージすること)では追加の連結リストを作成する必要がありません。

具体的な実装の詳細は比較的複雑なので、興味のある読者は関連資料を参照して学習してください。

","path":["第 11 章   ソート","11.6   マージソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   クイックソート","text":"

クイックソート(quick sort)は分割統治戦略に基づくソートアルゴリズムであり、実行効率が高く、広く利用されています。

クイックソートの中核操作は「パーティション」であり、その目的は、配列内のある要素を「基準数」として選び、基準数より小さいすべての要素を左側へ、大きい要素を右側へ移動することです。具体的には、パーティションの流れを下図に示します。

  1. 配列の最左端の要素を基準数として選び、2 つのポインタ ij を初期化して、それぞれ配列の両端を指すようにします。
  2. ループを設定し、各ラウンドで ij)を使ってそれぞれ基準数より大きい(小さい)最初の要素を探し、その後この 2 つの要素を交換します。
  3. ij が出会うまでステップ 2. を繰り返し、最後に基準数を 2 つの部分配列の境界へ交換します。
<1><2><3><4><5><6><7><8><9>

図 11-8   パーティションの手順

パーティションが完了すると、元の配列は左部分配列、基準数、右部分配列の 3 つに分けられ、「左部分配列の任意の要素 \\(\\leq\\) 基準数 \\(\\leq\\) 右部分配列の任意の要素」を満たします。したがって、次はこの 2 つの部分配列だけをソートすれば済みます。

クイックソートの分割統治戦略

パーティションの本質は、長い配列のソート問題を 2 つの短い配列のソート問題へ簡略化することです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"番兵分割\"\"\"\n    # nums[left] を基準値とする\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # 右から左へ基準値未満の最初の要素を探す\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # 左から右へ基準値より大きい最初の要素を探す\n        # 要素の交換\n        nums[i], nums[j] = nums[j], nums[i]\n    # 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # 基準値のインデックスを返す\n
quick_sort.cpp
/* 番兵分割 */\nint partition(vector<int> &nums, int left, int right) {\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;                // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums[i], nums[j]); // この 2 つの要素を交換\n    }\n    swap(nums[i], nums[left]);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;                   // 基準値のインデックスを返す\n}\n
quick_sort.java
/* 要素の交換 */\nvoid swap(int[] nums, int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 番兵分割 */\nint partition(int[] nums, int left, int right) {\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums, i, j); // この 2 つの要素を交換\n    }\n    swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
quick_sort.cs
/* 要素の交換 */\nvoid Swap(int[] nums, int i, int j) {\n    (nums[j], nums[i]) = (nums[i], nums[j]);\n}\n\n/* 番兵分割 */\nint Partition(int[] nums, int left, int right) {\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 左から右へ基準値より大きい最初の要素を探す\n        Swap(nums, i, j); // この 2 つの要素を交換\n    }\n    Swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
quick_sort.go
/* 番兵分割 */\nfunc (q *quickSort) partition(nums []int, left, right int) int {\n    // nums[left] を基準値とする\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // 右から左へ基準値未満の最初の要素を探す\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // 左から右へ基準値より大きい最初の要素を探す\n        }\n        // 要素の交換\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // 基準値のインデックスを返す\n}\n
quick_sort.swift
/* 番兵分割 */\nfunc partition(nums: inout [Int], left: Int, right: Int) -> Int {\n    // nums[left] を基準値とする\n    var i = left\n    var j = right\n    while i < j {\n        while i < j, nums[j] >= nums[left] {\n            j -= 1 // 右から左へ基準値未満の最初の要素を探す\n        }\n        while i < j, nums[i] <= nums[left] {\n            i += 1 // 左から右へ基準値より大きい最初の要素を探す\n        }\n        nums.swapAt(i, j) // この 2 つの要素を交換\n    }\n    nums.swapAt(i, left) // 基準値を 2 つの部分配列の境界へ交換する\n    return i // 基準値のインデックスを返す\n}\n
quick_sort.js
/* 要素の交換 */\nswap(nums, i, j) {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 番兵分割 */\npartition(nums, left, right) {\n    // nums[left] を基準値とする\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        // 要素の交換\n        this.swap(nums, i, j); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
quick_sort.ts
/* 要素の交換 */\nswap(nums: number[], i: number, j: number): void {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 番兵分割 */\npartition(nums: number[], left: number, right: number): number {\n    // nums[left] を基準値とする\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        // 要素の交換\n        this.swap(nums, i, j); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
quick_sort.dart
/* 要素の交換 */\nvoid _swap(List<int> nums, int i, int j) {\n  int tmp = nums[i];\n  nums[i] = nums[j];\n  nums[j] = tmp;\n}\n\n/* 番兵分割 */\nint _partition(List<int> nums, int left, int right) {\n  // nums[left] を基準値とする\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // 右から左へ基準値未満の最初の要素を探す\n    while (i < j && nums[i] <= nums[left]) i++; // 左から右へ基準値より大きい最初の要素を探す\n    _swap(nums, i, j); // この 2 つの要素を交換\n  }\n  _swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n  return i; // 基準値のインデックスを返す\n}\n
quick_sort.rs
/* 番兵分割 */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // nums[left] を基準値とする\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        nums.swap(i, j); // この 2 つの要素を交換\n    }\n    nums.swap(i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    i // 基準値のインデックスを返す\n}\n
quick_sort.c
/* 要素の交換 */\nvoid swap(int nums[], int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 番兵分割 */\nint partition(int nums[], int left, int right) {\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        // この 2 つの要素を交換\n        swap(nums, i, j);\n    }\n    // 基準値を 2 つの部分配列の境界へ交換する\n    swap(nums, i, left);\n    // 基準値のインデックスを返す\n    return i;\n}\n
quick_sort.kt
/* 要素の交換 */\nfun swap(nums: IntArray, i: Int, j: Int) {\n    val temp = nums[i]\n    nums[i] = nums[j]\n    nums[j] = temp\n}\n\n/* 番兵分割 */\nfun partition(nums: IntArray, left: Int, right: Int): Int {\n    // nums[left] を基準値とする\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--           // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++           // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums, i, j)  // この 2 つの要素を交換\n    }\n    swap(nums, i, left)   // 基準値を 2 つの部分配列の境界へ交換する\n    return i              // 基準値のインデックスを返す\n}\n
quick_sort.rb
### 番兵分割 ###\ndef partition(nums, left, right)\n  # nums[left] を基準値とする\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # 右から左へ基準値未満の最初の要素を探す\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # 左から右へ基準値より大きい最初の要素を探す\n    end\n    # 要素の交換\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # 基準値を 2 つの部分配列の境界へ交換する\n  nums[i], nums[left] = nums[left], nums[i]\n  i # 基準値のインデックスを返す\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1151","level":2,"title":"11.5.1   アルゴリズムの流れ","text":"

クイックソート全体の流れを下図に示します。

  1. まず、元の配列に対して 1 回「パーティション」を実行し、未ソートの左部分配列と右部分配列を得ます。
  2. 次に、左部分配列と右部分配列に対してそれぞれ再帰的に「パーティション」を実行します。
  3. 部分配列の長さが 1 になるまで再帰を続け、配列全体のソートを完了します。

図 11-9   クイックソートの流れ

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"クイックソート\"\"\"\n    # 部分配列の長さが 1 なら再帰を終了する\n    if left >= right:\n        return\n    # 番兵分割\n    pivot = self.partition(nums, left, right)\n    # 左右の部分配列を再帰処理\n    self.quick_sort(nums, left, pivot - 1)\n    self.quick_sort(nums, pivot + 1, right)\n
quick_sort.cpp
/* クイックソート */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right)\n        return;\n    // 番兵分割\n    int pivot = partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.java
/* クイックソート */\nvoid quickSort(int[] nums, int left, int right) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right)\n        return;\n    // 番兵分割\n    int pivot = partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.cs
/* クイックソート */\nvoid QuickSort(int[] nums, int left, int right) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right)\n        return;\n    // 番兵分割\n    int pivot = Partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    QuickSort(nums, left, pivot - 1);\n    QuickSort(nums, pivot + 1, right);\n}\n
quick_sort.go
/* クイックソート */\nfunc (q *quickSort) quickSort(nums []int, left, right int) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if left >= right {\n        return\n    }\n    // 番兵分割\n    pivot := q.partition(nums, left, right)\n    // 左右の部分配列を再帰処理\n    q.quickSort(nums, left, pivot-1)\n    q.quickSort(nums, pivot+1, right)\n}\n
quick_sort.swift
/* クイックソート */\nfunc quickSort(nums: inout [Int], left: Int, right: Int) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if left >= right {\n        return\n    }\n    // 番兵分割\n    let pivot = partition(nums: &nums, left: left, right: right)\n    // 左右の部分配列を再帰処理\n    quickSort(nums: &nums, left: left, right: pivot - 1)\n    quickSort(nums: &nums, left: pivot + 1, right: right)\n}\n
quick_sort.js
/* クイックソート */\nquickSort(nums, left, right) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right) return;\n    // 番兵分割\n    const pivot = this.partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
quick_sort.ts
/* クイックソート */\nquickSort(nums: number[], left: number, right: number): void {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right) {\n        return;\n    }\n    // 番兵分割\n    const pivot = this.partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
quick_sort.dart
/* クイックソート */\nvoid quickSort(List<int> nums, int left, int right) {\n  // 部分配列の長さが 1 なら再帰を終了する\n  if (left >= right) return;\n  // 番兵分割\n  int pivot = _partition(nums, left, right);\n  // 左右の部分配列を再帰処理\n  quickSort(nums, left, pivot - 1);\n  quickSort(nums, pivot + 1, right);\n}\n
quick_sort.rs
/* クイックソート */\npub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if left >= right {\n        return;\n    }\n    // 番兵分割\n    let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n    // 左右の部分配列を再帰処理\n    Self::quick_sort(left, pivot - 1, nums);\n    Self::quick_sort(pivot + 1, right, nums);\n}\n
quick_sort.c
/* クイックソート */\nvoid quickSort(int nums[], int left, int right) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right) {\n        return;\n    }\n    // 番兵分割\n    int pivot = partition(nums, left, right);\n    // 左右の部分配列を再帰処理\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.kt
/* クイックソート */\nfun quickSort(nums: IntArray, left: Int, right: Int) {\n    // 部分配列の長さが 1 なら再帰を終了する\n    if (left >= right) return\n    // 番兵分割\n    val pivot = partition(nums, left, right)\n    // 左右の部分配列を再帰処理\n    quickSort(nums, left, pivot - 1)\n    quickSort(nums, pivot + 1, right)\n}\n
quick_sort.rb
### クイックソートクラス ###\ndef quick_sort(nums, left, right)\n  # 部分配列の長さが 1 でない場合は再帰する\n  if left < right\n    # 番兵分割\n    pivot = partition(nums, left, right)\n    # 左右の部分配列を再帰処理\n    quick_sort(nums, left, pivot - 1)\n    quick_sort(nums, pivot + 1, right)\n  end\n  nums\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1152","level":2,"title":"11.5.2   アルゴリズムの特性","text":"
  • 時間計算量は \\(O(n \\log n)\\)、非適応型ソート:平均的な場合、パーティションの再帰の深さは \\(\\log n\\) で、各層の総ループ回数は \\(n\\) のため、全体で \\(O(n \\log n)\\) 時間を要します。最悪の場合、各回のパーティション操作で長さ \\(n\\) の配列が長さ \\(0\\) と \\(n - 1\\) の 2 つの部分配列に分割され、このとき再帰の深さは \\(n\\) に達し、各層のループ回数は \\(n\\) となるため、全体で \\(O(n^2)\\) 時間を要します。
  • 空間計算量は \\(O(n)\\)、インプレースソート:入力配列が完全な逆順の場合、最悪の再帰深さ \\(n\\) に達し、\\(O(n)\\) のスタックフレーム空間を使用します。ソート操作は元の配列上で行われ、追加の配列は用いません。
  • 非安定ソート:パーティションの最後のステップで、基準数が等しい要素の右側へ交換される可能性があります。
","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1153","level":2,"title":"11.5.3   クイックソートが速い理由","text":"

名前からも分かるように、クイックソートは効率面で一定の優位性を持っています。クイックソートの平均時間計算量は「マージソート」や「ヒープソート」と同じですが、通常はクイックソートのほうが高効率であり、主な理由は次のとおりです。

  • 最悪ケースが起こる確率が低い:クイックソートの最悪時間計算量は \\(O(n^2)\\) で、「マージソート」ほど安定ではありませんが、大半のケースでは \\(O(n \\log n)\\) の時間計算量で動作します。
  • キャッシュ利用効率が高い:パーティション操作の実行時には、システムが部分配列全体をキャッシュに読み込めるため、要素アクセスの効率が高くなります。一方、「ヒープソート」のようなアルゴリズムは要素へ飛び飛びにアクセスする必要があり、この性質を持ちません。
  • 計算量の定数係数が小さい:上記 3 つのアルゴリズムの中で、クイックソートは比較、代入、交換などの操作総数が最も少なくなります。これは「挿入ソート」が「バブルソート」より速い理由と似ています。
","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154","level":2,"title":"11.5.4   基準数の最適化","text":"

クイックソートは、入力によっては時間効率が低下する可能性があります。極端な例として、入力配列が完全な逆順であるとします。最左端の要素を基準数として選ぶため、パーティション完了後には基準数が配列の最右端へ交換され、左部分配列の長さが \\(n - 1\\)、右部分配列の長さが \\(0\\) になります。この再帰を続けると、各回のパーティション後に必ず一方の部分配列の長さが \\(0\\) となり、分割統治戦略が機能せず、クイックソートは「バブルソート」に近い形へ退化します。

この状況をできるだけ避けるため、パーティションにおける基準数の選び方を最適化できます。たとえば、ランダムに 1 つの要素を選んで基準数にできます。しかし、運が悪く毎回望ましくない基準数を選んでしまうと、効率は依然として十分ではありません。

注意すべき点として、プログラミング言語が通常生成するのは「疑似乱数」です。疑似乱数列に合わせて特定のテストケースを構築すると、クイックソートの効率はやはり劣化する可能性があります。

さらに改善するために、配列から 3 つの候補要素(通常は先頭、末尾、中間の要素)を選び、**その 3 つの候補要素の中央値を基準数とする**ことができます。こうすると、基準数が「小さすぎず大きすぎもしない」確率が大幅に上がります。もちろん、候補要素をさらに増やして、アルゴリズムの頑健性をいっそう高めることも可能です。この方法を採用すると、時間計算量が \\(O(n^2)\\) まで劣化する確率は大きく下がります。

コード例を以下に示します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n    \"\"\"3つの候補要素の中央値を選ぶ\"\"\"\n    l, m, r = nums[left], nums[mid], nums[right]\n    if (l <= m <= r) or (r <= m <= l):\n        return mid  # m は l と r の間\n    if (m <= l <= r) or (r <= l <= m):\n        return left  # l は m と r の間\n    return right\n\ndef partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"番兵による分割処理(3 点中央値)\"\"\"\n    # nums[left] を基準値とする\n    med = self.median_three(nums, left, (left + right) // 2, right)\n    # 中央値を配列の最左端に交換する\n    nums[left], nums[med] = nums[med], nums[left]\n    # nums[left] を基準値とする\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # 右から左へ基準値未満の最初の要素を探す\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # 左から右へ基準値より大きい最初の要素を探す\n        # 要素の交換\n        nums[i], nums[j] = nums[j], nums[i]\n    # 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # 基準値のインデックスを返す\n
quick_sort.cpp
/* 3つの候補要素の中央値を選ぶ */\nint medianThree(vector<int> &nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m は l と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l は m と r の間\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\nint partition(vector<int> &nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // 中央値を配列の最左端に交換する\n    swap(nums[left], nums[med]);\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;                // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums[i], nums[j]); // この 2 つの要素を交換\n    }\n    swap(nums[i], nums[left]);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;                   // 基準値のインデックスを返す\n}\n
quick_sort.java
/* 3つの候補要素の中央値を選ぶ */\nint medianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m は l と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l は m と r の間\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\nint partition(int[] nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // 中央値を配列の最左端に交換する\n    swap(nums, left, med);\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums, i, j); // この 2 つの要素を交換\n    }\n    swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
quick_sort.cs
/* 3つの候補要素の中央値を選ぶ */\nint MedianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m は l と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l は m と r の間\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\nint Partition(int[] nums, int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\n    int med = MedianThree(nums, left, (left + right) / 2, right);\n    // 中央値を配列の最左端に交換する\n    Swap(nums, left, med);\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 左から右へ基準値より大きい最初の要素を探す\n        Swap(nums, i, j); // この 2 つの要素を交換\n    }\n    Swap(nums, i, left);  // 基準値を 2 つの部分配列の境界へ交換する\n    return i;             // 基準値のインデックスを返す\n}\n
quick_sort.go
/* 3つの候補要素の中央値を選ぶ */\nfunc (q *quickSortMedian) medianThree(nums []int, left, mid, right int) int {\n    l, m, r := nums[left], nums[mid], nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m は l と r の間\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l は m と r の間\n    }\n    return right\n}\n\n/* 番兵による分割処理(3 点中央値) */\nfunc (q *quickSortMedian) partition(nums []int, left, right int) int {\n    // nums[left] を基準値とする\n    med := q.medianThree(nums, left, (left+right)/2, right)\n    // 中央値を配列の最左端に交換する\n    nums[left], nums[med] = nums[med], nums[left]\n    // nums[left] を基準値とする\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // 右から左へ基準値未満の最初の要素を探す\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // 左から右へ基準値より大きい最初の要素を探す\n        }\n        // 要素の交換\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // 基準値を 2 つの部分配列の境界へ交換する\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // 基準値のインデックスを返す\n}\n
quick_sort.swift
/* 3つの候補要素の中央値を選ぶ */\nfunc medianThree(nums: [Int], left: Int, mid: Int, right: Int) -> Int {\n    let l = nums[left]\n    let m = nums[mid]\n    let r = nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m は l と r の間\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l は m と r の間\n    }\n    return right\n}\n\n/* 番兵による分割処理(3 点中央値) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int {\n    // 3つの候補要素の中央値を選ぶ\n    let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right)\n    // 中央値を配列の最左端に交換する\n    nums.swapAt(left, med)\n    return partition(nums: &nums, left: left, right: right)\n}\n
quick_sort.js
/* 3つの候補要素の中央値を選ぶ */\nmedianThree(nums, left, mid, right) {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m は l と r の間\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l は m と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\npartition(nums, left, right) {\n    // 3つの候補要素の中央値を選ぶ\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // 中央値を配列の最左端に交換する\n    this.swap(nums, left, med);\n    // nums[left] を基準値とする\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) j--; // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left]) i++; // 左から右へ基準値より大きい最初の要素を探す\n        this.swap(nums, i, j); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
quick_sort.ts
/* 3つの候補要素の中央値を選ぶ */\nmedianThree(\n    nums: number[],\n    left: number,\n    mid: number,\n    right: number\n): number {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m は l と r の間\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l は m と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\npartition(nums: number[], left: number, right: number): number {\n    // 3つの候補要素の中央値を選ぶ\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // 中央値を配列の最左端に交換する\n    this.swap(nums, left, med);\n    // nums[left] を基準値とする\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        this.swap(nums, i, j); // この 2 つの要素を交換\n    }\n    this.swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i; // 基準値のインデックスを返す\n}\n
quick_sort.dart
/* 3つの候補要素の中央値を選ぶ */\nint _medianThree(List<int> nums, int left, int mid, int right) {\n  int l = nums[left], m = nums[mid], r = nums[right];\n  if ((l <= m && m <= r) || (r <= m && m <= l))\n    return mid; // m は l と r の間\n  if ((m <= l && l <= r) || (r <= l && l <= m))\n    return left; // l は m と r の間\n  return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\nint _partition(List<int> nums, int left, int right) {\n  // 3つの候補要素の中央値を選ぶ\n  int med = _medianThree(nums, left, (left + right) ~/ 2, right);\n  // 中央値を配列の最左端に交換する\n  _swap(nums, left, med);\n  // nums[left] を基準値とする\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // 右から左へ基準値未満の最初の要素を探す\n    while (i < j && nums[i] <= nums[left]) i++; // 左から右へ基準値より大きい最初の要素を探す\n    _swap(nums, i, j); // この 2 つの要素を交換\n  }\n  _swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n  return i; // 基準値のインデックスを返す\n}\n
quick_sort.rs
/* 3つの候補要素の中央値を選ぶ */\nfn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize {\n    let (l, m, r) = (nums[left], nums[mid], nums[right]);\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid; // m は l と r の間\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left; // l は m と r の間\n    }\n    right\n}\n\n/* 番兵による分割処理(3 点中央値) */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // 3つの候補要素の中央値を選ぶ\n    let med = Self::median_three(nums, left, (left + right) / 2, right);\n    // 中央値を配列の最左端に交換する\n    nums.swap(left, med);\n    // nums[left] を基準値とする\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // 右から左へ基準値未満の最初の要素を探す\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // 左から右へ基準値より大きい最初の要素を探す\n        }\n        nums.swap(i, j); // この 2 つの要素を交換\n    }\n    nums.swap(i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    i // 基準値のインデックスを返す\n}\n
quick_sort.c
/* 3つの候補要素の中央値を選ぶ */\nint medianThree(int nums[], int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m は l と r の間\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l は m と r の間\n    return right;\n}\n\n/* 番兵による分割処理(3 点中央値) */\nint partitionMedian(int nums[], int left, int right) {\n    // 3つの候補要素の中央値を選ぶ\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // 中央値を配列の最左端に交換する\n    swap(nums, left, med);\n    // nums[left] を基準値とする\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--; // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums, i, j); // この 2 つの要素を交換\n    }\n    swap(nums, i, left); // 基準値を 2 つの部分配列の境界へ交換する\n    return i;            // 基準値のインデックスを返す\n}\n
quick_sort.kt
/* 3つの候補要素の中央値を選ぶ */\nfun medianThree(nums: IntArray, left: Int, mid: Int, right: Int): Int {\n    val l = nums[left]\n    val m = nums[mid]\n    val r = nums[right]\n    if ((m in l..r) || (m in r..l))\n        return mid  // m は l と r の間\n    if ((l in m..r) || (l in r..m))\n        return left // l は m と r の間\n    return right\n}\n\n/* 番兵による分割処理(3 点中央値) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n    // 3つの候補要素の中央値を選ぶ\n    val med = medianThree(nums, left, (left + right) / 2, right)\n    // 中央値を配列の最左端に交換する\n    swap(nums, left, med)\n    // nums[left] を基準値とする\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--                      // 右から左へ基準値未満の最初の要素を探す\n        while (i < j && nums[i] <= nums[left])\n            i++                      // 左から右へ基準値より大きい最初の要素を探す\n        swap(nums, i, j)             // この 2 つの要素を交換\n    }\n    swap(nums, i, left)              // 基準値を 2 つの部分配列の境界へ交換する\n    return i                         // 基準値のインデックスを返す\n}\n
quick_sort.rb
### 3 つの候補要素の中央値を選ぶ ###\ndef median_three(nums, left, mid, right)\n  # 3つの候補要素の中央値を選ぶ\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m は l と r の間\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l は m と r の間\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n### 3 つの候補要素の中央値を選ぶ ###\ndef median_three(nums, left, mid, right)\n  # 3つの候補要素の中央値を選ぶ\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m は l と r の間\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l は m と r の間\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n# ## 番兵分割(三数中央値)###\ndef partition(nums, left, right)\n  # ## nums[left] を基準値とする\n  med = median_three(nums, left, (left + right) / 2, right)\n  # 中央値を配列の最左端に交換する\n  nums[left], nums[med] = nums[med], nums[left]\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # 右から左へ基準値未満の最初の要素を探す\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # 左から右へ基準値より大きい最初の要素を探す\n    end\n    # 要素の交換\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # 基準値を 2 つの部分配列の境界へ交換する\n  nums[i], nums[left] = nums[left], nums[i]\n  i # 基準値のインデックスを返す\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1155","level":2,"title":"11.5.5   再帰の深さの最適化","text":"

一部の入力では、クイックソートは多くの空間を消費する可能性があります。完全に整列済みの入力配列を例にとり、再帰中の部分配列の長さを \\(m\\) とします。各回のパーティション操作では長さ \\(0\\) の左部分配列と長さ \\(m - 1\\) の右部分配列が生成されます。これは、各再帰呼び出しで減る問題サイズが非常に小さいこと(要素が 1 つ減るだけ)を意味し、再帰木の高さは \\(n - 1\\) に達するため、このとき \\(O(n)\\) のスタックフレーム空間を占有します。

スタックフレーム空間の蓄積を防ぐために、各回のパーティション完了後に 2 つの部分配列の長さを比較し、**短いほうの部分配列に対してのみ再帰**を行えます。短い部分配列の長さは \\(n / 2\\) を超えないため、この方法なら再帰の深さを \\(\\log n\\) 以下に抑えられ、最悪時の空間計算量を \\(O(\\log n)\\) まで最適化できます。コードを以下に示します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"クイックソート(再帰深度最適化)\"\"\"\n    # 部分配列の長さが 1 なら終了\n    while left < right:\n        # 番兵による分割処理\n        pivot = self.partition(nums, left, right)\n        # 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if pivot - left < right - pivot:\n            self.quick_sort(nums, left, pivot - 1)  # 左部分配列を再帰的にソート\n            left = pivot + 1  # 未ソート区間の残りは [pivot + 1, right]\n        else:\n            self.quick_sort(nums, pivot + 1, right)  # 右部分配列を再帰的にソート\n            right = pivot - 1  # 未ソート区間の残りは [left, pivot - 1]\n
quick_sort.cpp
/* クイックソート(再帰深度最適化) */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        int pivot = partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート\n            left = pivot + 1;                 // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n            right = pivot - 1;                 // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.java
/* クイックソート(再帰深度最適化) */\nvoid quickSort(int[] nums, int left, int right) {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        int pivot = partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート\n            left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n            right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.cs
/* クイックソート(再帰深度最適化) */\nvoid QuickSort(int[] nums, int left, int right) {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        int pivot = Partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            QuickSort(nums, left, pivot - 1);  // 左部分配列を再帰的にソート\n            left = pivot + 1;  // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            QuickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n            right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.go
/* クイックソート(再帰深度最適化) */\nfunc (q *quickSortTailCall) quickSort(nums []int, left, right int) {\n    // 部分配列の長さが 1 なら終了\n    for left < right {\n        // 番兵による分割処理\n        pivot := q.partition(nums, left, right)\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if pivot-left < right-pivot {\n            q.quickSort(nums, left, pivot-1) // 左部分配列を再帰的にソート\n            left = pivot + 1                 // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            q.quickSort(nums, pivot+1, right) // 右部分配列を再帰的にソート\n            right = pivot - 1                 // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.swift
/* クイックソート(再帰深度最適化) */\nfunc quickSortTailCall(nums: inout [Int], left: Int, right: Int) {\n    var left = left\n    var right = right\n    // 部分配列の長さが 1 なら終了\n    while left < right {\n        // 番兵による分割処理\n        let pivot = partition(nums: &nums, left: left, right: right)\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left) < (right - pivot) {\n            quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // 左部分配列を再帰的にソート\n            left = pivot + 1 // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // 右部分配列を再帰的にソート\n            right = pivot - 1 // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.js
/* クイックソート(再帰深度最適化) */\nquickSort(nums, left, right) {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        let pivot = this.partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート\n            left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n            right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.ts
/* クイックソート(再帰深度最適化) */\nquickSort(nums: number[], left: number, right: number): void {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        let pivot = this.partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート\n            left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n            right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.dart
/* クイックソート(再帰深度最適化) */\nvoid quickSort(List<int> nums, int left, int right) {\n  // 部分配列の長さが 1 なら終了\n  while (left < right) {\n    // 番兵による分割処理\n    int pivot = _partition(nums, left, right);\n    // 2 つの部分配列のうち短いほうにクイックソートを適用する\n    if (pivot - left < right - pivot) {\n      quickSort(nums, left, pivot - 1); // 左部分配列を再帰的にソート\n      left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right]\n    } else {\n      quickSort(nums, pivot + 1, right); // 右部分配列を再帰的にソート\n      right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n    }\n  }\n}\n
quick_sort.rs
/* クイックソート(再帰深度最適化) */\npub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {\n    // 部分配列の長さが 1 なら終了\n    while left < right {\n        // 番兵による分割処理\n        let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if pivot - left < right - pivot {\n            Self::quick_sort(left, pivot - 1, nums); // 左部分配列を再帰的にソート\n            left = pivot + 1; // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            Self::quick_sort(pivot + 1, right, nums); // 右部分配列を再帰的にソート\n            right = pivot - 1; // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.c
/* クイックソート(再帰深度最適化) */\nvoid quickSortTailCall(int nums[], int left, int right) {\n    // 部分配列の長さが 1 なら終了\n    while (left < right) {\n        // 番兵による分割処理\n        int pivot = partition(nums, left, right);\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - left < right - pivot) {\n            // 左部分配列を再帰的にソート\n            quickSortTailCall(nums, left, pivot - 1);\n            // 未ソート区間の残りは [pivot + 1, right]\n            left = pivot + 1;\n        } else {\n            // 右部分配列を再帰的にソート\n            quickSortTailCall(nums, pivot + 1, right);\n            // 未ソート区間の残りは [left, pivot - 1]\n            right = pivot - 1;\n        }\n    }\n}\n
quick_sort.kt
/* クイックソート(再帰深度最適化) */\nfun quickSortTailCall(nums: IntArray, left: Int, right: Int) {\n    // 部分配列の長さが 1 なら終了\n    var l = left\n    var r = right\n    while (l < r) {\n        // 番兵による分割処理\n        val pivot = partition(nums, l, r)\n        // 2 つの部分配列のうち短いほうにクイックソートを適用する\n        if (pivot - l < r - pivot) {\n            quickSort(nums, l, pivot - 1) // 左部分配列を再帰的にソート\n            l = pivot + 1 // 未ソート区間の残りは [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, r) // 右部分配列を再帰的にソート\n            r = pivot - 1 // 未ソート区間の残りは [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.rb
### 番兵分割 ###\ndef partition(nums, left, right)\n  # nums[left] を基準値とする\n  i = left\n  j = right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # 右から左へ基準値未満の最初の要素を探す\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # 左から右へ基準値より大きい最初の要素を探す\n    end\n    # 要素の交換\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # 基準値を 2 つの部分配列の境界へ交換する\n  nums[i], nums[left] = nums[left], nums[i]\n  i # 基準値のインデックスを返す\nend\n\n# ## クイックソート(再帰深度最適化)###\ndef quick_sort(nums, left, right)\n  # 部分配列の長さが 1 でない場合は再帰する\n  while left < right\n    # 番兵分割\n    pivot = partition(nums, left, right)\n    # 2 つの部分配列のうち短いほうにクイックソートを適用する\n    if pivot - left < right - pivot\n      quick_sort(nums, left, pivot - 1)\n      left = pivot + 1 # 未ソート区間の残りは [pivot + 1, right]\n    else\n      quick_sort(nums, pivot + 1, right)\n      right = pivot - 1 # 未ソート区間の残りは [left, pivot - 1]\n    end\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.5   クイックソート"],"tags":[]},{"location":"chapter_sorting/radix_sort/","level":1,"title":"11.10   基数ソート","text":"

前節では計数ソートを紹介しました。これは、データ量 \\(n\\) が大きく、データ範囲 \\(m\\) が小さい場合に適しています。\\(n = 10^6\\) 個の学籍番号をソートすると仮定すると、学籍番号は \\(8\\) 桁の数字なので、データ範囲 \\(m = 10^8\\) は非常に大きくなります。計数ソートでは大量のメモリ空間を確保する必要がありますが、基数ソートではこの問題を回避できます。

基数ソート(radix sort)の基本的な考え方は計数ソートと同じで、個数を数えることによってソートを実現します。そのうえで、基数ソートは各桁の段階的な関係を利用し、各桁を順にソートすることで、最終的なソート結果を得ます。

","path":["第 11 章   ソート","11.10   基数ソート"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11101","level":2,"title":"11.10.1   アルゴリズムの流れ","text":"

学籍番号データを例にすると、数字の最下位桁を第 \\(1\\) 位、最上位桁を第 \\(8\\) 位としたとき、基数ソートの流れは次図のようになります。

  1. 桁番号 \\(k = 1\\) を初期化します。
  2. 学籍番号の第 \\(k\\) 位に対して「計数ソート」を実行します。完了すると、データは第 \\(k\\) 位に従って昇順に並びます。
  3. \\(k\\) を \\(1\\) 増やし、手順 2. に戻って反復を続けます。すべての桁のソートが完了したら終了します。

図 11-18   基数ソートのアルゴリズムの流れ

以下ではコード実装を分解して見ていきます。\\(d\\) 進数の数値 \\(x\\) について、その第 \\(k\\) 位 \\(x_k\\) を取得するには、次の計算式を用います。

\\[ x_k = \\lfloor\\frac{x}{d^{k-1}}\\rfloor \\bmod d \\]

ここで、\\(\\lfloor a \\rfloor\\) は浮動小数点数 \\(a\\) の切り捨てを表し、\\(\\bmod \\: d\\) は \\(d\\) による剰余を表します。学籍番号データでは、\\(d = 10\\) かつ \\(k \\in [1, 8]\\) です。

さらに、数字の第 \\(k\\) 位に基づいてソートできるように、計数ソートのコードを少し変更する必要があります。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby radix_sort.py
def digit(num: int, exp: int) -> int:\n    \"\"\"要素 num の下から k 桁目を取得(exp = 10^(k-1))\"\"\"\n    # ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"計数ソート(nums の k 桁目でソート)\"\"\"\n    # 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    counter = [0] * 10\n    n = len(nums)\n    # 0~9 の各数字の出現回数を集計する\n    for i in range(n):\n        d = digit(nums[i], exp)  # nums[i] の第 k 位を取得し、d とする\n        counter[d] += 1  # 数字 d の出現回数を数える\n    # 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for i in range(1, 10):\n        counter[i] += counter[i - 1]\n    # 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        d = digit(nums[i], exp)\n        j = counter[d] - 1  # d の配列内インデックス j を取得する\n        res[j] = nums[i]  # 現在の要素をインデックス j に格納する\n        counter[d] -= 1  # d の個数を 1 減らす\n    # 結果で元の配列 nums を上書きする\n    for i in range(n):\n        nums[i] = res[i]\n\ndef radix_sort(nums: list[int]):\n    \"\"\"基数ソート\"\"\"\n    # 最大桁数の判定用に配列の最大要素を取得\n    m = max(nums)\n    # 下位桁から上位桁の順に走査する\n    exp = 1\n    while exp <= m:\n        # 配列要素の k 桁目に対して計数ソートを行う\n        # k = 1 -> exp = 1\n        # k = 2 -> exp = 10\n        # つまり exp = 10^(k-1)\n        counting_sort_digit(nums, exp)\n        exp *= 10\n
radix_sort.cpp
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    vector<int> counter(10, 0);\n    int n = nums.size();\n    // 0~9 の各数字の出現回数を集計する\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d]++;                // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    vector<int> res(n, 0);\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i];       // 現在の要素をインデックス j に格納する\n        counter[d]--;           // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* 基数ソート */\nvoid radixSort(vector<int> &nums) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    int m = *max_element(nums.begin(), nums.end());\n    // 下位桁から上位桁の順に走査する\n    for (int exp = 1; exp <= m; exp *= 10)\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n}\n
radix_sort.java
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(int[] nums, int exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    int[] counter = new int[10];\n    int n = nums.length;\n    // 0~9 の各数字の出現回数を集計する\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d]++;                // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i];       // 現在の要素をインデックス j に格納する\n        counter[d]--;           // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* 基数ソート */\nvoid radixSort(int[] nums) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    int m = Integer.MIN_VALUE;\n    for (int num : nums)\n        if (num > m)\n            m = num;\n    // 下位桁から上位桁の順に走査する\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.cs
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint Digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid CountingSortDigit(int[] nums, int exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    int[] counter = new int[10];\n    int n = nums.Length;\n    // 0~9 の各数字の出現回数を集計する\n    for (int i = 0; i < n; i++) {\n        int d = Digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d]++;                // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = Digit(nums[i], exp);\n        int j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i];       // 現在の要素をインデックス j に格納する\n        counter[d]--;           // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* 基数ソート */\nvoid RadixSort(int[] nums) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    int m = int.MinValue;\n    foreach (int num in nums) {\n        if (num > m) m = num;\n    }\n    // 下位桁から上位桁の順に走査する\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        CountingSortDigit(nums, exp);\n    }\n}\n
radix_sort.go
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunc digit(num, exp int) int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunc countingSortDigit(nums []int, exp int) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    counter := make([]int, 10)\n    n := len(nums)\n    // 0~9 の各数字の出現回数を集計する\n    for i := 0; i < n; i++ {\n        d := digit(nums[i], exp) // nums[i] の第 k 位を取得し、d とする\n        counter[d]++             // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for i := 1; i < 10; i++ {\n        counter[i] += counter[i-1]\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        d := digit(nums[i], exp)\n        j := counter[d] - 1 // d の配列内インデックス j を取得する\n        res[j] = nums[i]    // 現在の要素をインデックス j に格納する\n        counter[d]--        // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for i := 0; i < n; i++ {\n        nums[i] = res[i]\n    }\n}\n\n/* 基数ソート */\nfunc radixSort(nums []int) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    max := math.MinInt\n    for _, num := range nums {\n        if num > max {\n            max = num\n        }\n    }\n    // 下位桁から上位桁の順に走査する\n    for exp := 1; max >= exp; exp *= 10 {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n    }\n}\n
radix_sort.swift
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    var counter = Array(repeating: 0, count: 10)\n    // 0~9 の各数字の出現回数を集計する\n    for i in nums.indices {\n        let d = digit(num: nums[i], exp: exp) // nums[i] の第 k 位を取得し、d とする\n        counter[d] += 1 // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for i in 1 ..< 10 {\n        counter[i] += counter[i - 1]\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let d = digit(num: nums[i], exp: exp)\n        let j = counter[d] - 1 // d の配列内インデックス j を取得する\n        res[j] = nums[i] // 現在の要素をインデックス j に格納する\n        counter[d] -= 1 // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n\n/* 基数ソート */\nfunc radixSort(nums: inout [Int]) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    var m = Int.min\n    for num in nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 下位桁から上位桁の順に走査する\n    for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums: &nums, exp: exp)\n    }\n}\n
radix_sort.js
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunction digit(num, exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunction countingSortDigit(nums, exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // 0~9 の各数字の出現回数を集計する\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d]++; // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i]; // 現在の要素をインデックス j に格納する\n        counter[d]--; // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* 基数ソート */\nfunction radixSort(nums) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    let m = Math.max(... nums);\n    // 下位桁から上位桁の順に走査する\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.ts
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfunction digit(num: number, exp: number): number {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return Math.floor(num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // 0~9 の各数字の出現回数を集計する\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d]++; // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i]; // 現在の要素をインデックス j に格納する\n        counter[d]--; // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* 基数ソート */\nfunction radixSort(nums: number[]): void {\n    // 最大桁数の判定用に配列の最大要素を取得\n    let m: number = Math.max(... nums);\n    // 下位桁から上位桁の順に走査する\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.dart
/* 要素 `_num` の第 k 桁を取得する。ここで `exp = 10^(k-1)` */\nint digit(int _num, int exp) {\n  // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n  return (_num ~/ exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(List<int> nums, int exp) {\n  // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n  List<int> counter = List<int>.filled(10, 0);\n  int n = nums.length;\n  // 0~9 の各数字の出現回数を集計する\n  for (int i = 0; i < n; i++) {\n    int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n    counter[d]++; // 数字 d の出現回数を数える\n  }\n  // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n  for (int i = 1; i < 10; i++) {\n    counter[i] += counter[i - 1];\n  }\n  // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n  List<int> res = List<int>.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int d = digit(nums[i], exp);\n    int j = counter[d] - 1; // d の配列内インデックス j を取得する\n    res[j] = nums[i]; // 現在の要素をインデックス j に格納する\n    counter[d]--; // d の個数を 1 減らす\n  }\n  // 結果で元の配列 nums を上書きする\n  for (int i = 0; i < n; i++) nums[i] = res[i];\n}\n\n/* 基数ソート */\nvoid radixSort(List<int> nums) {\n  // 最大桁数の判定用に配列の最大要素を取得する\n  // dart の `int` の長さは 64 ビット\n  int m = -1 << 63;\n  for (int _num in nums) if (_num > m) m = _num;\n  // 下位桁から上位桁の順に走査する\n  for (int exp = 1; exp <= m; exp *= 10)\n    // 配列要素の k 桁目に対して計数ソートを行う\n    // k = 1 -> exp = 1\n    // k = 2 -> exp = 10\n    // つまり exp = 10^(k-1)\n    countingSortDigit(nums, exp);\n}\n
radix_sort.rs
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfn digit(num: i32, exp: i32) -> usize {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return ((num / exp) % 10) as usize;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfn counting_sort_digit(nums: &mut [i32], exp: i32) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    let mut counter = [0; 10];\n    let n = nums.len();\n    // 0~9 の各数字の出現回数を集計する\n    for i in 0..n {\n        let d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする\n        counter[d] += 1; // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for i in 1..10 {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let d = digit(nums[i], exp);\n        let j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i]; // 現在の要素をインデックス j に格納する\n        counter[d] -= 1; // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    nums.copy_from_slice(&res);\n}\n\n/* 基数ソート */\nfn radix_sort(nums: &mut [i32]) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    let m = *nums.into_iter().max().unwrap();\n    // 下位桁から上位桁の順に走査する\n    let mut exp = 1;\n    while exp <= m {\n        counting_sort_digit(nums, exp);\n        exp *= 10;\n    }\n}\n
radix_sort.c
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nint digit(int num, int exp) {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10;\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    int *counter = (int *)malloc((sizeof(int) * 10));\n    memset(counter, 0, sizeof(int) * 10); // 後続のメモリ解放に備えて 0 で初期化する\n    // 0~9 の各数字の出現回数を集計する\n    for (int i = 0; i < size; i++) {\n        // nums[i] の第 k 位を取得し、d とする\n        int d = digit(nums[i], exp);\n        // 数字 d の出現回数を数える\n        counter[d]++;\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    int *res = (int *)malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // d の配列内インデックス j を取得する\n        res[j] = nums[i];       // 現在の要素をインデックス j に格納する\n        counter[d]--;           // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (int i = 0; i < size; i++) {\n        nums[i] = res[i];\n    }\n    // メモリを解放する\n    free(res);\n    free(counter);\n}\n\n/* 基数ソート */\nvoid radixSort(int nums[], int size) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    int max = INT32_MIN;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > max) {\n            max = nums[i];\n        }\n    }\n    // 下位桁から上位桁の順に走査する\n    for (int exp = 1; max >= exp; exp *= 10)\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, size, exp);\n}\n
radix_sort.kt
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */\nfun digit(num: Int, exp: Int): Int {\n    // ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す\n    return (num / exp) % 10\n}\n\n/* 計数ソート(nums の k 桁目でソート) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n    // 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n    val counter = IntArray(10)\n    val n = nums.size\n    // 0~9 の各数字の出現回数を集計する\n    for (i in 0..<n) {\n        val d = digit(nums[i], exp) // nums[i] の第 k 位を取得し、d とする\n        counter[d]++                // 数字 d の出現回数を数える\n    }\n    // 累積和を求め、「出現回数」を「配列インデックス」に変換する\n    for (i in 1..9) {\n        counter[i] += counter[i - 1]\n    }\n    // 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val d = digit(nums[i], exp)\n        val j = counter[d] - 1 // d の配列内インデックス j を取得する\n        res[j] = nums[i]       // 現在の要素をインデックス j に格納する\n        counter[d]--           // d の個数を 1 減らす\n    }\n    // 結果で元の配列 nums を上書きする\n    for (i in 0..<n)\n        nums[i] = res[i]\n}\n\n/* 基数ソート */\nfun radixSort(nums: IntArray) {\n    // 最大桁数の判定用に配列の最大要素を取得\n    var m = Int.MIN_VALUE\n    for (num in nums) if (num > m) m = num\n    var exp = 1\n    // 下位桁から上位桁の順に走査する\n    while (exp <= m) {\n        // 配列要素の k 桁目に対して計数ソートを行う\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // つまり exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n        exp *= 10\n    }\n}\n
radix_sort.rb
### num の第 k 桁を取得する。ここで exp = 10^(k-1) ###\ndef digit(num, exp)\n  # k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる\n  (num / exp) % 10\nend\n\n### num の第 k 桁を取得する。ここで exp = 10^(k-1) ###\ndef digit(num, exp)\n  # k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる\n  (num / exp) % 10\nend\n\n# ## 計数ソート(nums の k 桁目でソート)###\ndef counting_sort_digit(nums, exp)\n  # 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要\n  counter = Array.new(10, 0)\n  n = nums.length\n  # 0~9 の各数字の出現回数を集計する\n  for i in 0...n\n    d = digit(nums[i], exp) # nums[i] の第 k 位を取得し、d とする\n    counter[d] += 1 # 数字 d の出現回数を数える\n  end\n  # 累積和を求め、「出現回数」を「配列インデックス」に変換する\n  (1...10).each { |i| counter[i] += counter[i - 1] }\n  # 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する\n  res = Array.new(n, 0)\n  for i in (n - 1).downto(0)\n    d = digit(nums[i], exp)\n    j = counter[d] - 1 # d の配列内インデックス j を取得する\n    res[j] = nums[i] # 現在の要素をインデックス j に格納する\n    counter[d] -= 1 # d の個数を 1 減らす\n  end\n  # 結果で元の配列 nums を上書きする\n  (0...n).each { |i| nums[i] = res[i] }\nend\n\n### 基数ソート ###\ndef radix_sort(nums)\n  # 最大桁数の判定用に配列の最大要素を取得\n  m = nums.max\n  # 下位桁から上位桁の順に走査する\n  exp = 1\n  while exp <= m\n    # 配列要素の k 桁目に対して計数ソートを行う\n    # k = 1 -> exp = 1\n    # k = 2 -> exp = 10\n    # つまり exp = 10^(k-1)\n    counting_sort_digit(nums, exp)\n    exp *= 10\n  end\nend\n
コードの可視化

全画面で見る >

なぜ最下位桁からソートするのですか?

連続するソートの各ラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。たとえば、第1ラウンドで \\(a < b\\) となっていても、第2ラウンドで \\(a > b\\) となれば、第2ラウンドの結果が優先されます。数字では高位の優先度が低位より高いため、先に低位をソートし、その後で高位をソートする必要があります。

","path":["第 11 章   ソート","11.10   基数ソート"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11102","level":2,"title":"11.10.2   アルゴリズムの特徴","text":"

計数ソートと比べると、基数ソートは値の範囲が大きい場合に適しています。ただし、データが固定桁数の形式で表せること、かつ桁数が大きすぎないことが前提です。たとえば、浮動小数点数は基数ソートに適していません。桁数 \\(k\\) が大きすぎて、時間計算量が \\(O(nk) \\gg O(n^2)\\) になる可能性があるためです。

  • 時間計算量は \\(O(nk)\\)、非適応ソート:データ量を \\(n\\)、データが \\(d\\) 進数、最大桁数を \\(k\\) とすると、ある1桁に対して計数ソートを実行する時間は \\(O(n + d)\\) であり、全 \\(k\\) 桁をソートする時間は \\(O((n + d)k)\\) です。通常、\\(d\\) と \\(k\\) はどちらも比較的小さいため、時間計算量は \\(O(n)\\) に近づきます。
  • 空間計算量は \\(O(n + d)\\)、非原地ソート:計数ソートと同様に、基数ソートでは長さ \\(n\\) と \\(d\\) の配列 rescounter を補助的に用います。
  • 安定ソート:計数ソートが安定であれば基数ソートも安定です。計数ソートが不安定な場合、基数ソートでは正しいソート結果を保証できません。
","path":["第 11 章   ソート","11.10   基数ソート"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   選択ソート","text":"

選択ソート(selection sort)の仕組みは非常に単純です。ループを開始し、各ラウンドで未ソート区間から最小の要素を選び、整列済み区間の末尾に配置します。

配列の長さを \\(n\\) とすると、選択ソートの手順は次の図のようになります。

  1. 初期状態では、すべての要素が未ソートであり、未ソートな(インデックス)区間は \\([0, n-1]\\) です。
  2. 区間 \\([0, n-1]\\) 内の最小要素を選び、インデックス \\(0\\) の要素と交換します。これにより、配列の先頭 1 要素が整列済みになります。
  3. 区間 \\([1, n-1]\\) 内の最小要素を選び、インデックス \\(1\\) の要素と交換します。これにより、配列の先頭 2 要素が整列済みになります。
  4. これを繰り返します。\\(n - 1\\) 回の選択と交換を経ると、配列の先頭 \\(n - 1\\) 要素が整列済みになります。
  5. 残った 1 つの要素は必ず最大要素なので、ソートは不要です。これで配列のソートは完了します。
<1><2><3><4><5><6><7><8><9><10><11>

図 11-2   選択ソートの手順

コードでは、\\(k\\) を用いて未ソート区間内の最小要素を記録します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby selection_sort.py
def selection_sort(nums: list[int]):\n    \"\"\"選択ソート\"\"\"\n    n = len(nums)\n    # 外側ループ:未整列区間は [i, n-1]\n    for i in range(n - 1):\n        # 内側のループ:未ソート区間の最小要素を見つける\n        k = i\n        for j in range(i + 1, n):\n            if nums[j] < nums[k]:\n                k = j  # 最小要素のインデックスを記録\n        # その最小要素を未整列区間の先頭要素と交換する\n        nums[i], nums[k] = nums[k], nums[i]\n
selection_sort.cpp
/* 選択ソート */\nvoid selectionSort(vector<int> &nums) {\n    int n = nums.size();\n    // 外側ループ:未整列区間は [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 最小要素のインデックスを記録\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        swap(nums[i], nums[k]);\n    }\n}\n
selection_sort.java
/* 選択ソート */\nvoid selectionSort(int[] nums) {\n    int n = nums.length;\n    // 外側ループ:未整列区間は [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 最小要素のインデックスを記録\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
selection_sort.cs
/* 選択ソート */\nvoid SelectionSort(int[] nums) {\n    int n = nums.Length;\n    // 外側ループ:未整列区間は [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 最小要素のインデックスを記録\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        (nums[k], nums[i]) = (nums[i], nums[k]);\n    }\n}\n
selection_sort.go
/* 選択ソート */\nfunc selectionSort(nums []int) {\n    n := len(nums)\n    // 外側ループ:未整列区間は [i, n-1]\n    for i := 0; i < n-1; i++ {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        k := i\n        for j := i + 1; j < n; j++ {\n            if nums[j] < nums[k] {\n                // 最小要素のインデックスを記録\n                k = j\n            }\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        nums[i], nums[k] = nums[k], nums[i]\n\n    }\n}\n
selection_sort.swift
/* 選択ソート */\nfunc selectionSort(nums: inout [Int]) {\n    // 外側ループ:未整列区間は [i, n-1]\n    for i in nums.indices.dropLast() {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        var k = i\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[j] < nums[k] {\n                k = j // 最小要素のインデックスを記録\n            }\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        nums.swapAt(i, k)\n    }\n}\n
selection_sort.js
/* 選択ソート */\nfunction selectionSort(nums) {\n    let n = nums.length;\n    // 外側ループ:未整列区間は [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // 最小要素のインデックスを記録\n            }\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
selection_sort.ts
/* 選択ソート */\nfunction selectionSort(nums: number[]): void {\n    let n = nums.length;\n    // 外側ループ:未整列区間は [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // 最小要素のインデックスを記録\n            }\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
selection_sort.dart
/* 選択ソート */\nvoid selectionSort(List<int> nums) {\n  int n = nums.length;\n  // 外側ループ:未整列区間は [i, n-1]\n  for (int i = 0; i < n - 1; i++) {\n    // 内側のループ:未ソート区間の最小要素を見つける\n    int k = i;\n    for (int j = i + 1; j < n; j++) {\n      if (nums[j] < nums[k]) k = j; // 最小要素のインデックスを記録\n    }\n    // その最小要素を未整列区間の先頭要素と交換する\n    int temp = nums[i];\n    nums[i] = nums[k];\n    nums[k] = temp;\n  }\n}\n
selection_sort.rs
/* 選択ソート */\nfn selection_sort(nums: &mut [i32]) {\n    if nums.is_empty() {\n        return;\n    }\n    let n = nums.len();\n    // 外側ループ:未整列区間は [i, n-1]\n    for i in 0..n - 1 {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        let mut k = i;\n        for j in i + 1..n {\n            if nums[j] < nums[k] {\n                k = j; // 最小要素のインデックスを記録\n            }\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        nums.swap(i, k);\n    }\n}\n
selection_sort.c
/* 選択ソート */\nvoid selectionSort(int nums[], int n) {\n    // 外側ループ:未整列区間は [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内側のループ:未ソート区間の最小要素を見つける\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 最小要素のインデックスを記録\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
selection_sort.kt
/* 選択ソート */\nfun selectionSort(nums: IntArray) {\n    val n = nums.size\n    // 外側ループ:未整列区間は [i, n-1]\n    for (i in 0..<n - 1) {\n        var k = i\n        // 内側のループ:未ソート区間の最小要素を見つける\n        for (j in i + 1..<n) {\n            if (nums[j] < nums[k])\n                k = j // 最小要素のインデックスを記録\n        }\n        // その最小要素を未整列区間の先頭要素と交換する\n        val temp = nums[i]\n        nums[i] = nums[k]\n        nums[k] = temp\n    }\n}\n
selection_sort.rb
### 選択ソート ###\ndef selection_sort(nums)\n  n = nums.length\n  # 外側ループ:未整列区間は [i, n-1]\n  for i in 0...(n - 1)\n    # 内側のループ:未ソート区間の最小要素を見つける\n    k = i\n    for j in (i + 1)...n\n      if nums[j] < nums[k]\n        k = j # 最小要素のインデックスを記録\n      end\n    end\n    # その最小要素を未整列区間の先頭要素と交換する\n    nums[i], nums[k] = nums[k], nums[i]\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 11 章   ソート","11.2   選択ソート"],"tags":[]},{"location":"chapter_sorting/selection_sort/#1121","level":2,"title":"11.2.1   アルゴリズムの特徴","text":"
  • 時間計算量は \\(O(n^2)\\)、非適応ソート:外側のループは合計 \\(n - 1\\) 回です。最初のラウンドの未ソート区間の長さは \\(n\\)、最後のラウンドでは \\(2\\) であり、各ラウンドの内側のループ回数はそれぞれ \\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(3\\)、\\(2\\) となります。総和は \\(\\frac{(n - 1)(n + 2)}{2}\\) です。
  • 空間計算量は \\(O(1)\\)、インプレースソート:ポインタ \\(i\\) と \\(j\\) は定数サイズの追加領域しか使用しません。
  • 不安定ソート:次の図のように、要素 nums[i] がそれと等しい要素の右側へ交換され、両者の相対的な順序が変わる可能性があります。

図 11-3   選択ソートの不安定な例

","path":["第 11 章   ソート","11.2   選択ソート"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   ソートアルゴリズム","text":"

ソートアルゴリズム(sorting algorithm)は、データの集合を特定の順序に従って並べ替えるために用いられます。ソートアルゴリズムは幅広く応用されており、整列済みデータは通常、より効率的に検索、分析、処理できるためです。

下図に示すように、ソートアルゴリズムにおけるデータ型は整数、浮動小数点数、文字、文字列などです。ソートの判定規則は、数値の大小、文字の ASCII コード順、またはカスタムルールなど、要件に応じて設定できます。

図 11-1   データ型と判定規則の例

","path":["第 11 章   ソート","11.1   ソートアルゴリズム"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111","level":2,"title":"11.1.1   評価軸","text":"

実行効率:ソートアルゴリズムの時間計算量はできるだけ低く、かつ全体の操作回数も少ないこと(時間計算量における定数項が小さいこと)が望まれます。大量データの場合、実行効率はとりわけ重要です。

インプレース性:その名のとおり、インプレースソートは元の配列を直接操作して並べ替えを行うため、追加の補助配列を必要とせず、メモリを節約できます。通常、インプレースソートはデータの移動操作が少なく、実行速度もより高速です。

安定性:安定ソートは、並べ替え完了後も、等しい要素の配列内での相対順序が変化しません。

安定ソートは多段ソートの場面で必要条件となります。学生情報を保存した表があり、第 1 列と第 2 列がそれぞれ氏名と年齢であると仮定します。この場合、不安定ソートによって入力データの順序性が失われる可能性があります。

# 入力データは氏名順にソートされている\n# (name, age)\n  ('A', 19)\n  ('B', 18)\n  ('C', 21)\n  ('D', 19)\n  ('E', 23)\n\n# 不安定ソートアルゴリズムで年齢順にリストを並べ替えると仮定すると、\n# 結果では ('D', 19) と ('A', 19) の相対位置が変わり、\n# 入力データが氏名順である性質が失われる\n  ('B', 18)\n  ('D', 19)\n  ('A', 19)\n  ('C', 21)\n  ('E', 23)\n

適応性:適応的ソートは、入力データに既に存在する順序情報を利用して計算量を減らし、より優れた時間効率を実現できます。適応的ソートアルゴリズムの最良時間計算量は、通常、平均時間計算量より優れています。

比較ベースかどうか:比較ベースのソートは、比較演算子(\\(<\\)、\\(=\\)、\\(>\\))に依存して要素の相対順序を判定し、それによって配列全体をソートします。理論上の最良時間計算量は \\(O(n \\log n)\\) です。一方、非比較ソートは比較演算子を使用せず、時間計算量は \\(O(n)\\) に達しますが、汎用性は相対的に低くなります。

","path":["第 11 章   ソート","11.1   ソートアルゴリズム"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1112","level":2,"title":"11.1.2   理想的なソートアルゴリズム","text":"

高速、インプレース、安定、適応的、高い汎用性。明らかに、これまでのところ、以上のすべての特性を兼ね備えたソートアルゴリズムはまだ見つかっていません。そのため、ソートアルゴリズムを選択する際には、具体的なデータの特徴と問題の要件に応じて判断する必要があります。

次に、さまざまなソートアルゴリズムを一緒に学び、上記の評価軸に基づいて各ソートアルゴリズムの長所と短所を分析していきます。

","path":["第 11 章   ソート","11.1   ソートアルゴリズム"],"tags":[]},{"location":"chapter_sorting/summary/","level":1,"title":"11.11   まとめ","text":"","path":["第 11 章   ソート","11.11   まとめ"],"tags":[]},{"location":"chapter_sorting/summary/#1","level":3,"title":"1.   重要なポイントの振り返り","text":"
  • バブルソートは隣接する要素を交換することで整列を行います。フラグを追加して早期リターンを可能にすると、バブルソートの最良時間計算量を \\(O(n)\\) に最適化できます。
  • 挿入ソートは各ラウンドで未整列区間の要素を整列済み区間の正しい位置に挿入することで整列を完了します。挿入ソートの時間計算量は \\(O(n^2)\\) ですが、基本操作が比較的少ないため、小規模データのソート処理で非常に人気があります。
  • クイックソートは番兵分割操作に基づいて整列を行います。番兵分割では毎回最悪の基準値を選んでしまう可能性があり、その結果、時間計算量は \\(O(n^2)\\) まで劣化することがあります。中央値の基準値やランダムな基準値を導入すると、この劣化の確率を下げられます。短い部分配列を優先して再帰すれば、再帰の深さを効果的に抑え、空間計算量を \\(O(\\log n)\\) に最適化できます。
  • マージソートは分割とマージという 2 つの段階からなり、分割統治戦略を典型的に体現しています。マージソートでは配列を整列する際に補助配列の作成が必要で、空間計算量は \\(O(n)\\) です。一方、連結リストを整列する場合の空間計算量は \\(O(1)\\) まで最適化できます。
  • バケットソートはデータのバケット分配、バケット内ソート、結果の結合という 3 つの手順を含みます。これも分割統治戦略を体現しており、データ量が非常に大きい場合に適しています。バケットソートの鍵は、データを平均的に分配することにあります。
  • カウントソートはバケットソートの特例であり、データの出現回数を数えることで整列を行います。カウントソートはデータ量が大きく、かつデータ範囲が限られている場合に適しており、データを正の整数に変換できることが前提です。
  • 基数ソートは各桁ごとの整列によってデータを整列し、データが固定桁数の数値として表せることを前提とします。
  • 総じて言えば、私たちは高効率で、安定で、インプレースで、さらに適応的であるといった利点を備えたソートアルゴリズムを見つけたいと考えます。しかし、ほかのデータ構造やアルゴリズムと同様に、これらすべての条件を同時に満たせるソートアルゴリズムは存在しません。実際の応用では、データの特性に応じて適切なソートアルゴリズムを選ぶ必要があります。
  • 下図では、主流のソートアルゴリズムについて、効率、安定性、インプレース性、適応性などを比較しています。

図 11-19   ソートアルゴリズムの比較

","path":["第 11 章   ソート","11.11   まとめ"],"tags":[]},{"location":"chapter_sorting/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:ソートアルゴリズムの安定性は、どのような場合に必須ですか?

現実には、オブジェクトのある属性に基づいて整列することがあります。たとえば、学生には氏名と身長という 2 つの属性があり、多段階のソートを行いたいとします。まず氏名で整列して (A, 180) (B, 185) (C, 170) (D, 170) を得て、その後に身長で整列します。ソートアルゴリズムが不安定である場合、結果は (D, 170) (C, 170) (A, 180) (B, 185) になる可能性があります。

このように、学生 D と C の位置が入れ替わり、氏名に関する順序性が壊れてしまいます。これは望ましくありません。

Q:番兵分割において、「右から左へ探索する」順序と「左から右へ探索する」順序は入れ替えられますか?

できません。最も左端の要素を基準値とする場合は、必ず先に「右から左へ探索する」を行い、その後に「左から右へ探索する」を行う必要があります。この結論はやや直感に反するので、理由を分析してみましょう。

番兵分割 partition() の最後の手順は、nums[left]nums[i] を交換することです。交換が終わると、基準値の左側にある要素はすべて基準値 <= になります。したがって、最後の交換の前に nums[left] >= nums[i] が必ず成り立っていなければなりません。仮に先に「左から右へ探索する」を行うと、基準値より大きい要素が見つからない場合、i == j の時点でループを抜け、このとき nums[j] == nums[i] > nums[left] となる可能性があります。つまり、この最後の交換によって、基準値より大きい要素が配列の最左端へ移されてしまい、番兵分割は失敗します。

たとえば、配列 [0, 0, 0, 0, 1] が与えられたとき、先に「左から右へ探索する」を行うと、番兵分割後の配列は [1, 0, 0, 0, 0] になります。これは誤った結果です。

さらに考えると、nums[right] を基準値に選ぶ場合はちょうど逆になり、必ず先に「左から右へ探索する」を行う必要があります。

Q:クイックソートの再帰深度最適化について、短い配列を選ぶとなぜ再帰深度が \\(\\log n\\) を超えないと保証できるのですか?

再帰深度とは、現在まだ戻っていない再帰呼び出しの数のことです。各ラウンドの番兵分割では、元の配列を 2 つの部分配列に分けます。再帰深度の最適化後は、下方向に再帰する部分配列の長さは最大でも元の配列長の半分です。最悪の場合でも毎回半分の長さになると仮定すれば、最終的な再帰深度は \\(\\log n\\) になります。

元のクイックソートを振り返ると、長いほうの配列に対して連続して再帰してしまう可能性があり、最悪の場合は \\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) と続き、再帰深度は \\(n\\) になります。再帰深度の最適化により、このような状況を避けられます。

Q:配列内のすべての要素が等しい場合、クイックソートの時間計算量は \\(O(n^2)\\) になりますか?このような退化はどう処理すべきですか?

はい。この場合は、番兵分割によって配列を「基準値より小さい」「基準値に等しい」「基準値より大きい」の 3 つの部分に分ける方法を検討できます。下方向に再帰するのは、小さい部分と大きい部分だけです。この方法では、入力要素がすべて等しい配列は、1 回の番兵分割だけで整列を完了できます。

Q:バケットソートの最悪時間計算量が \\(O(n^2)\\) なのはなぜですか?

最悪の場合、すべての要素が同じバケットに振り分けられます。その要素群を整列するのに \\(O(n^2)\\) のアルゴリズムを使えば、時間計算量は \\(O(n^2)\\) になります。

","path":["第 11 章   ソート","11.11   まとめ"],"tags":[]},{"location":"chapter_stack_and_queue/","level":1,"title":"第 5 章   スタックとキュー","text":"

Abstract

スタックは猫を積み重ねるようなもので、キューは猫が列に並ぶようなものです。

両者はそれぞれ、後入れ先出しと先入れ先出しの論理関係を表します。

","path":["第 5 章   スタックとキュー"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"章の内容","text":"
  • 5.1   スタック
  • 5.2   キュー
  • 5.3   両端キュー
  • 5.4   まとめ
","path":["第 5 章   スタックとキュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   両端キュー","text":"

キューでは、先頭要素を削除するか末尾に要素を追加することしかできません。次の図に示すように、両端キュー(double-ended queue)はより高い柔軟性を備えており、先頭と末尾の両方で要素の追加や削除を行えます。

図 5-7   両端キューの操作

","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#531","level":2,"title":"5.3.1   両端キューの基本操作","text":"

両端キューの基本操作を次の表に示します。具体的なメソッド名は、使用するプログラミング言語によって異なります。

表 5-3   両端キューの操作効率

メソッド名 説明 時間計算量 push_first() 先頭に要素を追加 \\(O(1)\\) push_last() 末尾に要素を追加 \\(O(1)\\) pop_first() 先頭要素を削除 \\(O(1)\\) pop_last() 末尾要素を削除 \\(O(1)\\) peek_first() 先頭要素にアクセス \\(O(1)\\) peek_last() 末尾要素にアクセス \\(O(1)\\)

同様に、プログラミング言語に組み込み実装されている両端キューのクラスを直接使うこともできます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby deque.py
from collections import deque\n\n# 両端キューを初期化\ndeq: deque[int] = deque()\n\n# 要素をエンキュー\ndeq.append(2)      # 末尾に追加\ndeq.append(5)\ndeq.append(4)\ndeq.appendleft(3)  # 先頭に追加\ndeq.appendleft(1)\n\n# 要素にアクセス\nfront: int = deq[0]  # 先頭要素\nrear: int = deq[-1]  # 末尾要素\n\n# 要素をデキュー\npop_front: int = deq.popleft()  # 先頭要素をデキュー\npop_rear: int = deq.pop()       # 末尾要素をデキュー\n\n# 両端キューの長さを取得\nsize: int = len(deq)\n\n# 両端キューが空かどうかを判定\nis_empty: bool = len(deq) == 0\n
deque.cpp
/* 両端キューを初期化 */\ndeque<int> deque;\n\n/* 要素をエンキュー */\ndeque.push_back(2);   // 末尾に追加\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3);  // 先頭に追加\ndeque.push_front(1);\n\n/* 要素にアクセス */\nint front = deque.front(); // 先頭要素\nint back = deque.back();   // 末尾要素\n\n/* 要素をデキュー */\ndeque.pop_front();  // 先頭要素をデキュー\ndeque.pop_back();   // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.size();\n\n/* 両端キューが空かどうかを判定 */\nbool empty = deque.empty();\n
deque.java
/* 両端キューを初期化 */\nDeque<Integer> deque = new LinkedList<>();\n\n/* 要素をエンキュー */\ndeque.offerLast(2);   // 末尾に追加\ndeque.offerLast(5);\ndeque.offerLast(4);\ndeque.offerFirst(3);  // 先頭に追加\ndeque.offerFirst(1);\n\n/* 要素にアクセス */\nint peekFirst = deque.peekFirst();  // 先頭要素\nint peekLast = deque.peekLast();    // 末尾要素\n\n/* 要素をデキュー */\nint popFirst = deque.pollFirst();  // 先頭要素をデキュー\nint popLast = deque.pollLast();    // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.size();\n\n/* 両端キューが空かどうかを判定 */\nboolean isEmpty = deque.isEmpty();\n
deque.cs
/* 両端キューを初期化 */\n// C# では、連結リスト LinkedList を両端キューとして使用する\nLinkedList<int> deque = new();\n\n/* 要素をエンキュー */\ndeque.AddLast(2);   // 末尾に追加\ndeque.AddLast(5);\ndeque.AddLast(4);\ndeque.AddFirst(3);  // 先頭に追加\ndeque.AddFirst(1);\n\n/* 要素にアクセス */\nint peekFirst = deque.First.Value;  // 先頭要素\nint peekLast = deque.Last.Value;    // 末尾要素\n\n/* 要素をデキュー */\ndeque.RemoveFirst();  // 先頭要素をデキュー\ndeque.RemoveLast();   // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.Count;\n\n/* 両端キューが空かどうかを判定 */\nbool isEmpty = deque.Count == 0;\n
deque_test.go
/* 両端キューを初期化 */\n// Go では、list を両端キューとして使用する\ndeque := list.New()\n\n/* 要素をエンキュー */\ndeque.PushBack(2)      // 末尾に追加\ndeque.PushBack(5)\ndeque.PushBack(4)\ndeque.PushFront(3)     // 先頭に追加\ndeque.PushFront(1)\n\n/* 要素にアクセス */\nfront := deque.Front() // 先頭要素\nrear := deque.Back()   // 末尾要素\n\n/* 要素をデキュー */\ndeque.Remove(front)    // 先頭要素をデキュー\ndeque.Remove(rear)     // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nsize := deque.Len()\n\n/* 両端キューが空かどうかを判定 */\nisEmpty := deque.Len() == 0\n
deque.swift
/* 両端キューを初期化 */\n// Swift には組み込みの両端キュークラスがないため、Array を両端キューとして使用する\nvar deque: [Int] = []\n\n/* 要素をエンキュー */\ndeque.append(2) // 末尾に追加\ndeque.append(5)\ndeque.append(4)\ndeque.insert(3, at: 0) // 先頭に追加\ndeque.insert(1, at: 0)\n\n/* 要素にアクセス */\nlet peekFirst = deque.first! // 先頭要素\nlet peekLast = deque.last! // 末尾要素\n\n/* 要素をデキュー */\n// Array で模擬する場合、popFirst の計算量は O(n)\nlet popFirst = deque.removeFirst() // 先頭要素をデキュー\nlet popLast = deque.removeLast() // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nlet size = deque.count\n\n/* 両端キューが空かどうかを判定 */\nlet isEmpty = deque.isEmpty\n
deque.js
/* 両端キューを初期化 */\n// JavaScript には組み込みの両端キューがないため、Array を両端キューとして使用するしかない\nconst deque = [];\n\n/* 要素をエンキュー */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// 配列であるため、unshift() メソッドの時間計算量は O(n) です\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* 要素にアクセス */\nconst peekFirst = deque[0];\nconst peekLast = deque[deque.length - 1];\n\n/* 要素をデキュー */\n// 配列であるため、shift() メソッドの時間計算量は O(n) です\nconst popFront = deque.shift();\nconst popBack = deque.pop();\n\n/* 両端キューの長さを取得 */\nconst size = deque.length;\n\n/* 両端キューが空かどうかを判定 */\nconst isEmpty = size === 0;\n
deque.ts
/* 両端キューを初期化 */\n// TypeScript には組み込みの両端キューがないため、Array を両端キューとして使用するしかない\nconst deque: number[] = [];\n\n/* 要素をエンキュー */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// 配列であるため、unshift() メソッドの時間計算量は O(n) です\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* 要素にアクセス */\nconst peekFirst: number = deque[0];\nconst peekLast: number = deque[deque.length - 1];\n\n/* 要素をデキュー */\n// 配列であるため、shift() メソッドの時間計算量は O(n) です\nconst popFront: number = deque.shift() as number;\nconst popBack: number = deque.pop() as number;\n\n/* 両端キューの長さを取得 */\nconst size: number = deque.length;\n\n/* 両端キューが空かどうかを判定 */\nconst isEmpty: boolean = size === 0;\n
deque.dart
/* 両端キューを初期化 */\n// Dart では、Queue は両端キューとして定義されています\nQueue<int> deque = Queue<int>();\n\n/* 要素をエンキュー */\ndeque.addLast(2);  // 末尾に追加\ndeque.addLast(5);\ndeque.addLast(4);\ndeque.addFirst(3); // 先頭に追加\ndeque.addFirst(1);\n\n/* 要素にアクセス */\nint peekFirst = deque.first; // 先頭要素\nint peekLast = deque.last;   // 末尾要素\n\n/* 要素をデキュー */\nint popFirst = deque.removeFirst(); // 先頭要素をデキュー\nint popLast = deque.removeLast();   // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nint size = deque.length;\n\n/* 両端キューが空かどうかを判定 */\nbool isEmpty = deque.isEmpty;\n
deque.rs
/* 両端キューを初期化 */\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* 要素をエンキュー */\ndeque.push_back(2);  // 末尾に追加\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // 先頭に追加\ndeque.push_front(1);\n\n/* 要素にアクセス */\nif let Some(front) = deque.front() { // 先頭要素\n}\nif let Some(rear) = deque.back() {   // 末尾要素\n}\n\n/* 要素をデキュー */\nif let Some(pop_front) = deque.pop_front() { // 先頭要素をデキュー\n}\nif let Some(pop_rear) = deque.pop_back() {   // 末尾要素をデキュー\n}\n\n/* 両端キューの長さを取得 */\nlet size = deque.len();\n\n/* 両端キューが空かどうかを判定 */\nlet is_empty = deque.is_empty();\n
deque.c
// C には組み込みの両端キューがありません\n
deque.kt
/* 両端キューを初期化 */\nval deque = LinkedList<Int>()\n\n/* 要素をエンキュー */\ndeque.offerLast(2)  // 末尾に追加\ndeque.offerLast(5)\ndeque.offerLast(4)\ndeque.offerFirst(3) // 先頭に追加\ndeque.offerFirst(1)\n\n/* 要素にアクセス */\nval peekFirst = deque.peekFirst() // 先頭要素\nval peekLast = deque.peekLast()   // 末尾要素\n\n/* 要素をデキュー */\nval popFirst = deque.pollFirst() // 先頭要素をデキュー\nval popLast = deque.pollLast()   // 末尾要素をデキュー\n\n/* 両端キューの長さを取得 */\nval size = deque.size\n\n/* 両端キューが空かどうかを判定 */\nval isEmpty = deque.isEmpty()\n
deque.rb
# 両端キューを初期化\n# Ruby には組み込みの両端キューがないため、Array を両端キューとして使用するしかありません\ndeque = []\n\n# 要素をエンキュー\ndeque << 2\ndeque << 5\ndeque << 4\n# 配列であるため、Array#unshift メソッドの時間計算量は O(n) です\ndeque.unshift(3)\ndeque.unshift(1)\n\n# 要素にアクセス\npeek_first = deque.first\npeek_last = deque.last\n\n# 要素をデキュー\n# 配列であるため、 Array#shift メソッドの時間計算量は O(n) です\npop_front = deque.shift\npop_back = deque.pop\n\n# 両端キューの長さを取得\nsize = deque.length\n\n# 両端キューが空かどうかを判定\nis_empty = size.zero?\n
実行の可視化

https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%0A%20%20%20%20deq%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20deq.append%282%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E5%B0%BE%0A%20%20%20%20deq.append%285%29%0A%20%20%20%20deq.append%284%29%0A%20%20%20%20deq.appendleft%283%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E9%A6%96%0A%20%20%20%20deq.appendleft%281%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20deq%5B0%5D%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%20%20%20%20rear%20%3D%20deq%5B-1%5D%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%20rear%20%3D%22,%20rear%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop_front%20%3D%20deq.popleft%28%29%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_front%20%3D%22,%20pop_front%29%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%20%20%20%20pop_rear%20%3D%20deq.pop%28%29%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_rear%20%3D%22,%20pop_rear%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28deq%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28deq%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#532","level":2,"title":"5.3.2   両端キューの実装 *","text":"

両端キューの実装はキューと似ており、連結リストまたは配列を基盤となるデータ構造として選べます。

","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#1","level":3,"title":"1.   双方向連結リストに基づく実装","text":"

前節を振り返ると、通常の単方向連結リストを使ってキューを実装しました。これは、先頭ノードの削除(デキューに対応)と末尾ノードの後ろへの新規ノード追加(エンキューに対応)を容易に行えるためです。

両端キューでは、先頭と末尾のどちらでもエンキューとデキューを行えます。言い換えると、両端キューではもう一方の対称方向の操作も実装する必要があります。そのため、両端キューの基盤データ構造として「双方向連結リスト」を採用します。

次の図に示すように、双方向連結リストの先頭ノードと末尾ノードを両端キューの先頭と末尾と見なし、両端でノードを追加および削除する機能を実現します。

LinkedListDequepush_last()push_first()pop_last()pop_first()

図 5-8   連結リストによる両端キューのエンキューとデキュー

実装コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_deque.py
class ListNode:\n    \"\"\"双方向連結リストノード\"\"\"\n\n    def __init__(self, val: int):\n        \"\"\"コンストラクタ\"\"\"\n        self.val: int = val\n        self.next: ListNode | None = None  # 後続ノードへの参照\n        self.prev: ListNode | None = None  # 前駆ノードへの参照\n\nclass LinkedListDeque:\n    \"\"\"双方向連結リストベースの両端キュー\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self._front: ListNode | None = None  # 先頭ノード front\n        self._rear: ListNode | None = None  # 末尾ノード rear\n        self._size: int = 0  # 両端キューの長さ\n\n    def size(self) -> int:\n        \"\"\"両端キューの長さを取得\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"両端キューが空かどうかを判定\"\"\"\n        return self._size == 0\n\n    def push(self, num: int, is_front: bool):\n        \"\"\"エンキュー操作\"\"\"\n        node = ListNode(num)\n        # 連結リストが空なら、front と rear の両方を node に向ける\n        if self.is_empty():\n            self._front = self._rear = node\n        # 先頭へのエンキュー操作\n        elif is_front:\n            # node を連結リストの先頭に追加\n            self._front.prev = node\n            node.next = self._front\n            self._front = node  # 先頭ノードを更新する\n        # 末尾へのエンキュー操作\n        else:\n            # node を連結リストの末尾に追加\n            self._rear.next = node\n            node.prev = self._rear\n            self._rear = node  # 末尾ノードを更新する\n        self._size += 1  # キューの長さを更新\n\n    def push_first(self, num: int):\n        \"\"\"キュー先頭にエンキュー\"\"\"\n        self.push(num, True)\n\n    def push_last(self, num: int):\n        \"\"\"キュー末尾にエンキュー\"\"\"\n        self.push(num, False)\n\n    def pop(self, is_front: bool) -> int:\n        \"\"\"デキュー操作\"\"\"\n        if self.is_empty():\n            raise IndexError(\"両端キューが空です\")\n        # キュー先頭からの取り出し\n        if is_front:\n            val: int = self._front.val  # 先頭ノードの値を一時保存\n            # 先頭ノードを削除\n            fnext: ListNode | None = self._front.next\n            if fnext is not None:\n                fnext.prev = None\n                self._front.next = None\n            self._front = fnext  # 先頭ノードを更新する\n        # キュー末尾からの取り出し\n        else:\n            val: int = self._rear.val  # 末尾ノードの値を一時保存\n            # 末尾ノードを削除\n            rprev: ListNode | None = self._rear.prev\n            if rprev is not None:\n                rprev.next = None\n                self._rear.prev = None\n            self._rear = rprev  # 末尾ノードを更新する\n        self._size -= 1  # キューの長さを更新\n        return val\n\n    def pop_first(self) -> int:\n        \"\"\"キュー先頭からデキュー\"\"\"\n        return self.pop(True)\n\n    def pop_last(self) -> int:\n        \"\"\"キュー末尾からデキュー\"\"\"\n        return self.pop(False)\n\n    def peek_first(self) -> int:\n        \"\"\"キュー先頭の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"両端キューが空です\")\n        return self._front.val\n\n    def peek_last(self) -> int:\n        \"\"\"キュー末尾の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"両端キューが空です\")\n        return self._rear.val\n\n    def to_array(self) -> list[int]:\n        \"\"\"出力用の配列を返す\"\"\"\n        node = self._front\n        res = [0] * self.size()\n        for i in range(self.size()):\n            res[i] = node.val\n            node = node.next\n        return res\n
linkedlist_deque.cpp
/* 双方向連結リストノード */\nstruct DoublyListNode {\n    int val;              // ノード値\n    DoublyListNode *next; // 後継ノードへのポインタ\n    DoublyListNode *prev; // 前駆ノードへのポインタ\n    DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {\n    }\n};\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n  private:\n    DoublyListNode *front, *rear; // 先頭ノード front、末尾ノード rear\n    int queSize = 0;              // 両端キューの長さ\n\n  public:\n    /* コンストラクタ */\n    LinkedListDeque() : front(nullptr), rear(nullptr) {\n    }\n\n    /* デストラクタメソッド */\n    ~LinkedListDeque() {\n        // 連結リストを走査してノードを削除し、メモリを解放する\n        DoublyListNode *pre, *cur = front;\n        while (cur != nullptr) {\n            pre = cur;\n            cur = cur->next;\n            delete pre;\n        }\n    }\n\n    /* 両端キューの長さを取得 */\n    int size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* エンキュー操作 */\n    void push(int num, bool isFront) {\n        DoublyListNode *node = new DoublyListNode(num);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (isEmpty())\n            front = rear = node;\n        // 先頭へのエンキュー操作\n        else if (isFront) {\n            // node を連結リストの先頭に追加\n            front->prev = node;\n            node->next = front;\n            front = node; // 先頭ノードを更新する\n        // 末尾へのエンキュー操作\n        } else {\n            // node を連結リストの末尾に追加\n            rear->next = node;\n            node->prev = rear;\n            rear = node; // 末尾ノードを更新する\n        }\n        queSize++; // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* キュー末尾にエンキュー */\n    void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* デキュー操作 */\n    int pop(bool isFront) {\n        if (isEmpty())\n            throw out_of_range(\"キューが空です\");\n        int val;\n        // キュー先頭からの取り出し\n        if (isFront) {\n            val = front->val; // 先頭ノードの値を一時保存\n            // 先頭ノードを削除\n            DoublyListNode *fNext = front->next;\n            if (fNext != nullptr) {\n                fNext->prev = nullptr;\n                front->next = nullptr;\n            }\n            delete front;\n            front = fNext; // 先頭ノードを更新する\n        // キュー末尾からの取り出し\n        } else {\n            val = rear->val; // 末尾ノードの値を一時保存\n            // 末尾ノードを削除\n            DoublyListNode *rPrev = rear->prev;\n            if (rPrev != nullptr) {\n                rPrev->next = nullptr;\n                rear->prev = nullptr;\n            }\n            delete rear;\n            rear = rPrev; // 末尾ノードを更新する\n        }\n        queSize--; // キューの長さを更新\n        return val;\n    }\n\n    /* キュー先頭からデキュー */\n    int popFirst() {\n        return pop(true);\n    }\n\n    /* キュー末尾からデキュー */\n    int popLast() {\n        return pop(false);\n    }\n\n    /* キュー先頭の要素にアクセス */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"両端キューが空です\");\n        return front->val;\n    }\n\n    /* キュー末尾の要素にアクセス */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"両端キューが空です\");\n        return rear->val;\n    }\n\n    /* 出力用の配列を返す */\n    vector<int> toVector() {\n        DoublyListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_deque.java
/* 双方向連結リストノード */\nclass ListNode {\n    int val; // ノード値\n    ListNode next; // 後続ノードへの参照\n    ListNode prev; // 前駆ノードへの参照\n\n    ListNode(int val) {\n        this.val = val;\n        prev = next = null;\n    }\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    private ListNode front, rear; // 先頭ノード front、末尾ノード rear\n    private int queSize = 0; // 両端キューの長さ\n\n    public LinkedListDeque() {\n        front = rear = null;\n    }\n\n    /* 両端キューの長さを取得 */\n    public int size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* エンキュー操作 */\n    private void push(int num, boolean isFront) {\n        ListNode node = new ListNode(num);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (isEmpty())\n            front = rear = node;\n        // 先頭へのエンキュー操作\n        else if (isFront) {\n            // node を連結リストの先頭に追加\n            front.prev = node;\n            node.next = front;\n            front = node; // 先頭ノードを更新する\n        // 末尾へのエンキュー操作\n        } else {\n            // node を連結リストの末尾に追加\n            rear.next = node;\n            node.prev = rear;\n            rear = node; // 末尾ノードを更新する\n        }\n        queSize++; // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    public void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* キュー末尾にエンキュー */\n    public void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* デキュー操作 */\n    private int pop(boolean isFront) {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        int val;\n        // キュー先頭からの取り出し\n        if (isFront) {\n            val = front.val; // 先頭ノードの値を一時保存\n            // 先頭ノードを削除\n            ListNode fNext = front.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front.next = null;\n            }\n            front = fNext; // 先頭ノードを更新する\n        // キュー末尾からの取り出し\n        } else {\n            val = rear.val; // 末尾ノードの値を一時保存\n            // 末尾ノードを削除\n            ListNode rPrev = rear.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear.prev = null;\n            }\n            rear = rPrev; // 末尾ノードを更新する\n        }\n        queSize--; // キューの長さを更新\n        return val;\n    }\n\n    /* キュー先頭からデキュー */\n    public int popFirst() {\n        return pop(true);\n    }\n\n    /* キュー末尾からデキュー */\n    public int popLast() {\n        return pop(false);\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* キュー末尾の要素にアクセス */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return rear.val;\n    }\n\n    /* 出力用の配列を返す */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_deque.cs
/* 双方向連結リストノード */\nclass ListNode(int val) {\n    public int val = val;       // ノード値\n    public ListNode? next = null; // 後続ノードへの参照\n    public ListNode? prev = null; // 前駆ノードへの参照\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    ListNode? front, rear; // 先頭ノード front、末尾ノード rear\n    int queSize = 0;      // 両端キューの長さ\n\n    public LinkedListDeque() {\n        front = null;\n        rear = null;\n    }\n\n    /* 両端キューの長さを取得 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* エンキュー操作 */\n    void Push(int num, bool isFront) {\n        ListNode node = new(num);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (IsEmpty()) {\n            front = node;\n            rear = node;\n        }\n        // 先頭へのエンキュー操作\n        else if (isFront) {\n            // node を連結リストの先頭に追加\n            front!.prev = node;\n            node.next = front;\n            front = node; // 先頭ノードを更新する\n        }\n        // 末尾へのエンキュー操作\n        else {\n            // node を連結リストの末尾に追加\n            rear!.next = node;\n            node.prev = rear;\n            rear = node;  // 末尾ノードを更新する\n        }\n\n        queSize++; // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    public void PushFirst(int num) {\n        Push(num, true);\n    }\n\n    /* キュー末尾にエンキュー */\n    public void PushLast(int num) {\n        Push(num, false);\n    }\n\n    /* デキュー操作 */\n    int? Pop(bool isFront) {\n        if (IsEmpty())\n            throw new Exception();\n        int? val;\n        // キュー先頭からの取り出し\n        if (isFront) {\n            val = front?.val; // 先頭ノードの値を一時保存\n            // 先頭ノードを削除\n            ListNode? fNext = front?.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front!.next = null;\n            }\n            front = fNext;   // 先頭ノードを更新する\n        }\n        // キュー末尾からの取り出し\n        else {\n            val = rear?.val;  // 末尾ノードの値を一時保存\n            // 末尾ノードを削除\n            ListNode? rPrev = rear?.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear!.prev = null;\n            }\n            rear = rPrev;    // 末尾ノードを更新する\n        }\n\n        queSize--; // キューの長さを更新\n        return val;\n    }\n\n    /* キュー先頭からデキュー */\n    public int? PopFirst() {\n        return Pop(true);\n    }\n\n    /* キュー末尾からデキュー */\n    public int? PopLast() {\n        return Pop(false);\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int? PeekFirst() {\n        if (IsEmpty())\n            throw new Exception();\n        return front?.val;\n    }\n\n    /* キュー末尾の要素にアクセス */\n    public int? PeekLast() {\n        if (IsEmpty())\n            throw new Exception();\n        return rear?.val;\n    }\n\n    /* 出力用の配列を返す */\n    public int?[] ToArray() {\n        ListNode? node = front;\n        int?[] res = new int?[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node?.val;\n            node = node?.next;\n        }\n\n        return res;\n    }\n}\n
linkedlist_deque.go
/* 双方向連結リストベースの両端キュー */\ntype linkedListDeque struct {\n    // 組み込みパッケージ list を使う\n    data *list.List\n}\n\n/* 両端キューを初期化する */\nfunc newLinkedListDeque() *linkedListDeque {\n    return &linkedListDeque{\n        data: list.New(),\n    }\n}\n\n/* キュー先頭に要素を追加する */\nfunc (s *linkedListDeque) pushFirst(value any) {\n    s.data.PushFront(value)\n}\n\n/* キュー末尾に要素を追加する */\nfunc (s *linkedListDeque) pushLast(value any) {\n    s.data.PushBack(value)\n}\n\n/* 先頭要素を取り出す */\nfunc (s *linkedListDeque) popFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* 末尾要素を取り出す */\nfunc (s *linkedListDeque) popLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* キュー先頭の要素にアクセス */\nfunc (s *linkedListDeque) peekFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* キュー末尾の要素にアクセス */\nfunc (s *linkedListDeque) peekLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* キューの長さを取得 */\nfunc (s *linkedListDeque) size() int {\n    return s.data.Len()\n}\n\n/* キューが空かどうかを判定 */\nfunc (s *linkedListDeque) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* 表示用に List を取得 */\nfunc (s *linkedListDeque) toList() *list.List {\n    return s.data\n}\n
linkedlist_deque.swift
/* 双方向連結リストノード */\nclass ListNode {\n    var val: Int // ノード値\n    var next: ListNode? // 後続ノードへの参照\n    weak var prev: ListNode? // 前駆ノードへの参照\n\n    init(val: Int) {\n        self.val = val\n    }\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    private var front: ListNode? // 先頭ノード front\n    private var rear: ListNode? // 末尾ノード rear\n    private var _size: Int // 両端キューの長さ\n\n    init() {\n        _size = 0\n    }\n\n    /* 両端キューの長さを取得 */\n    func size() -> Int {\n        _size\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* エンキュー操作 */\n    private func push(num: Int, isFront: Bool) {\n        let node = ListNode(val: num)\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if isEmpty() {\n            front = node\n            rear = node\n        }\n        // 先頭へのエンキュー操作\n        else if isFront {\n            // node を連結リストの先頭に追加\n            front?.prev = node\n            node.next = front\n            front = node // 先頭ノードを更新する\n        }\n        // 末尾へのエンキュー操作\n        else {\n            // node を連結リストの末尾に追加\n            rear?.next = node\n            node.prev = rear\n            rear = node // 末尾ノードを更新する\n        }\n        _size += 1 // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    func pushFirst(num: Int) {\n        push(num: num, isFront: true)\n    }\n\n    /* キュー末尾にエンキュー */\n    func pushLast(num: Int) {\n        push(num: num, isFront: false)\n    }\n\n    /* デキュー操作 */\n    private func pop(isFront: Bool) -> Int {\n        if isEmpty() {\n            fatalError(\"両端キューが空です\")\n        }\n        let val: Int\n        // キュー先頭からの取り出し\n        if isFront {\n            val = front!.val // 先頭ノードの値を一時保存\n            // 先頭ノードを削除\n            let fNext = front?.next\n            if fNext != nil {\n                fNext?.prev = nil\n                front?.next = nil\n            }\n            front = fNext // 先頭ノードを更新する\n        }\n        // キュー末尾からの取り出し\n        else {\n            val = rear!.val // 末尾ノードの値を一時保存\n            // 末尾ノードを削除\n            let rPrev = rear?.prev\n            if rPrev != nil {\n                rPrev?.next = nil\n                rear?.prev = nil\n            }\n            rear = rPrev // 末尾ノードを更新する\n        }\n        _size -= 1 // キューの長さを更新\n        return val\n    }\n\n    /* キュー先頭からデキュー */\n    func popFirst() -> Int {\n        pop(isFront: true)\n    }\n\n    /* キュー末尾からデキュー */\n    func popLast() -> Int {\n        pop(isFront: false)\n    }\n\n    /* キュー先頭の要素にアクセス */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"両端キューが空です\")\n        }\n        return front!.val\n    }\n\n    /* キュー末尾の要素にアクセス */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"両端キューが空です\")\n        }\n        return rear!.val\n    }\n\n    /* 出力用の配列を返す */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_deque.js
/* 双方向連結リストノード */\nclass ListNode {\n    prev; // 前駆ノードへの参照(ポインタ)\n    next; // 後継ノードへの参照(ポインタ)\n    val; // ノード値\n\n    constructor(val) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    #front; // 先頭ノード front\n    #rear; // 末尾ノード rear\n    #queSize; // 両端キューの長さ\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n        this.#queSize = 0;\n    }\n\n    /* 末尾へのエンキュー操作 */\n    pushLast(val) {\n        const node = new ListNode(val);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // node を連結リストの末尾に追加\n            this.#rear.next = node;\n            node.prev = this.#rear;\n            this.#rear = node; // 末尾ノードを更新する\n        }\n        this.#queSize++;\n    }\n\n    /* 先頭へのエンキュー操作 */\n    pushFirst(val) {\n        const node = new ListNode(val);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // node を連結リストの先頭に追加\n            this.#front.prev = node;\n            node.next = this.#front;\n            this.#front = node; // 先頭ノードを更新する\n        }\n        this.#queSize++;\n    }\n\n    /* キュー末尾からの取り出し */\n    popLast() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#rear.val; // 末尾ノードの値を保存する\n        // 末尾ノードを削除\n        let temp = this.#rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.#rear.prev = null;\n        }\n        this.#rear = temp; // 末尾ノードを更新する\n        this.#queSize--;\n        return value;\n    }\n\n    /* キュー先頭からの取り出し */\n    popFirst() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#front.val; // 末尾ノードの値を保存する\n        // 先頭ノードを削除\n        let temp = this.#front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.#front.next = null;\n        }\n        this.#front = temp; // 先頭ノードを更新する\n        this.#queSize--;\n        return value;\n    }\n\n    /* キュー末尾の要素にアクセス */\n    peekLast() {\n        return this.#queSize === 0 ? null : this.#rear.val;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peekFirst() {\n        return this.#queSize === 0 ? null : this.#front.val;\n    }\n\n    /* 両端キューの長さを取得 */\n    size() {\n        return this.#queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* 両端キューを出力する */\n    print() {\n        const arr = [];\n        let temp = this.#front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
linkedlist_deque.ts
/* 双方向連結リストノード */\nclass ListNode {\n    prev: ListNode; // 前駆ノードへの参照(ポインタ)\n    next: ListNode; // 後継ノードへの参照(ポインタ)\n    val: number; // ノード値\n\n    constructor(val: number) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    private front: ListNode; // 先頭ノード front\n    private rear: ListNode; // 末尾ノード rear\n    private queSize: number; // 両端キューの長さ\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n        this.queSize = 0;\n    }\n\n    /* 末尾へのエンキュー操作 */\n    pushLast(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // node を連結リストの末尾に追加\n            this.rear.next = node;\n            node.prev = this.rear;\n            this.rear = node; // 末尾ノードを更新する\n        }\n        this.queSize++;\n    }\n\n    /* 先頭へのエンキュー操作 */\n    pushFirst(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // node を連結リストの先頭に追加\n            this.front.prev = node;\n            node.next = this.front;\n            this.front = node; // 先頭ノードを更新する\n        }\n        this.queSize++;\n    }\n\n    /* キュー末尾からの取り出し */\n    popLast(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.rear.val; // 末尾ノードの値を保存する\n        // 末尾ノードを削除\n        let temp: ListNode = this.rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.rear.prev = null;\n        }\n        this.rear = temp; // 末尾ノードを更新する\n        this.queSize--;\n        return value;\n    }\n\n    /* キュー先頭からの取り出し */\n    popFirst(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.front.val; // 末尾ノードの値を保存する\n        // 先頭ノードを削除\n        let temp: ListNode = this.front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.front.next = null;\n        }\n        this.front = temp; // 先頭ノードを更新する\n        this.queSize--;\n        return value;\n    }\n\n    /* キュー末尾の要素にアクセス */\n    peekLast(): number {\n        return this.queSize === 0 ? null : this.rear.val;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peekFirst(): number {\n        return this.queSize === 0 ? null : this.front.val;\n    }\n\n    /* 両端キューの長さを取得 */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* 両端キューを出力する */\n    print(): void {\n        const arr: number[] = [];\n        let temp: ListNode = this.front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
linkedlist_deque.dart
/* 双方向連結リストノード */\nclass ListNode {\n  int val; // ノード値\n  ListNode? next; // 後続ノードへの参照\n  ListNode? prev; // 前駆ノードへの参照\n\n  ListNode(this.val, {this.next, this.prev});\n}\n\n/* 双方向連結リストに基づく双方向キュー */\nclass LinkedListDeque {\n  late ListNode? _front; // 先頭ノード _front\n  late ListNode? _rear; // 末尾ノード _rear\n  int _queSize = 0; // 両端キューの長さ\n\n  LinkedListDeque() {\n    this._front = null;\n    this._rear = null;\n  }\n\n  /* 両端キューの長さを取得 */\n  int size() {\n    return this._queSize;\n  }\n\n  /* 両端キューが空かどうかを判定 */\n  bool isEmpty() {\n    return size() == 0;\n  }\n\n  /* エンキュー操作 */\n  void push(int _num, bool isFront) {\n    final ListNode node = ListNode(_num);\n    if (isEmpty()) {\n      // 連結リストが空なら、`_front` と `_rear` の両方を `node` に向ける\n      _front = _rear = node;\n    } else if (isFront) {\n      // 先頭へのエンキュー操作\n      // node を連結リストの先頭に追加する\n      _front!.prev = node;\n      node.next = _front;\n      _front = node; // 先頭ノードを更新する\n    } else {\n      // 末尾へのエンキュー操作\n      // node を連結リストの末尾に追加する\n      _rear!.next = node;\n      node.prev = _rear;\n      _rear = node; // 末尾ノードを更新する\n    }\n    _queSize++; // キューの長さを更新\n  }\n\n  /* キュー先頭にエンキュー */\n  void pushFirst(int _num) {\n    push(_num, true);\n  }\n\n  /* キュー末尾にエンキュー */\n  void pushLast(int _num) {\n    push(_num, false);\n  }\n\n  /* デキュー操作 */\n  int? pop(bool isFront) {\n    // キューが空なら、そのまま `null` を返す\n    if (isEmpty()) {\n      return null;\n    }\n    final int val;\n    if (isFront) {\n      // キュー先頭からの取り出し\n      val = _front!.val; // 先頭ノードの値を一時保存\n      // 先頭ノードを削除\n      ListNode? fNext = _front!.next;\n      if (fNext != null) {\n        fNext.prev = null;\n        _front!.next = null;\n      }\n      _front = fNext; // 先頭ノードを更新する\n    } else {\n      // キュー末尾からの取り出し\n      val = _rear!.val; // 末尾ノードの値を一時保存\n      // 末尾ノードを削除\n      ListNode? rPrev = _rear!.prev;\n      if (rPrev != null) {\n        rPrev.next = null;\n        _rear!.prev = null;\n      }\n      _rear = rPrev; // 末尾ノードを更新する\n    }\n    _queSize--; // キューの長さを更新\n    return val;\n  }\n\n  /* キュー先頭からデキュー */\n  int? popFirst() {\n    return pop(true);\n  }\n\n  /* キュー末尾からデキュー */\n  int? popLast() {\n    return pop(false);\n  }\n\n  /* キュー先頭の要素にアクセス */\n  int? peekFirst() {\n    return _front?.val;\n  }\n\n  /* キュー末尾の要素にアクセス */\n  int? peekLast() {\n    return _rear?.val;\n  }\n\n  /* 出力用の配列を返す */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> res = [];\n    for (int i = 0; i < _queSize; i++) {\n      res.add(node!.val);\n      node = node.next;\n    }\n    return res;\n  }\n}\n
linkedlist_deque.rs
/* 双方向連結リストノード */\npub struct ListNode<T> {\n    pub val: T,                                 // ノード値\n    pub next: Option<Rc<RefCell<ListNode<T>>>>, // 後継ノードへのポインタ\n    pub prev: Option<Rc<RefCell<ListNode<T>>>>, // 前駆ノードへのポインタ\n}\n\nimpl<T> ListNode<T> {\n    pub fn new(val: T) -> Rc<RefCell<ListNode<T>>> {\n        Rc::new(RefCell::new(ListNode {\n            val,\n            next: None,\n            prev: None,\n        }))\n    }\n}\n\n/* 双方向連結リストベースの両端キュー */\n#[allow(dead_code)]\npub struct LinkedListDeque<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // 先頭ノード front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // 末尾ノード rear\n    que_size: usize,                         // 両端キューの長さ\n}\n\nimpl<T: Copy> LinkedListDeque<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* 両端キューの長さを取得 */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* エンキュー操作 */\n    fn push(&mut self, num: T, is_front: bool) {\n        let node = ListNode::new(num);\n        // 先頭へのエンキュー操作\n        if is_front {\n            match self.front.take() {\n                // 連結リストが空なら、front と rear の両方を node に向ける\n                None => {\n                    self.rear = Some(node.clone());\n                    self.front = Some(node);\n                }\n                // node を連結リストの先頭に追加\n                Some(old_front) => {\n                    old_front.borrow_mut().prev = Some(node.clone());\n                    node.borrow_mut().next = Some(old_front);\n                    self.front = Some(node); // 先頭ノードを更新する\n                }\n            }\n        }\n        // 末尾へのエンキュー操作\n        else {\n            match self.rear.take() {\n                // 連結リストが空なら、front と rear の両方を node に向ける\n                None => {\n                    self.front = Some(node.clone());\n                    self.rear = Some(node);\n                }\n                // node を連結リストの末尾に追加\n                Some(old_rear) => {\n                    old_rear.borrow_mut().next = Some(node.clone());\n                    node.borrow_mut().prev = Some(old_rear);\n                    self.rear = Some(node); // 末尾ノードを更新する\n                }\n            }\n        }\n        self.que_size += 1; // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    pub fn push_first(&mut self, num: T) {\n        self.push(num, true);\n    }\n\n    /* キュー末尾にエンキュー */\n    pub fn push_last(&mut self, num: T) {\n        self.push(num, false);\n    }\n\n    /* デキュー操作 */\n    fn pop(&mut self, is_front: bool) -> Option<T> {\n        // キューが空なら、そのまま `None` を返す\n        if self.is_empty() {\n            return None;\n        };\n        // キュー先頭からの取り出し\n        if is_front {\n            self.front.take().map(|old_front| {\n                match old_front.borrow_mut().next.take() {\n                    Some(new_front) => {\n                        new_front.borrow_mut().prev.take();\n                        self.front = Some(new_front); // 先頭ノードを更新する\n                    }\n                    None => {\n                        self.rear.take();\n                    }\n                }\n                self.que_size -= 1; // キューの長さを更新\n                old_front.borrow().val\n            })\n        }\n        // キュー末尾からの取り出し\n        else {\n            self.rear.take().map(|old_rear| {\n                match old_rear.borrow_mut().prev.take() {\n                    Some(new_rear) => {\n                        new_rear.borrow_mut().next.take();\n                        self.rear = Some(new_rear); // 末尾ノードを更新する\n                    }\n                    None => {\n                        self.front.take();\n                    }\n                }\n                self.que_size -= 1; // キューの長さを更新\n                old_rear.borrow().val\n            })\n        }\n    }\n\n    /* キュー先頭からデキュー */\n    pub fn pop_first(&mut self) -> Option<T> {\n        return self.pop(true);\n    }\n\n    /* キュー末尾からデキュー */\n    pub fn pop_last(&mut self) -> Option<T> {\n        return self.pop(false);\n    }\n\n    /* キュー先頭の要素にアクセス */\n    pub fn peek_first(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* キュー末尾の要素にアクセス */\n    pub fn peek_last(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.rear.as_ref()\n    }\n\n    /* 出力用の配列を返す */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n        res\n    }\n}\n
linkedlist_deque.c
/* 双方向連結リストノード */\ntypedef struct DoublyListNode {\n    int val;                     // ノード値\n    struct DoublyListNode *next; // 後続ノード\n    struct DoublyListNode *prev; // 前駆ノード\n} DoublyListNode;\n\n/* コンストラクタ */\nDoublyListNode *newDoublyListNode(int num) {\n    DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode));\n    new->val = num;\n    new->next = NULL;\n    new->prev = NULL;\n    return new;\n}\n\n/* デストラクタ */\nvoid delDoublyListNode(DoublyListNode *node) {\n    free(node);\n}\n\n/* 双方向連結リストベースの両端キュー */\ntypedef struct {\n    DoublyListNode *front, *rear; // 先頭ノード front、末尾ノード rear\n    int queSize;                  // 両端キューの長さ\n} LinkedListDeque;\n\n/* コンストラクタ */\nLinkedListDeque *newLinkedListDeque() {\n    LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque));\n    deque->front = NULL;\n    deque->rear = NULL;\n    deque->queSize = 0;\n    return deque;\n}\n\n/* デストラクタ */\nvoid delLinkedListdeque(LinkedListDeque *deque) {\n    // すべてのノードを解放\n    for (int i = 0; i < deque->queSize && deque->front != NULL; i++) {\n        DoublyListNode *tmp = deque->front;\n        deque->front = deque->front->next;\n        free(tmp);\n    }\n    // deque 構造体を解放する\n    free(deque);\n}\n\n/* キューの長さを取得 */\nint size(LinkedListDeque *deque) {\n    return deque->queSize;\n}\n\n/* キューが空かどうかを判定 */\nbool empty(LinkedListDeque *deque) {\n    return (size(deque) == 0);\n}\n\n/* エンキュー */\nvoid push(LinkedListDeque *deque, int num, bool isFront) {\n    DoublyListNode *node = newDoublyListNode(num);\n    // 連結リストが空なら、`front` と `rear` の両方を `node` に向ける\n    if (empty(deque)) {\n        deque->front = deque->rear = node;\n    }\n    // 先頭へのエンキュー操作\n    else if (isFront) {\n        // node を連結リストの先頭に追加\n        deque->front->prev = node;\n        node->next = deque->front;\n        deque->front = node; // 先頭ノードを更新する\n    }\n    // 末尾へのエンキュー操作\n    else {\n        // node を連結リストの末尾に追加\n        deque->rear->next = node;\n        node->prev = deque->rear;\n        deque->rear = node;\n    }\n    deque->queSize++; // キューの長さを更新\n}\n\n/* キュー先頭にエンキュー */\nvoid pushFirst(LinkedListDeque *deque, int num) {\n    push(deque, num, true);\n}\n\n/* キュー末尾にエンキュー */\nvoid pushLast(LinkedListDeque *deque, int num) {\n    push(deque, num, false);\n}\n\n/* キュー先頭の要素にアクセス */\nint peekFirst(LinkedListDeque *deque) {\n    assert(size(deque) && deque->front);\n    return deque->front->val;\n}\n\n/* キュー末尾の要素にアクセス */\nint peekLast(LinkedListDeque *deque) {\n    assert(size(deque) && deque->rear);\n    return deque->rear->val;\n}\n\n/* デキュー */\nint pop(LinkedListDeque *deque, bool isFront) {\n    if (empty(deque))\n        return -1;\n    int val;\n    // キュー先頭からの取り出し\n    if (isFront) {\n        val = peekFirst(deque); // 先頭ノードの値を一時保存\n        DoublyListNode *fNext = deque->front->next;\n        if (fNext) {\n            fNext->prev = NULL;\n            deque->front->next = NULL;\n        }\n        delDoublyListNode(deque->front);\n        deque->front = fNext; // 先頭ノードを更新する\n    }\n    // キュー末尾からの取り出し\n    else {\n        val = peekLast(deque); // 末尾ノードの値を一時保存\n        DoublyListNode *rPrev = deque->rear->prev;\n        if (rPrev) {\n            rPrev->next = NULL;\n            deque->rear->prev = NULL;\n        }\n        delDoublyListNode(deque->rear);\n        deque->rear = rPrev; // 末尾ノードを更新する\n    }\n    deque->queSize--; // キューの長さを更新\n    return val;\n}\n\n/* キュー先頭からデキュー */\nint popFirst(LinkedListDeque *deque) {\n    return pop(deque, true);\n}\n\n/* キュー末尾からデキュー */\nint popLast(LinkedListDeque *deque) {\n    return pop(deque, false);\n}\n\n/* キューを出力する */\nvoid printLinkedListDeque(LinkedListDeque *deque) {\n    int *arr = malloc(sizeof(int) * deque->queSize);\n    // 連結リスト内のデータを配列にコピー\n    int i;\n    DoublyListNode *node;\n    for (i = 0, node = deque->front; i < deque->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, deque->queSize);\n    free(arr);\n}\n
linkedlist_deque.kt
/* 双方向連結リストノード */\nclass ListNode(var _val: Int) {\n    // ノード値\n    var next: ListNode? = null // 後続ノードへの参照\n    var prev: ListNode? = null // 前駆ノードへの参照\n}\n\n/* 双方向連結リストベースの両端キュー */\nclass LinkedListDeque {\n    private var front: ListNode? = null // 先頭ノード front\n    private var rear: ListNode? = null // 末尾ノード rear\n    private var queSize: Int = 0 // 両端キューの長さ\n\n    /* 両端キューの長さを取得 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* エンキュー操作 */\n    fun push(num: Int, isFront: Boolean) {\n        val node = ListNode(num)\n        // 連結リストが空なら、front と rear の両方を node に向ける\n        if (isEmpty()) {\n            rear = node\n            front = rear\n            // 先頭へのエンキュー操作\n        } else if (isFront) {\n            // node を連結リストの先頭に追加\n            front?.prev = node\n            node.next = front\n            front = node // 先頭ノードを更新する\n            // 末尾へのエンキュー操作\n        } else {\n            // node を連結リストの末尾に追加\n            rear?.next = node\n            node.prev = rear\n            rear = node // 末尾ノードを更新する\n        }\n        queSize++ // キューの長さを更新\n    }\n\n    /* キュー先頭にエンキュー */\n    fun pushFirst(num: Int) {\n        push(num, true)\n    }\n\n    /* キュー末尾にエンキュー */\n    fun pushLast(num: Int) {\n        push(num, false)\n    }\n\n    /* デキュー操作 */\n    fun pop(isFront: Boolean): Int {\n        if (isEmpty()) \n            throw IndexOutOfBoundsException()\n        val _val: Int\n        // キュー先頭からの取り出し\n        if (isFront) {\n            _val = front!!._val // 先頭ノードの値を一時保存\n            // 先頭ノードを削除\n            val fNext = front!!.next\n            if (fNext != null) {\n                fNext.prev = null\n                front!!.next = null\n            }\n            front = fNext // 先頭ノードを更新する\n            // キュー末尾からの取り出し\n        } else {\n            _val = rear!!._val // 末尾ノードの値を一時保存\n            // 末尾ノードを削除\n            val rPrev = rear!!.prev\n            if (rPrev != null) {\n                rPrev.next = null\n                rear!!.prev = null\n            }\n            rear = rPrev // 末尾ノードを更新する\n        }\n        queSize-- // キューの長さを更新\n        return _val\n    }\n\n    /* キュー先頭からデキュー */\n    fun popFirst(): Int {\n        return pop(true)\n    }\n\n    /* キュー末尾からデキュー */\n    fun popLast(): Int {\n        return pop(false)\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* キュー末尾の要素にアクセス */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return rear!!._val\n    }\n\n    /* 出力用の配列を返す */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_deque.rb
=begin\nFile: linkedlist_deque.rb\nCreated Time: 2024-04-06\nAuthor: Xuan Khoa Tu Nguyen (ngxktuzkai2000@gmail.com)\n=end\n\n# ## 双方向連結リストノード\nclass ListNode\n  attr_accessor :val\n  attr_accessor :next # 後続ノードへの参照\n  attr_accessor :prev # 前ノードへの参照\n\n  ### コンストラクタ ###\n  def initialize(val)\n    @val = val\n  end\nend\n\n### 双方向連結リストで実装した両端キュー ###\nclass LinkedListDeque\n  ### 両端キューの長さを取得 ###\n  attr_reader :size\n\n  ### コンストラクタ ###\n  def initialize\n    @front = nil  # 先頭ノード front\n    @rear = nil   # 末尾ノード rear\n    @size = 0     # 両端キューの長さ\n  end\n\n  ### 両端キューが空か判定 ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### エンキュー操作 ###\n  def push(num, is_front)\n    node = ListNode.new(num)\n    # 連結リストが空なら、`front` と `rear` の両方を `node` に向ける\n    if is_empty?\n      @front = @rear = node\n    # 先頭へのエンキュー操作\n    elsif is_front\n      # node を連結リストの先頭に追加\n      @front.prev = node\n      node.next = @front\n      @front = node # 先頭ノードを更新する\n    # 末尾へのエンキュー操作\n    else\n      # node を連結リストの末尾に追加\n      @rear.next = node\n      node.prev = @rear\n      @rear = node # 末尾ノードを更新する\n    end\n    @size += 1 # キューの長さを更新\n  end\n\n  ### キュー先頭に追加 ###\n  def push_first(num)\n    push(num, true)\n  end\n\n  ### キュー末尾に追加 ###\n  def push_last(num)\n    push(num, false)\n  end\n\n  ### デキュー操作 ###\n  def pop(is_front)\n    raise IndexError, '両端キューは空です' if is_empty?\n\n    # キュー先頭からの取り出し\n    if is_front\n      val = @front.val # 先頭ノードの値を一時保存\n      # 先頭ノードを削除\n      fnext = @front.next\n      unless fnext.nil?\n        fnext.prev = nil\n        @front.next = nil\n      end\n      @front = fnext # 先頭ノードを更新する\n    # キュー末尾からの取り出し\n    else\n      val = @rear.val # 末尾ノードの値を一時保存\n      # 末尾ノードを削除\n      rprev = @rear.prev\n      unless rprev.nil?\n        rprev.next = nil\n        @rear.prev = nil\n      end\n      @rear = rprev # 末尾ノードを更新する\n    end\n    @size -= 1 # キューの長さを更新\n\n    val\n  end\n\n  ### キュー先頭から取り出す ###\n  def pop_first\n    pop(true)\n  end\n\n  ### キュー先頭から取り出す ###\n  def pop_last\n    pop(false)\n  end\n\n  ### 先頭要素にアクセス ###\n  def peek_first\n    raise IndexError, '両端キューは空です' if is_empty?\n\n    @front.val\n  end\n\n  ### キュー末尾要素を参照 ###\n  def peek_last\n    raise IndexError, '両端キューは空です' if is_empty?\n\n    @rear.val\n  end\n\n  ### 表示用の配列を返す ###\n  def to_array\n    node = @front\n    res = Array.new(size, 0)\n    for i in 0...size\n      res[i] = node.val\n      node = node.next\n    end\n    res\n  end\nend\n
","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#2","level":3,"title":"2.   配列に基づく実装","text":"

次の図に示すように、配列によるキュー実装と同様に、循環配列を使って両端キューを実装することもできます。

ArrayDequepush_last()push_first()pop_last()pop_first()

図 5-9   配列による両端キューのエンキューとデキュー

キュー実装を土台として、「先頭へのエンキュー」と「末尾からのデキュー」のメソッドを追加するだけで済みます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_deque.py
class ArrayDeque:\n    \"\"\"循環配列ベースの両端キュー\"\"\"\n\n    def __init__(self, capacity: int):\n        \"\"\"コンストラクタ\"\"\"\n        self._nums: list[int] = [0] * capacity\n        self._front: int = 0\n        self._size: int = 0\n\n    def capacity(self) -> int:\n        \"\"\"両端キューの容量を取得\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"両端キューの長さを取得\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"両端キューが空かどうかを判定\"\"\"\n        return self._size == 0\n\n    def index(self, i: int) -> int:\n        \"\"\"循環配列のインデックスを計算\"\"\"\n        # 剰余演算により配列の先頭と末尾をつなげる\n        # i が配列の末尾を越えたら先頭に戻る\n        # i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + self.capacity()) % self.capacity()\n\n    def push_first(self, num: int):\n        \"\"\"キュー先頭にエンキュー\"\"\"\n        if self._size == self.capacity():\n            print(\"両端キューがいっぱいです\")\n            return\n        # 先頭ポインタを左に 1 つ移動する\n        # 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        self._front = self.index(self._front - 1)\n        # num をキュー先頭に追加\n        self._nums[self._front] = num\n        self._size += 1\n\n    def push_last(self, num: int):\n        \"\"\"キュー末尾にエンキュー\"\"\"\n        if self._size == self.capacity():\n            print(\"両端キューがいっぱいです\")\n            return\n        # キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        rear = self.index(self._front + self._size)\n        # num をキュー末尾に追加\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop_first(self) -> int:\n        \"\"\"キュー先頭からデキュー\"\"\"\n        num = self.peek_first()\n        # 先頭ポインタを 1 つ後ろへ進める\n        self._front = self.index(self._front + 1)\n        self._size -= 1\n        return num\n\n    def pop_last(self) -> int:\n        \"\"\"キュー末尾からデキュー\"\"\"\n        num = self.peek_last()\n        self._size -= 1\n        return num\n\n    def peek_first(self) -> int:\n        \"\"\"キュー先頭の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"両端キューが空です\")\n        return self._nums[self._front]\n\n    def peek_last(self) -> int:\n        \"\"\"キュー末尾の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"両端キューが空です\")\n        # 末尾要素のインデックスを計算\n        last = self.index(self._front + self._size - 1)\n        return self._nums[last]\n\n    def to_array(self) -> list[int]:\n        \"\"\"出力用の配列を返す\"\"\"\n        # 有効長の範囲内のリスト要素のみを変換\n        res = []\n        for i in range(self._size):\n            res.append(self._nums[self.index(self._front + i)])\n        return res\n
array_deque.cpp
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n  private:\n    vector<int> nums; // 両端キューの要素を格納する配列\n    int front;        // 先頭ポインタ。先頭要素を指す\n    int queSize;      // 両端キューの長さ\n\n  public:\n    /* コンストラクタ */\n    ArrayDeque(int capacity) {\n        nums.resize(capacity);\n        front = queSize = 0;\n    }\n\n    /* 両端キューの容量を取得 */\n    int capacity() {\n        return nums.size();\n    }\n\n    /* 両端キューの長さを取得 */\n    int size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* 循環配列のインデックスを計算 */\n    int index(int i) {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + capacity()) % capacity();\n    }\n\n    /* キュー先頭にエンキュー */\n    void pushFirst(int num) {\n        if (queSize == capacity()) {\n            cout << \"両端キューがいっぱいです\" << endl;\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        front = index(front - 1);\n        // num をキュー先頭に追加\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* キュー末尾にエンキュー */\n    void pushLast(int num) {\n        if (queSize == capacity()) {\n            cout << \"両端キューがいっぱいです\" << endl;\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        int rear = index(front + queSize);\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* キュー先頭からデキュー */\n    int popFirst() {\n        int num = peekFirst();\n        // 先頭ポインタを 1 つ後ろへ進める\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* キュー末尾からデキュー */\n    int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"両端キューが空です\");\n        return nums[front];\n    }\n\n    /* キュー末尾の要素にアクセス */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"両端キューが空です\");\n        // 末尾要素のインデックスを計算\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* 出力用の配列を返す */\n    vector<int> toVector() {\n        // 有効長の範囲内のリスト要素のみを変換\n        vector<int> res(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n};\n
array_deque.java
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n    private int[] nums; // 両端キューの要素を格納する配列\n    private int front; // 先頭ポインタ。先頭要素を指す\n    private int queSize; // 両端キューの長さ\n\n    /* コンストラクタ */\n    public ArrayDeque(int capacity) {\n        this.nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* 両端キューの容量を取得 */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* 両端キューの長さを取得 */\n    public int size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* 循環配列のインデックスを計算 */\n    private int index(int i) {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + capacity()) % capacity();\n    }\n\n    /* キュー先頭にエンキュー */\n    public void pushFirst(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"双方向キューは満杯です\");\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        front = index(front - 1);\n        // num をキュー先頭に追加\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* キュー末尾にエンキュー */\n    public void pushLast(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"双方向キューは満杯です\");\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        int rear = index(front + queSize);\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* キュー先頭からデキュー */\n    public int popFirst() {\n        int num = peekFirst();\n        // 先頭ポインタを 1 つ後ろへ進める\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* キュー末尾からデキュー */\n    public int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* キュー末尾の要素にアクセス */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        // 末尾要素のインデックスを計算\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* 出力用の配列を返す */\n    public int[] toArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.cs
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n    int[] nums;  // 両端キューの要素を格納する配列\n    int front;   // 先頭ポインタ。先頭要素を指す\n    int queSize; // 両端キューの長さ\n\n    /* コンストラクタ */\n    public ArrayDeque(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* 両端キューの容量を取得 */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* 両端キューの長さを取得 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* 循環配列のインデックスを計算 */\n    int Index(int i) {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + Capacity()) % Capacity();\n    }\n\n    /* キュー先頭にエンキュー */\n    public void PushFirst(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"両端キューは満杯です\");\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        front = Index(front - 1);\n        // num をキュー先頭に追加\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* キュー末尾にエンキュー */\n    public void PushLast(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"両端キューは満杯です\");\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        int rear = Index(front + queSize);\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* キュー先頭からデキュー */\n    public int PopFirst() {\n        int num = PeekFirst();\n        // 先頭ポインタを 1 つ後ろへ進める\n        front = Index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* キュー末尾からデキュー */\n    public int PopLast() {\n        int num = PeekLast();\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int PeekFirst() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        return nums[front];\n    }\n\n    /* キュー末尾の要素にアクセス */\n    public int PeekLast() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        // 末尾要素のインデックスを計算\n        int last = Index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* 出力用の配列を返す */\n    public int[] ToArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[Index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.go
/* 循環配列ベースの両端キュー */\ntype arrayDeque struct {\n    nums        []int // 両端キューの要素を格納する配列\n    front       int   // 先頭ポインタ。先頭要素を指す\n    queSize     int   // 両端キューの長さ\n    queCapacity int   // キュー容量(格納できる要素数の上限)\n}\n\n/* キューを初期化 */\nfunc newArrayDeque(queCapacity int) *arrayDeque {\n    return &arrayDeque{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* 両端キューの長さを取得 */\nfunc (q *arrayDeque) size() int {\n    return q.queSize\n}\n\n/* 両端キューが空かどうかを判定 */\nfunc (q *arrayDeque) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* 循環配列のインデックスを計算 */\nfunc (q *arrayDeque) index(i int) int {\n    // 剰余演算により配列の先頭と末尾をつなげる\n    // i が配列の末尾を越えたら先頭に戻る\n    // i が配列の先頭を越えて前に出たら末尾に戻る\n    return (i + q.queCapacity) % q.queCapacity\n}\n\n/* キュー先頭にエンキュー */\nfunc (q *arrayDeque) pushFirst(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"両端キューは満杯です\")\n        return\n    }\n    // 先頭ポインタを左に 1 つ移動する\n    // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n    q.front = q.index(q.front - 1)\n    // num をキュー先頭に追加\n    q.nums[q.front] = num\n    q.queSize++\n}\n\n/* キュー末尾にエンキュー */\nfunc (q *arrayDeque) pushLast(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"両端キューは満杯です\")\n        return\n    }\n    // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n    rear := q.index(q.front + q.queSize)\n    // num をキュー末尾に追加\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* キュー先頭からデキュー */\nfunc (q *arrayDeque) popFirst() any {\n    num := q.peekFirst()\n    if num == nil {\n        return nil\n    }\n    // 先頭ポインタを 1 つ後ろへ進める\n    q.front = q.index(q.front + 1)\n    q.queSize--\n    return num\n}\n\n/* キュー末尾からデキュー */\nfunc (q *arrayDeque) popLast() any {\n    num := q.peekLast()\n    if num == nil {\n        return nil\n    }\n    q.queSize--\n    return num\n}\n\n/* キュー先頭の要素にアクセス */\nfunc (q *arrayDeque) peekFirst() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* キュー末尾の要素にアクセス */\nfunc (q *arrayDeque) peekLast() any {\n    if q.isEmpty() {\n        return nil\n    }\n    // 末尾要素のインデックスを計算\n    last := q.index(q.front + q.queSize - 1)\n    return q.nums[last]\n}\n\n/* 表示用に Slice を取得 */\nfunc (q *arrayDeque) toSlice() []int {\n    // 有効長の範囲内のリスト要素のみを変換\n    res := make([]int, q.queSize)\n    for i, j := 0, q.front; i < q.queSize; i++ {\n        res[i] = q.nums[q.index(j)]\n        j++\n    }\n    return res\n}\n
array_deque.swift
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n    private var nums: [Int] // 両端キューの要素を格納する配列\n    private var front: Int // 先頭ポインタ。先頭要素を指す\n    private var _size: Int // 両端キューの長さ\n\n    /* コンストラクタ */\n    init(capacity: Int) {\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* 両端キューの容量を取得 */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* 両端キューの長さを取得 */\n    func size() -> Int {\n        _size\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* 循環配列のインデックスを計算 */\n    private func index(i: Int) -> Int {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        (i + capacity()) % capacity()\n    }\n\n    /* キュー先頭にエンキュー */\n    func pushFirst(num: Int) {\n        if size() == capacity() {\n            print(\"両端キューがいっぱいです\")\n            return\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        front = index(i: front - 1)\n        // num をキュー先頭に追加\n        nums[front] = num\n        _size += 1\n    }\n\n    /* キュー末尾にエンキュー */\n    func pushLast(num: Int) {\n        if size() == capacity() {\n            print(\"両端キューがいっぱいです\")\n            return\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        let rear = index(i: front + size())\n        // num をキュー末尾に追加\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* キュー先頭からデキュー */\n    func popFirst() -> Int {\n        let num = peekFirst()\n        // 先頭ポインタを 1 つ後ろへ進める\n        front = index(i: front + 1)\n        _size -= 1\n        return num\n    }\n\n    /* キュー末尾からデキュー */\n    func popLast() -> Int {\n        let num = peekLast()\n        _size -= 1\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"両端キューが空です\")\n        }\n        return nums[front]\n    }\n\n    /* キュー末尾の要素にアクセス */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"両端キューが空です\")\n        }\n        // 末尾要素のインデックスを計算\n        let last = index(i: front + size() - 1)\n        return nums[last]\n    }\n\n    /* 出力用の配列を返す */\n    func toArray() -> [Int] {\n        // 有効長の範囲内のリスト要素のみを変換\n        (front ..< front + size()).map { nums[index(i: $0)] }\n    }\n}\n
array_deque.js
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n    #nums; // 両端キューの要素を格納する配列\n    #front; // 先頭ポインタ。先頭要素を指す\n    #queSize; // 両端キューの長さ\n\n    /* コンストラクタ */\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n        this.#front = 0;\n        this.#queSize = 0;\n    }\n\n    /* 両端キューの容量を取得 */\n    capacity() {\n        return this.#nums.length;\n    }\n\n    /* 両端キューの長さを取得 */\n    size() {\n        return this.#queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* 循環配列のインデックスを計算 */\n    index(i) {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* キュー先頭にエンキュー */\n    pushFirst(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('両端キューがいっぱいです');\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        this.#front = this.index(this.#front - 1);\n        // num をキュー先頭に追加\n        this.#nums[this.#front] = num;\n        this.#queSize++;\n    }\n\n    /* キュー末尾にエンキュー */\n    pushLast(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('両端キューがいっぱいです');\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        const rear = this.index(this.#front + this.#queSize);\n        // num をキュー末尾に追加\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* キュー先頭からデキュー */\n    popFirst() {\n        const num = this.peekFirst();\n        // 先頭ポインタを 1 つ後ろへ進める\n        this.#front = this.index(this.#front + 1);\n        this.#queSize--;\n        return num;\n    }\n\n    /* キュー末尾からデキュー */\n    popLast() {\n        const num = this.peekLast();\n        this.#queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peekFirst() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.#nums[this.#front];\n    }\n\n    /* キュー末尾の要素にアクセス */\n    peekLast() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // 末尾要素のインデックスを計算\n        const last = this.index(this.#front + this.#queSize - 1);\n        return this.#nums[last];\n    }\n\n    /* 出力用の配列を返す */\n    toArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        const res = [];\n        for (let i = 0, j = this.#front; i < this.#queSize; i++, j++) {\n            res[i] = this.#nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.ts
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n    private nums: number[]; // 両端キューの要素を格納する配列\n    private front: number; // 先頭ポインタ。先頭要素を指す\n    private queSize: number; // 両端キューの長さ\n\n    /* コンストラクタ */\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = 0;\n        this.queSize = 0;\n    }\n\n    /* 両端キューの容量を取得 */\n    capacity(): number {\n        return this.nums.length;\n    }\n\n    /* 両端キューの長さを取得 */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* 循環配列のインデックスを計算 */\n    index(i: number): number {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* キュー先頭にエンキュー */\n    pushFirst(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('両端キューは満杯です');\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        this.front = this.index(this.front - 1);\n        // num をキュー先頭に追加\n        this.nums[this.front] = num;\n        this.queSize++;\n    }\n\n    /* キュー末尾にエンキュー */\n    pushLast(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('両端キューは満杯です');\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        const rear: number = this.index(this.front + this.queSize);\n        // num をキュー末尾に追加\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* キュー先頭からデキュー */\n    popFirst(): number {\n        const num: number = this.peekFirst();\n        // 先頭ポインタを 1 つ後ろへ進める\n        this.front = this.index(this.front + 1);\n        this.queSize--;\n        return num;\n    }\n\n    /* キュー末尾からデキュー */\n    popLast(): number {\n        const num: number = this.peekLast();\n        this.queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peekFirst(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.nums[this.front];\n    }\n\n    /* キュー末尾の要素にアクセス */\n    peekLast(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // 末尾要素のインデックスを計算\n        const last = this.index(this.front + this.queSize - 1);\n        return this.nums[last];\n    }\n\n    /* 出力用の配列を返す */\n    toArray(): number[] {\n        // 有効長の範囲内のリスト要素のみを変換\n        const res: number[] = [];\n        for (let i = 0, j = this.front; i < this.queSize; i++, j++) {\n            res[i] = this.nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.dart
/* 循環配列ベースの両端キュー */\nclass ArrayDeque {\n  late List<int> _nums; // 両端キューの要素を格納する配列\n  late int _front; // 先頭ポインタ。先頭要素を指す\n  late int _queSize; // 両端キューの長さ\n\n  /* コンストラクタ */\n  ArrayDeque(int capacity) {\n    this._nums = List.filled(capacity, 0);\n    this._front = this._queSize = 0;\n  }\n\n  /* 両端キューの容量を取得 */\n  int capacity() {\n    return _nums.length;\n  }\n\n  /* 両端キューの長さを取得 */\n  int size() {\n    return _queSize;\n  }\n\n  /* 両端キューが空かどうかを判定 */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* 循環配列のインデックスを計算 */\n  int index(int i) {\n    // 剰余演算により配列の先頭と末尾をつなげる\n    // i が配列の末尾を越えたら先頭に戻る\n    // i が配列の先頭を越えて前に出たら末尾に戻る\n    return (i + capacity()) % capacity();\n  }\n\n  /* キュー先頭にエンキュー */\n  void pushFirst(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"両端キューがいっぱいです\");\n    }\n    // 先頭ポインタを左に 1 つ移動する\n    // 剰余演算により _front が配列の先頭を越えたあと末尾に戻るようにする\n    _front = index(_front - 1);\n    // _num をキューの先頭に追加\n    _nums[_front] = _num;\n    _queSize++;\n  }\n\n  /* キュー末尾にエンキュー */\n  void pushLast(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"両端キューがいっぱいです\");\n    }\n    // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n    int rear = index(_front + _queSize);\n    // _num をキュー末尾に追加\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* キュー先頭からデキュー */\n  int popFirst() {\n    int _num = peekFirst();\n    // 先頭ポインタを右に 1 つ移動する\n    _front = index(_front + 1);\n    _queSize--;\n    return _num;\n  }\n\n  /* キュー末尾からデキュー */\n  int popLast() {\n    int _num = peekLast();\n    _queSize--;\n    return _num;\n  }\n\n  /* キュー先頭の要素にアクセス */\n  int peekFirst() {\n    if (isEmpty()) {\n      throw Exception(\"両端キューが空です\");\n    }\n    return _nums[_front];\n  }\n\n  /* キュー末尾の要素にアクセス */\n  int peekLast() {\n    if (isEmpty()) {\n      throw Exception(\"両端キューが空です\");\n    }\n    // 末尾要素のインデックスを計算\n    int last = index(_front + _queSize - 1);\n    return _nums[last];\n  }\n\n  /* 出力用の配列を返す */\n  List<int> toArray() {\n    // 有効長の範囲内のリスト要素のみを変換\n    List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[index(j)];\n    }\n    return res;\n  }\n}\n
array_deque.rs
/* 循環配列ベースの両端キュー */\nstruct ArrayDeque<T> {\n    nums: Vec<T>,    // 両端キューの要素を格納する配列\n    front: usize,    // 先頭ポインタ。先頭要素を指す\n    que_size: usize, // 両端キューの長さ\n}\n\nimpl<T: Copy + Default> ArrayDeque<T> {\n    /* コンストラクタ */\n    pub fn new(capacity: usize) -> Self {\n        Self {\n            nums: vec![T::default(); capacity],\n            front: 0,\n            que_size: 0,\n        }\n    }\n\n    /* 両端キューの容量を取得 */\n    pub fn capacity(&self) -> usize {\n        self.nums.len()\n    }\n\n    /* 両端キューの長さを取得 */\n    pub fn size(&self) -> usize {\n        self.que_size\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    pub fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* 循環配列のインデックスを計算 */\n    fn index(&self, i: i32) -> usize {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        ((i + self.capacity() as i32) % self.capacity() as i32) as usize\n    }\n\n    /* キュー先頭にエンキュー */\n    pub fn push_first(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"両端キューがいっぱいです\");\n            return;\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        self.front = self.index(self.front as i32 - 1);\n        // num をキュー先頭に追加\n        self.nums[self.front] = num;\n        self.que_size += 1;\n    }\n\n    /* キュー末尾にエンキュー */\n    pub fn push_last(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"両端キューがいっぱいです\");\n            return;\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        let rear = self.index(self.front as i32 + self.que_size as i32);\n        // num をキュー末尾に追加\n        self.nums[rear] = num;\n        self.que_size += 1;\n    }\n\n    /* キュー先頭からデキュー */\n    fn pop_first(&mut self) -> T {\n        let num = self.peek_first();\n        // 先頭ポインタを 1 つ後ろへ進める\n        self.front = self.index(self.front as i32 + 1);\n        self.que_size -= 1;\n        num\n    }\n\n    /* キュー末尾からデキュー */\n    fn pop_last(&mut self) -> T {\n        let num = self.peek_last();\n        self.que_size -= 1;\n        num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fn peek_first(&self) -> T {\n        if self.is_empty() {\n            panic!(\"両端キューが空です\")\n        };\n        self.nums[self.front]\n    }\n\n    /* キュー末尾の要素にアクセス */\n    fn peek_last(&self) -> T {\n        if self.is_empty() {\n            panic!(\"両端キューが空です\")\n        };\n        // 末尾要素のインデックスを計算\n        let last = self.index(self.front as i32 + self.que_size as i32 - 1);\n        self.nums[last]\n    }\n\n    /* 出力用の配列を返す */\n    fn to_array(&self) -> Vec<T> {\n        // 有効長の範囲内のリスト要素のみを変換\n        let mut res = vec![T::default(); self.que_size];\n        let mut j = self.front;\n        for i in 0..self.que_size {\n            res[i] = self.nums[self.index(j as i32)];\n            j += 1;\n        }\n        res\n    }\n}\n
array_deque.c
/* 循環配列ベースの両端キュー */\ntypedef struct {\n    int *nums;       // キュー要素を格納する配列\n    int front;       // 先頭ポインタ。先頭要素を指す\n    int queSize;     // 末尾ポインタ。キューの末尾 + 1 を指す\n    int queCapacity; // キューの容量\n} ArrayDeque;\n\n/* コンストラクタ */\nArrayDeque *newArrayDeque(int capacity) {\n    ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque));\n    // 配列を初期化\n    deque->queCapacity = capacity;\n    deque->nums = (int *)malloc(sizeof(int) * deque->queCapacity);\n    deque->front = deque->queSize = 0;\n    return deque;\n}\n\n/* デストラクタ */\nvoid delArrayDeque(ArrayDeque *deque) {\n    free(deque->nums);\n    free(deque);\n}\n\n/* 両端キューの容量を取得 */\nint capacity(ArrayDeque *deque) {\n    return deque->queCapacity;\n}\n\n/* 両端キューの長さを取得 */\nint size(ArrayDeque *deque) {\n    return deque->queSize;\n}\n\n/* 両端キューが空かどうかを判定 */\nbool empty(ArrayDeque *deque) {\n    return deque->queSize == 0;\n}\n\n/* 循環配列のインデックスを計算 */\nint dequeIndex(ArrayDeque *deque, int i) {\n    // 剰余演算により配列の先頭と末尾をつなげる\n    // i が配列の末尾を越えたら先頭に戻る\n    // i が配列の先頭を越えたら末尾に戻る\n    return ((i + capacity(deque)) % capacity(deque));\n}\n\n/* キュー先頭にエンキュー */\nvoid pushFirst(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"両端キューがいっぱいです\\r\\n\");\n        return;\n    }\n    // 先頭ポインタを左に 1 つ移動する\n    // 剰余演算により front が配列の先頭を越えたあと末尾に戻るようにする\n    deque->front = dequeIndex(deque, deque->front - 1);\n    // num をキューの先頭に追加\n    deque->nums[deque->front] = num;\n    deque->queSize++;\n}\n\n/* キュー末尾にエンキュー */\nvoid pushLast(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"両端キューがいっぱいです\\r\\n\");\n        return;\n    }\n    // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n    int rear = dequeIndex(deque, deque->front + deque->queSize);\n    // num をキュー末尾に追加\n    deque->nums[rear] = num;\n    deque->queSize++;\n}\n\n/* キュー先頭の要素にアクセス */\nint peekFirst(ArrayDeque *deque) {\n    // アクセス例外:双方向キューが空です\n    assert(empty(deque) == 0);\n    return deque->nums[deque->front];\n}\n\n/* キュー末尾の要素にアクセス */\nint peekLast(ArrayDeque *deque) {\n    // アクセス例外:双方向キューが空です\n    assert(empty(deque) == 0);\n    int last = dequeIndex(deque, deque->front + deque->queSize - 1);\n    return deque->nums[last];\n}\n\n/* キュー先頭からデキュー */\nint popFirst(ArrayDeque *deque) {\n    int num = peekFirst(deque);\n    // 先頭ポインタを 1 つ後ろへ進める\n    deque->front = dequeIndex(deque, deque->front + 1);\n    deque->queSize--;\n    return num;\n}\n\n/* キュー末尾からデキュー */\nint popLast(ArrayDeque *deque) {\n    int num = peekLast(deque);\n    deque->queSize--;\n    return num;\n}\n\n/* 出力用の配列を返す */\nint *toArray(ArrayDeque *deque, int *queSize) {\n    *queSize = deque->queSize;\n    int *res = (int *)calloc(deque->queSize, sizeof(int));\n    int j = deque->front;\n    for (int i = 0; i < deque->queSize; i++) {\n        res[i] = deque->nums[j % deque->queCapacity];\n        j++;\n    }\n    return res;\n}\n
array_deque.kt
/* コンストラクタ */\nclass ArrayDeque(capacity: Int) {\n    private var nums: IntArray = IntArray(capacity) // 両端キューの要素を格納する配列\n    private var front: Int = 0 // 先頭ポインタ。先頭要素を指す\n    private var queSize: Int = 0 // 両端キューの長さ\n\n    /* 両端キューの容量を取得 */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* 両端キューの長さを取得 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* 両端キューが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* 循環配列のインデックスを計算 */\n    private fun index(i: Int): Int {\n        // 剰余演算により配列の先頭と末尾をつなげる\n        // i が配列の末尾を越えたら先頭に戻る\n        // i が配列の先頭を越えて前に出たら末尾に戻る\n        return (i + capacity()) % capacity()\n    }\n\n    /* キュー先頭にエンキュー */\n    fun pushFirst(num: Int) {\n        if (queSize == capacity()) {\n            println(\"双方向キューは満杯です\")\n            return\n        }\n        // 先頭ポインタを左に 1 つ移動する\n        // 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n        front = index(front - 1)\n        // num をキュー先頭に追加\n        nums[front] = num\n        queSize++\n    }\n\n    /* キュー末尾にエンキュー */\n    fun pushLast(num: Int) {\n        if (queSize == capacity()) {\n            println(\"双方向キューは満杯です\")\n            return\n        }\n        // キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n        val rear = index(front + queSize)\n        // num をキュー末尾に追加\n        nums[rear] = num\n        queSize++\n    }\n\n    /* キュー先頭からデキュー */\n    fun popFirst(): Int {\n        val num = peekFirst()\n        // 先頭ポインタを 1 つ後ろへ進める\n        front = index(front + 1)\n        queSize--\n        return num\n    }\n\n    /* キュー末尾からデキュー */\n    fun popLast(): Int {\n        val num = peekLast()\n        queSize--\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* キュー末尾の要素にアクセス */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // 末尾要素のインデックスを計算\n        val last = index(front + queSize - 1)\n        return nums[last]\n    }\n\n    /* 出力用の配列を返す */\n    fun toArray(): IntArray {\n        // 有効長の範囲内のリスト要素のみを変換\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[index(j)]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
array_deque.rb
### 循環配列で実装した両端キュー ###\nclass ArrayDeque\n  ### 両端キューの長さを取得 ###\n  attr_reader :size\n\n  ### コンストラクタ ###\n  def initialize(capacity)\n    @nums = Array.new(capacity, 0)\n    @front = 0\n    @size = 0\n  end\n\n  ### 両端キューの容量を取得 ###\n  def capacity\n    @nums.length\n  end\n\n  ### 両端キューが空か判定 ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### キュー先頭に追加 ###\n  def push_first(num)\n    if size == capacity\n      puts '両端キューがいっぱいです'\n      return\n    end\n\n    # 先頭ポインタを左に 1 つ移動する\n    # 剰余演算により、front が配列先頭を越えた後に末尾へ戻るようにする\n    @front = index(@front - 1)\n    # num をキュー先頭に追加\n    @nums[@front] = num\n    @size += 1\n  end\n\n  ### キュー末尾に追加 ###\n  def push_last(num)\n    if size == capacity\n      puts '両端キューがいっぱいです'\n      return\n    end\n\n    # キュー末尾ポインタを計算し、末尾インデックス + 1 を指す\n    rear = index(@front + size)\n    # num をキュー末尾に追加\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### キュー先頭から取り出す ###\n  def pop_first\n    num = peek_first\n    # 先頭ポインタを 1 つ後ろへ進める\n    @front = index(@front + 1)\n    @size -= 1\n    num\n  end\n\n  ### キューの末尾から取り出す ###\n  def pop_last\n    num = peek_last\n    @size -= 1\n    num\n  end\n\n  ### 先頭要素にアクセス ###\n  def peek_first\n    raise IndexError, '両端キューは空です' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### キュー末尾要素を参照 ###\n  def peek_last\n    raise IndexError, '両端キューは空です' if is_empty?\n\n    # 末尾要素のインデックスを計算\n    last = index(@front + size - 1)\n    @nums[last]\n  end\n\n  ### 表示用の配列を返す ###\n  def to_array\n    # 有効長の範囲内のリスト要素のみを変換\n    res = []\n    for i in 0...size\n      res << @nums[index(@front + i)]\n    end\n    res\n  end\n\n  private\n\n  ### 循環配列のインデックスを計算 ###\n  def index(i)\n    # 剰余演算により配列の先頭と末尾をつなげる\n    # i が配列の末尾を越えたら先頭に戻る\n    # i が配列の先頭を越えて前に出たら末尾に戻る\n    (i + capacity) % capacity\n  end\nend\n
","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#533","level":2,"title":"5.3.3   両端キューの応用","text":"

両端キューはスタックとキューの両方の論理を備えているため、これら 2 つのすべての応用場面を実現でき、さらに高い自由度を提供します。

私たちが知っているように、ソフトウェアの「元に戻す」機能は通常スタックを使って実装されます。システムは変更操作を毎回スタックに push し、その後 pop によって取り消しを実現します。しかし、システム資源の制約を考慮すると、通常ソフトウェアは取り消し可能な手数を制限します(たとえば \\(50\\) 手まで保存可能)。スタックの長さが \\(50\\) を超えると、ソフトウェアはスタックの底部(先頭)で削除操作を行う必要があります。しかしスタックではこの機能を実現できないため、この場合はスタックの代わりに両端キューを使用する必要があります。なお、「元に戻す」の中核ロジック自体は依然としてスタックの後入れ先出し原則に従っており、両端キューは追加のロジックをより柔軟に実装できるだけです。

","path":["第 5 章   スタックとキュー","5.3   両端キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   キュー","text":"

キュー(queue)は、先入れ先出しの規則に従う線形データ構造です。名前のとおり、キューは順番待ちの現象を模したもので、新しく来た人は絶えずキュー末尾に加わり、キュー先頭にいる人から順に離れていきます。

下図のように、キューの先頭を「キュー先頭」、末尾を「キュー末尾」と呼びます。要素をキュー末尾に加える操作を「エンキュー」、キュー先頭の要素を削除する操作を「デキュー」と呼びます。

図 5-4   キューの先入れ先出し規則

","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521","level":2,"title":"5.2.1   キューの基本操作","text":"

キューの基本操作を以下の表に示します。なお、メソッド名はプログラミング言語によって異なる場合があります。ここではスタックと同じ命名を採用します。

表 5-2   キュー操作の効率

メソッド名 説明 時間計算量 push() 要素をエンキューし、キュー末尾に追加する \\(O(1)\\) pop() キュー先頭の要素をデキューする \\(O(1)\\) peek() キュー先頭の要素にアクセスする \\(O(1)\\)

プログラミング言語に用意された既存のキュークラスをそのまま利用できます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby queue.py
from collections import deque\n\n# キューを初期化\n# Python では、通常は双方向キュークラス deque をキューとして使用する\n# queue.Queue() は純粋なキュークラスだが、やや使いにくいため非推奨\nque: deque[int] = deque()\n\n# 要素をエンキュー\nque.append(1)\nque.append(3)\nque.append(2)\nque.append(5)\nque.append(4)\n\n# キュー先頭の要素にアクセス\nfront: int = que[0]\n\n# 要素をデキュー\npop: int = que.popleft()\n\n# キューの長さを取得\nsize: int = len(que)\n\n# キューが空かどうかを判定\nis_empty: bool = len(que) == 0\n
queue.cpp
/* キューを初期化 */\nqueue<int> queue;\n\n/* 要素をエンキュー */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* キュー先頭の要素にアクセス */\nint front = queue.front();\n\n/* 要素をデキュー */\nqueue.pop();\n\n/* キューの長さを取得 */\nint size = queue.size();\n\n/* キューが空かどうかを判定 */\nbool empty = queue.empty();\n
queue.java
/* キューを初期化 */\nQueue<Integer> queue = new LinkedList<>();\n\n/* 要素をエンキュー */\nqueue.offer(1);\nqueue.offer(3);\nqueue.offer(2);\nqueue.offer(5);\nqueue.offer(4);\n\n/* キュー先頭の要素にアクセス */\nint peek = queue.peek();\n\n/* 要素をデキュー */\nint pop = queue.poll();\n\n/* キューの長さを取得 */\nint size = queue.size();\n\n/* キューが空かどうかを判定 */\nboolean isEmpty = queue.isEmpty();\n
queue.cs
/* キューを初期化 */\nQueue<int> queue = new();\n\n/* 要素をエンキュー */\nqueue.Enqueue(1);\nqueue.Enqueue(3);\nqueue.Enqueue(2);\nqueue.Enqueue(5);\nqueue.Enqueue(4);\n\n/* キュー先頭の要素にアクセス */\nint peek = queue.Peek();\n\n/* 要素をデキュー */\nint pop = queue.Dequeue();\n\n/* キューの長さを取得 */\nint size = queue.Count;\n\n/* キューが空かどうかを判定 */\nbool isEmpty = queue.Count == 0;\n
queue_test.go
/* キューを初期化 */\n// Go では、list をキューとして使用する\nqueue := list.New()\n\n/* 要素をエンキュー */\nqueue.PushBack(1)\nqueue.PushBack(3)\nqueue.PushBack(2)\nqueue.PushBack(5)\nqueue.PushBack(4)\n\n/* キュー先頭の要素にアクセス */\npeek := queue.Front()\n\n/* 要素をデキュー */\npop := queue.Front()\nqueue.Remove(pop)\n\n/* キューの長さを取得 */\nsize := queue.Len()\n\n/* キューが空かどうかを判定 */\nisEmpty := queue.Len() == 0\n
queue.swift
/* キューを初期化 */\n// Swift には組み込みのキュークラスがないため、Array をキューとして使える\nvar queue: [Int] = []\n\n/* 要素をエンキュー */\nqueue.append(1)\nqueue.append(3)\nqueue.append(2)\nqueue.append(5)\nqueue.append(4)\n\n/* キュー先頭の要素にアクセス */\nlet peek = queue.first!\n\n/* 要素をデキュー */\n// 配列であるため、removeFirst の計算量は O(n)\nlet pool = queue.removeFirst()\n\n/* キューの長さを取得 */\nlet size = queue.count\n\n/* キューが空かどうかを判定 */\nlet isEmpty = queue.isEmpty\n
queue.js
/* キューを初期化 */\n// JavaScript には組み込みのキューがないため、Array をキューとして使える\nconst queue = [];\n\n/* 要素をエンキュー */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* キュー先頭の要素にアクセス */\nconst peek = queue[0];\n\n/* 要素をデキュー */\n// 基盤は配列であるため、shift() メソッドの時間計算量は O(n)\nconst pop = queue.shift();\n\n/* キューの長さを取得 */\nconst size = queue.length;\n\n/* キューが空かどうかを判定 */\nconst empty = queue.length === 0;\n
queue.ts
/* キューを初期化 */\n// TypeScript には組み込みのキューがないため、Array をキューとして使える\nconst queue: number[] = [];\n\n/* 要素をエンキュー */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* キュー先頭の要素にアクセス */\nconst peek = queue[0];\n\n/* 要素をデキュー */\n// 基盤は配列であるため、shift() メソッドの時間計算量は O(n)\nconst pop = queue.shift();\n\n/* キューの長さを取得 */\nconst size = queue.length;\n\n/* キューが空かどうかを判定 */\nconst empty = queue.length === 0;\n
queue.dart
/* キューを初期化 */\n// Dart では、キュークラス Qeque は双方向キューであり、キューとしても使用できる\nQueue<int> queue = Queue();\n\n/* 要素をエンキュー */\nqueue.add(1);\nqueue.add(3);\nqueue.add(2);\nqueue.add(5);\nqueue.add(4);\n\n/* キュー先頭の要素にアクセス */\nint peek = queue.first;\n\n/* 要素をデキュー */\nint pop = queue.removeFirst();\n\n/* キューの長さを取得 */\nint size = queue.length;\n\n/* キューが空かどうかを判定 */\nbool isEmpty = queue.isEmpty;\n
queue.rs
/* 双方向キューを初期化 */\n// Rust では双方向キューを通常のキューとして使う\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* 要素をエンキュー */\ndeque.push_back(1);\ndeque.push_back(3);\ndeque.push_back(2);\ndeque.push_back(5);\ndeque.push_back(4);\n\n/* キュー先頭の要素にアクセス */\nif let Some(front) = deque.front() {\n}\n\n/* 要素をデキュー */\nif let Some(pop) = deque.pop_front() {\n}\n\n/* キューの長さを取得 */\nlet size = deque.len();\n\n/* キューが空かどうかを判定 */\nlet is_empty = deque.is_empty();\n
queue.c
// C には組み込みのキューがない\n
queue.kt
/* キューを初期化 */\nval queue = LinkedList<Int>()\n\n/* 要素をエンキュー */\nqueue.offer(1)\nqueue.offer(3)\nqueue.offer(2)\nqueue.offer(5)\nqueue.offer(4)\n\n/* キュー先頭の要素にアクセス */\nval peek = queue.peek()\n\n/* 要素をデキュー */\nval pop = queue.poll()\n\n/* キューの長さを取得 */\nval size = queue.size\n\n/* キューが空かどうかを判定 */\nval isEmpty = queue.isEmpty()\n
queue.rb
# キューを初期化\n# Ruby 組み込みのキュー(Thread::Queue) には peek と走査メソッドがないため、Array をキューとして使える\nqueue = []\n\n# 要素をエンキュー\nqueue.push(1)\nqueue.push(3)\nqueue.push(2)\nqueue.push(5)\nqueue.push(4)\n\n# キュー先頭の要素にアクセス\npeek = queue.first\n\n# 要素をデキュー\n# 注意:配列であるため、Array#shift メソッドの時間計算量は O(n)\npop = queue.shift\n\n# キューの長さを取得\nsize = queue.length\n\n# キューが空かどうかを判定\nis_empty = queue.empty?\n
可視化実行

https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%98%9F%E5%88%97%0A%20%20%20%20%23%20%E5%9C%A8%20Python%20%E4%B8%AD%EF%BC%8C%E6%88%91%E4%BB%AC%E4%B8%80%E8%88%AC%E5%B0%86%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%B1%BB%20deque%20%E7%9C%8B%E4%BD%9C%E9%98%9F%E5%88%97%E4%BD%BF%E7%94%A8%0A%20%20%20%20%23%20%E8%99%BD%E7%84%B6%20queue.Queue%28%29%20%E6%98%AF%E7%BA%AF%E6%AD%A3%E7%9A%84%E9%98%9F%E5%88%97%E7%B1%BB%EF%BC%8C%E4%BD%86%E4%B8%8D%E5%A4%AA%E5%A5%BD%E7%94%A8%0A%20%20%20%20que%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20que.append%281%29%0A%20%20%20%20que.append%283%29%0A%20%20%20%20que.append%282%29%0A%20%20%20%20que.append%285%29%0A%20%20%20%20que.append%284%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20que%5B0%5D%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop%20%3D%20que.popleft%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%90%8E%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28que%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28que%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#522","level":2,"title":"5.2.2   キューの実装","text":"

キューを実装するには、一方の端で要素を追加し、もう一方の端で要素を削除できるデータ構造が必要です。連結リストと配列はいずれもこの条件を満たします。

","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#1","level":3,"title":"1.   連結リストに基づく実装","text":"

下図のように、連結リストの「先頭ノード」と「末尾ノード」をそれぞれ「キュー先頭」と「キュー末尾」とみなし、キュー末尾ではノードの追加のみ、キュー先頭ではノードの削除のみを行うようにします。

LinkedListQueuepush()pop()

図 5-5   連結リストでキューを実装したエンキューとデキュー操作

以下は連結リストでキューを実装するコードです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_queue.py
class LinkedListQueue:\n    \"\"\"連結リストベースのキュー\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self._front: ListNode | None = None  # 先頭ノード front\n        self._rear: ListNode | None = None  # 末尾ノード rear\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"キューの長さを取得\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"キューが空かどうかを判定\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"エンキュー\"\"\"\n        # 末尾ノードの後ろに num を追加\n        node = ListNode(num)\n        # キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if self._front is None:\n            self._front = node\n            self._rear = node\n        # キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        else:\n            self._rear.next = node\n            self._rear = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"デキュー\"\"\"\n        num = self.peek()\n        # 先頭ノードを削除\n        self._front = self._front.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"キュー先頭の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"キューが空です\")\n        return self._front.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"表示用にリストへ変換\"\"\"\n        queue = []\n        temp = self._front\n        while temp:\n            queue.append(temp.val)\n            temp = temp.next\n        return queue\n
linkedlist_queue.cpp
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n  private:\n    ListNode *front, *rear; // 先頭ノード front、末尾ノード rear\n    int queSize;\n\n  public:\n    LinkedListQueue() {\n        front = nullptr;\n        rear = nullptr;\n        queSize = 0;\n    }\n\n    ~LinkedListQueue() {\n        // 連結リストを走査してノードを削除し、メモリを解放する\n        freeMemoryLinkedList(front);\n    }\n\n    /* キューの長さを取得 */\n    int size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* エンキュー */\n    void push(int num) {\n        // 末尾ノードの後ろに num を追加\n        ListNode *node = new ListNode(num);\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (front == nullptr) {\n            front = node;\n            rear = node;\n        }\n        // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        else {\n            rear->next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* デキュー */\n    int pop() {\n        int num = peek();\n        // 先頭ノードを削除\n        ListNode *tmp = front;\n        front = front->next;\n        // メモリを解放する\n        delete tmp;\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    int peek() {\n        if (size() == 0)\n            throw out_of_range(\"キューが空です\");\n        return front->val;\n    }\n\n    /* 連結リストを Vector に変換して返す */\n    vector<int> toVector() {\n        ListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_queue.java
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n    private ListNode front, rear; // 先頭ノード front、末尾ノード rear\n    private int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* キューの長さを取得 */\n    public int size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* エンキュー */\n    public void push(int num) {\n        // 末尾ノードの後ろに num を追加\n        ListNode node = new ListNode(num);\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (front == null) {\n            front = node;\n            rear = node;\n        // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        } else {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* デキュー */\n    public int pop() {\n        int num = peek();\n        // 先頭ノードを削除\n        front = front.next;\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.cs
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n    ListNode? front, rear;  // 先頭ノード front、末尾ノード rear\n    int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* キューの長さを取得 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* エンキュー */\n    public void Push(int num) {\n        // 末尾ノードの後ろに num を追加\n        ListNode node = new(num);\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (front == null) {\n            front = node;\n            rear = node;\n            // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        } else if (rear != null) {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* デキュー */\n    public int Pop() {\n        int num = Peek();\n        // 先頭ノードを削除\n        front = front?.next;\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return front!.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    public int[] ToArray() {\n        if (front == null)\n            return [];\n\n        ListNode? node = front;\n        int[] res = new int[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.go
/* 連結リストベースのキュー */\ntype linkedListQueue struct {\n    // 組み込みパッケージ list でキューを実装する\n    data *list.List\n}\n\n/* キューを初期化 */\nfunc newLinkedListQueue() *linkedListQueue {\n    return &linkedListQueue{\n        data: list.New(),\n    }\n}\n\n/* エンキュー */\nfunc (s *linkedListQueue) push(value any) {\n    s.data.PushBack(value)\n}\n\n/* デキュー */\nfunc (s *linkedListQueue) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* キュー先頭の要素にアクセス */\nfunc (s *linkedListQueue) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* キューの長さを取得 */\nfunc (s *linkedListQueue) size() int {\n    return s.data.Len()\n}\n\n/* キューが空かどうかを判定 */\nfunc (s *linkedListQueue) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* 表示用に List を取得 */\nfunc (s *linkedListQueue) toList() *list.List {\n    return s.data\n}\n
linkedlist_queue.swift
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n    private var front: ListNode? // 先頭ノード\n    private var rear: ListNode? // 末尾ノード\n    private var _size: Int\n\n    init() {\n        _size = 0\n    }\n\n    /* キューの長さを取得 */\n    func size() -> Int {\n        _size\n    }\n\n    /* キューが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* エンキュー */\n    func push(num: Int) {\n        // 末尾ノードの後ろに num を追加\n        let node = ListNode(x: num)\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if front == nil {\n            front = node\n            rear = node\n        }\n        // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        else {\n            rear?.next = node\n            rear = node\n        }\n        _size += 1\n    }\n\n    /* デキュー */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // 先頭ノードを削除\n        front = front?.next\n        _size -= 1\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"キューが空です\")\n        }\n        return front!.val\n    }\n\n    /* 連結リストを Array に変換して返す */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_queue.js
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n    #front; // 先頭ノード #front\n    #rear; // 末尾ノード #rear\n    #queSize = 0;\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n    }\n\n    /* キューの長さを取得 */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* エンキュー */\n    push(num) {\n        // 末尾ノードの後ろに num を追加\n        const node = new ListNode(num);\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (!this.#front) {\n            this.#front = node;\n            this.#rear = node;\n            // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        } else {\n            this.#rear.next = node;\n            this.#rear = node;\n        }\n        this.#queSize++;\n    }\n\n    /* デキュー */\n    pop() {\n        const num = this.peek();\n        // 先頭ノードを削除\n        this.#front = this.#front.next;\n        this.#queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peek() {\n        if (this.size === 0) throw new Error('キューが空');\n        return this.#front.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    toArray() {\n        let node = this.#front;\n        const res = new Array(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.ts
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n    private front: ListNode | null; // 先頭ノード front\n    private rear: ListNode | null; // 末尾ノード rear\n    private queSize: number = 0;\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n    }\n\n    /* キューの長さを取得 */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* エンキュー */\n    push(num: number): void {\n        // 末尾ノードの後ろに num を追加\n        const node = new ListNode(num);\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (!this.front) {\n            this.front = node;\n            this.rear = node;\n            // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        } else {\n            this.rear!.next = node;\n            this.rear = node;\n        }\n        this.queSize++;\n    }\n\n    /* デキュー */\n    pop(): number {\n        const num = this.peek();\n        if (!this.front) throw new Error('キューが空です');\n        // 先頭ノードを削除\n        this.front = this.front.next;\n        this.queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peek(): number {\n        if (this.size === 0) throw new Error('キューが空です');\n        return this.front!.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    toArray(): number[] {\n        let node = this.front;\n        const res = new Array<number>(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.dart
/* 連結リストベースのキュー */\nclass LinkedListQueue {\n  ListNode? _front; // 先頭ノード _front\n  ListNode? _rear; // 末尾ノード _rear\n  int _queSize = 0; // キューの長さ\n\n  LinkedListQueue() {\n    _front = null;\n    _rear = null;\n  }\n\n  /* キューの長さを取得 */\n  int size() {\n    return _queSize;\n  }\n\n  /* キューが空かどうかを判定 */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* エンキュー */\n  void push(int _num) {\n    // 末尾ノードの後ろに _num を追加\n    final node = ListNode(_num);\n    // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n    if (_front == null) {\n      _front = node;\n      _rear = node;\n    } else {\n      // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n      _rear!.next = node;\n      _rear = node;\n    }\n    _queSize++;\n  }\n\n  /* デキュー */\n  int pop() {\n    final int _num = peek();\n    // 先頭ノードを削除\n    _front = _front!.next;\n    _queSize--;\n    return _num;\n  }\n\n  /* キュー先頭の要素にアクセス */\n  int peek() {\n    if (_queSize == 0) {\n      throw Exception('キューが空です');\n    }\n    return _front!.val;\n  }\n\n  /* 連結リストを Array に変換して返す */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> queue = [];\n    while (node != null) {\n      queue.add(node.val);\n      node = node.next;\n    }\n    return queue;\n  }\n}\n
linkedlist_queue.rs
/* 連結リストベースのキュー */\n#[allow(dead_code)]\npub struct LinkedListQueue<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // 先頭ノード front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // 末尾ノード rear\n    que_size: usize,                         // キューの長さ\n}\n\nimpl<T: Copy> LinkedListQueue<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* キューの長さを取得 */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* キューが空かどうかを判定 */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* エンキュー */\n    pub fn push(&mut self, num: T) {\n        // 末尾ノードの後ろに num を追加\n        let new_rear = ListNode::new(num);\n        match self.rear.take() {\n            // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n            Some(old_rear) => {\n                old_rear.borrow_mut().next = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n            // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n            None => {\n                self.front = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n        }\n        self.que_size += 1;\n    }\n\n    /* デキュー */\n    pub fn pop(&mut self) -> Option<T> {\n        self.front.take().map(|old_front| {\n            match old_front.borrow_mut().next.take() {\n                Some(new_front) => {\n                    self.front = Some(new_front);\n                }\n                None => {\n                    self.rear.take();\n                }\n            }\n            self.que_size -= 1;\n            old_front.borrow().val\n        })\n    }\n\n    /* キュー先頭の要素にアクセス */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* 連結リストを Array に変換して返す */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n\n        res\n    }\n}\n
linkedlist_queue.c
/* 連結リストベースのキュー */\ntypedef struct {\n    ListNode *front, *rear;\n    int queSize;\n} LinkedListQueue;\n\n/* コンストラクタ */\nLinkedListQueue *newLinkedListQueue() {\n    LinkedListQueue *queue = (LinkedListQueue *)malloc(sizeof(LinkedListQueue));\n    queue->front = NULL;\n    queue->rear = NULL;\n    queue->queSize = 0;\n    return queue;\n}\n\n/* デストラクタ */\nvoid delLinkedListQueue(LinkedListQueue *queue) {\n    // すべてのノードを解放\n    while (queue->front != NULL) {\n        ListNode *tmp = queue->front;\n        queue->front = queue->front->next;\n        free(tmp);\n    }\n    // queue 構造体を解放する\n    free(queue);\n}\n\n/* キューの長さを取得 */\nint size(LinkedListQueue *queue) {\n    return queue->queSize;\n}\n\n/* キューが空かどうかを判定 */\nbool empty(LinkedListQueue *queue) {\n    return (size(queue) == 0);\n}\n\n/* エンキュー */\nvoid push(LinkedListQueue *queue, int num) {\n    // 末尾ノードに node を追加\n    ListNode *node = newListNode(num);\n    // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n    if (queue->front == NULL) {\n        queue->front = node;\n        queue->rear = node;\n    }\n    // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n    else {\n        queue->rear->next = node;\n        queue->rear = node;\n    }\n    queue->queSize++;\n}\n\n/* キュー先頭の要素にアクセス */\nint peek(LinkedListQueue *queue) {\n    assert(size(queue) && queue->front);\n    return queue->front->val;\n}\n\n/* デキュー */\nint pop(LinkedListQueue *queue) {\n    int num = peek(queue);\n    ListNode *tmp = queue->front;\n    queue->front = queue->front->next;\n    free(tmp);\n    queue->queSize--;\n    return num;\n}\n\n/* キューを出力する */\nvoid printLinkedListQueue(LinkedListQueue *queue) {\n    int *arr = malloc(sizeof(int) * queue->queSize);\n    // 連結リスト内のデータを配列にコピー\n    int i;\n    ListNode *node;\n    for (i = 0, node = queue->front; i < queue->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, queue->queSize);\n    free(arr);\n}\n
linkedlist_queue.kt
/* 連結リストベースのキュー */\nclass LinkedListQueue(\n    // 先頭ノード front、末尾ノード rear\n    private var front: ListNode? = null,\n    private var rear: ListNode? = null,\n    private var queSize: Int = 0\n) {\n\n    /* キューの長さを取得 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* キューが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* エンキュー */\n    fun push(num: Int) {\n        // 末尾ノードの後ろに num を追加\n        val node = ListNode(num)\n        // キューが空なら、先頭・末尾ノードをともにそのノードに設定\n        if (front == null) {\n            front = node\n            rear = node\n            // キューが空でなければ、そのノードを末尾ノードの後ろに追加\n        } else {\n            rear?.next = node\n            rear = node\n        }\n        queSize++\n    }\n\n    /* デキュー */\n    fun pop(): Int {\n        val num = peek()\n        // 先頭ノードを削除\n        front = front?.next\n        queSize--\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* 連結リストを Array に変換して返す */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_queue.rb
### 連結リストで実装したキュー ###\nclass LinkedListQueue\n  ### キューの長さを取得 ###\n  attr_reader :size\n\n  ### コンストラクタ ###\n  def initialize\n    @front = nil  # 先頭ノード front\n    @rear = nil   # 末尾ノード rear\n    @size = 0\n  end\n\n  ### キューが空か判定 ###\n  def is_empty?\n    @front.nil?\n  end\n\n  ### エンキュー ###\n  def push(num)\n    # 末尾ノードの後ろに num を追加\n    node = ListNode.new(num)\n\n    # キューが空なら、先頭ノードと末尾ノードの両方をそのノードに向ける\n    if @front.nil?\n      @front = node\n      @rear = node\n    # キューが空でなければ、そのノードを末尾ノードの後ろに追加する\n    else\n      @rear.next = node\n      @rear = node\n    end\n\n    @size += 1\n  end\n\n  ### デキュー ###\n  def pop\n    num = peek\n    # 先頭ノードを削除\n    @front = @front.next\n    @size -= 1\n    num\n  end\n\n  ### 先頭要素にアクセス ###\n  def peek\n    raise IndexError, 'キューは空です' if is_empty?\n\n    @front.val\n  end\n\n  ### 連結リストを Array に変換して返す ###\n  def to_array\n    queue = []\n    temp = @front\n    while temp\n      queue << temp.val\n      temp = temp.next\n    end\n    queue\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#2","level":3,"title":"2.   配列に基づく実装","text":"

配列で先頭要素を削除する時間計算量は \\(O(n)\\) であり、そのままではデキュー操作の効率が低くなります。しかし、次の巧妙な方法によってこの問題を回避できます。

変数 front を用いてキュー先頭要素のインデックスを指し、さらに変数 size でキューの長さを記録できます。rear = front + size と定義すると、この式で得られる rear はキュー末尾要素の次の位置を指します。

この設計に基づくと、配列内で要素を含む有効区間は [front, rear - 1] となります。各種操作の実装方法を下図に示します。

  • エンキュー操作:入力要素を rear の位置に代入し、size を 1 増やします。
  • デキュー操作:front を 1 増やし、size を 1 減らすだけです。

このように、エンキューとデキューはいずれも 1 回の操作だけで済み、時間計算量はともに \\(O(1)\\) です。

ArrayQueuepush()pop()

図 5-6   配列でキューを実装したエンキューとデキュー操作

ここで 1 つ問題があります。エンキューとデキューを繰り返すと、frontrear はどちらも右へ移動し続け、配列の末尾に達するとそれ以上進めなくなります。この問題を解決するために、配列を先頭と末尾がつながった「環状配列」とみなします。

環状配列では、front または rear が配列末尾を越えたときに、直ちに配列先頭へ戻って走査を続けられるようにする必要があります。この周期的な規則は「剰余演算」によって実現できます。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_queue.py
class ArrayQueue:\n    \"\"\"循環配列ベースのキュー\"\"\"\n\n    def __init__(self, size: int):\n        \"\"\"コンストラクタ\"\"\"\n        self._nums: list[int] = [0] * size  # キュー要素を格納する配列\n        self._front: int = 0  # 先頭ポインタ。先頭要素を指す\n        self._size: int = 0  # キューの長さ\n\n    def capacity(self) -> int:\n        \"\"\"キューの容量を取得\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"キューの長さを取得\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"キューが空かどうかを判定\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"エンキュー\"\"\"\n        if self._size == self.capacity():\n            raise IndexError(\"キューがいっぱいです\")\n        # 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        # 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        rear: int = (self._front + self._size) % self.capacity()\n        # num をキュー末尾に追加\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"デキュー\"\"\"\n        num: int = self.peek()\n        # 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        self._front = (self._front + 1) % self.capacity()\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"キュー先頭の要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"キューが空です\")\n        return self._nums[self._front]\n\n    def to_list(self) -> list[int]:\n        \"\"\"表示用のリストを返す\"\"\"\n        res = [0] * self.size()\n        j: int = self._front\n        for i in range(self.size()):\n            res[i] = self._nums[(j % self.capacity())]\n            j += 1\n        return res\n
array_queue.cpp
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n  private:\n    int *nums;       // キュー要素を格納する配列\n    int front;       // 先頭ポインタ。先頭要素を指す\n    int queSize;     // キューの長さ\n    int queCapacity; // キューの容量\n\n  public:\n    ArrayQueue(int capacity) {\n        // 配列を初期化\n        nums = new int[capacity];\n        queCapacity = capacity;\n        front = queSize = 0;\n    }\n\n    ~ArrayQueue() {\n        delete[] nums;\n    }\n\n    /* キューの容量を取得 */\n    int capacity() {\n        return queCapacity;\n    }\n\n    /* キューの長さを取得 */\n    int size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* エンキュー */\n    void push(int num) {\n        if (queSize == queCapacity) {\n            cout << \"キューがいっぱいです\" << endl;\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        int rear = (front + queSize) % queCapacity;\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* デキュー */\n    int pop() {\n        int num = peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        front = (front + 1) % queCapacity;\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    int peek() {\n        if (isEmpty())\n            throw out_of_range(\"キューが空です\");\n        return nums[front];\n    }\n\n    /* 配列を Vector に変換して返す */\n    vector<int> toVector() {\n        // 有効長の範囲内のリスト要素のみを変換\n        vector<int> arr(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            arr[i] = nums[j % queCapacity];\n        }\n        return arr;\n    }\n};\n
array_queue.java
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n    private int[] nums; // キュー要素を格納する配列\n    private int front; // 先頭ポインタ。先頭要素を指す\n    private int queSize; // キューの長さ\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* キューの容量を取得 */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* キューの長さを取得 */\n    public int size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* エンキュー */\n    public void push(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"キューは満杯です\");\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        int rear = (front + queSize) % capacity();\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* デキュー */\n    public int pop() {\n        int num = peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        front = (front + 1) % capacity();\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* 配列を返す */\n    public int[] toArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % capacity()];\n        }\n        return res;\n    }\n}\n
array_queue.cs
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n    int[] nums;  // キュー要素を格納する配列\n    int front;   // 先頭ポインタ。先頭要素を指す\n    int queSize; // キューの長さ\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* キューの容量を取得 */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* キューの長さを取得 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* エンキュー */\n    public void Push(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"キューは満杯です\");\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        int rear = (front + queSize) % Capacity();\n        // num をキュー末尾に追加\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* デキュー */\n    public int Pop() {\n        int num = Peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        front = (front + 1) % Capacity();\n        queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return nums[front];\n    }\n\n    /* 配列を返す */\n    public int[] ToArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % this.Capacity()];\n        }\n        return res;\n    }\n}\n
array_queue.go
/* 循環配列ベースのキュー */\ntype arrayQueue struct {\n    nums        []int // キュー要素を格納する配列\n    front       int   // 先頭ポインタ。先頭要素を指す\n    queSize     int   // キューの長さ\n    queCapacity int   // キュー容量(格納できる要素数の上限)\n}\n\n/* キューを初期化 */\nfunc newArrayQueue(queCapacity int) *arrayQueue {\n    return &arrayQueue{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* キューの長さを取得 */\nfunc (q *arrayQueue) size() int {\n    return q.queSize\n}\n\n/* キューが空かどうかを判定 */\nfunc (q *arrayQueue) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* エンキュー */\nfunc (q *arrayQueue) push(num int) {\n    // rear == queCapacity のときキューは満杯\n    if q.queSize == q.queCapacity {\n        return\n    }\n    // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n    // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n    rear := (q.front + q.queSize) % q.queCapacity\n    // num をキュー末尾に追加\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* デキュー */\nfunc (q *arrayQueue) pop() any {\n    num := q.peek()\n    if num == nil {\n        return nil\n    }\n\n    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n    q.front = (q.front + 1) % q.queCapacity\n    q.queSize--\n    return num\n}\n\n/* キュー先頭の要素にアクセス */\nfunc (q *arrayQueue) peek() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* 表示用に Slice を取得 */\nfunc (q *arrayQueue) toSlice() []int {\n    rear := (q.front + q.queSize)\n    if rear >= q.queCapacity {\n        rear %= q.queCapacity\n        return append(q.nums[q.front:], q.nums[:rear]...)\n    }\n    return q.nums[q.front:rear]\n}\n
array_queue.swift
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n    private var nums: [Int] // キュー要素を格納する配列\n    private var front: Int // 先頭ポインタ。先頭要素を指す\n    private var _size: Int // キューの長さ\n\n    init(capacity: Int) {\n        // 配列を初期化\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* キューの容量を取得 */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* キューの長さを取得 */\n    func size() -> Int {\n        _size\n    }\n\n    /* キューが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* エンキュー */\n    func push(num: Int) {\n        if size() == capacity() {\n            print(\"キューがいっぱいです\")\n            return\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        let rear = (front + size()) % capacity()\n        // num をキュー末尾に追加\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* デキュー */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        front = (front + 1) % capacity()\n        _size -= 1\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"キューが空です\")\n        }\n        return nums[front]\n    }\n\n    /* 配列を返す */\n    func toArray() -> [Int] {\n        // 有効長の範囲内のリスト要素のみを変換\n        (front ..< front + size()).map { nums[$0 % capacity()] }\n    }\n}\n
array_queue.js
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n    #nums; // キュー要素を格納する配列\n    #front = 0; // 先頭ポインタ。先頭要素を指す\n    #queSize = 0; // キューの長さ\n\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n    }\n\n    /* キューの容量を取得 */\n    get capacity() {\n        return this.#nums.length;\n    }\n\n    /* キューの長さを取得 */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* エンキュー */\n    push(num) {\n        if (this.size === this.capacity) {\n            console.log('キューがいっぱいです');\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        const rear = (this.#front + this.size) % this.capacity;\n        // num をキュー末尾に追加\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* デキュー */\n    pop() {\n        const num = this.peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        this.#front = (this.#front + 1) % this.capacity;\n        this.#queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peek() {\n        if (this.isEmpty()) throw new Error('キューが空です');\n        return this.#nums[this.#front];\n    }\n\n    /* Array を返す */\n    toArray() {\n        // 有効長の範囲内のリスト要素のみを変換\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.#front; i < this.size; i++, j++) {\n            arr[i] = this.#nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
array_queue.ts
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n    private nums: number[]; // キュー要素を格納する配列\n    private front: number; // 先頭ポインタ。先頭要素を指す\n    private queSize: number; // キューの長さ\n\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = this.queSize = 0;\n    }\n\n    /* キューの容量を取得 */\n    get capacity(): number {\n        return this.nums.length;\n    }\n\n    /* キューの長さを取得 */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* キューが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* エンキュー */\n    push(num: number): void {\n        if (this.size === this.capacity) {\n            console.log('キューは満杯です');\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        const rear = (this.front + this.queSize) % this.capacity;\n        // num をキュー末尾に追加\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* デキュー */\n    pop(): number {\n        const num = this.peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        this.front = (this.front + 1) % this.capacity;\n        this.queSize--;\n        return num;\n    }\n\n    /* キュー先頭の要素にアクセス */\n    peek(): number {\n        if (this.isEmpty()) throw new Error('キューが空です');\n        return this.nums[this.front];\n    }\n\n    /* Array を返す */\n    toArray(): number[] {\n        // 有効長の範囲内のリスト要素のみを変換\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.front; i < this.size; i++, j++) {\n            arr[i] = this.nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
array_queue.dart
/* 循環配列ベースのキュー */\nclass ArrayQueue {\n  late List<int> _nums; // キュー要素を格納する配列\n  late int _front; // 先頭ポインタ。先頭要素を指す\n  late int _queSize; // キューの長さ\n\n  ArrayQueue(int capacity) {\n    _nums = List.filled(capacity, 0);\n    _front = _queSize = 0;\n  }\n\n  /* キューの容量を取得 */\n  int capaCity() {\n    return _nums.length;\n  }\n\n  /* キューの長さを取得 */\n  int size() {\n    return _queSize;\n  }\n\n  /* キューが空かどうかを判定 */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* エンキュー */\n  void push(int _num) {\n    if (_queSize == capaCity()) {\n      throw Exception(\"キューは満杯です\");\n    }\n    // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n    // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n    int rear = (_front + _queSize) % capaCity();\n    // _num をキュー末尾に追加\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* デキュー */\n  int pop() {\n    int _num = peek();\n    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n    _front = (_front + 1) % capaCity();\n    _queSize--;\n    return _num;\n  }\n\n  /* キュー先頭の要素にアクセス */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"キューが空です\");\n    }\n    return _nums[_front];\n  }\n\n  /* Array を返す */\n  List<int> toArray() {\n    // 有効長の範囲内のリスト要素のみを変換\n    final List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[j % capaCity()];\n    }\n    return res;\n  }\n}\n
array_queue.rs
/* 循環配列ベースのキュー */\nstruct ArrayQueue<T> {\n    nums: Vec<T>,      // キュー要素を格納する配列\n    front: i32,        // 先頭ポインタ。先頭要素を指す\n    que_size: i32,     // キューの長さ\n    que_capacity: i32, // キューの容量\n}\n\nimpl<T: Copy + Default> ArrayQueue<T> {\n    /* コンストラクタ */\n    fn new(capacity: i32) -> ArrayQueue<T> {\n        ArrayQueue {\n            nums: vec![T::default(); capacity as usize],\n            front: 0,\n            que_size: 0,\n            que_capacity: capacity,\n        }\n    }\n\n    /* キューの容量を取得 */\n    fn capacity(&self) -> i32 {\n        self.que_capacity\n    }\n\n    /* キューの長さを取得 */\n    fn size(&self) -> i32 {\n        self.que_size\n    }\n\n    /* キューが空かどうかを判定 */\n    fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* エンキュー */\n    fn push(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"キューがいっぱいです\");\n            return;\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        let rear = (self.front + self.que_size) % self.que_capacity;\n        // num をキュー末尾に追加\n        self.nums[rear as usize] = num;\n        self.que_size += 1;\n    }\n\n    /* デキュー */\n    fn pop(&mut self) -> T {\n        let num = self.peek();\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        self.front = (self.front + 1) % self.que_capacity;\n        self.que_size -= 1;\n        num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fn peek(&self) -> T {\n        if self.is_empty() {\n            panic!(\"index out of bounds\");\n        }\n        self.nums[self.front as usize]\n    }\n\n    /* 配列を返す */\n    fn to_vector(&self) -> Vec<T> {\n        let cap = self.que_capacity;\n        let mut j = self.front;\n        let mut arr = vec![T::default(); cap as usize];\n        for i in 0..self.que_size {\n            arr[i as usize] = self.nums[(j % cap) as usize];\n            j += 1;\n        }\n        arr\n    }\n}\n
array_queue.c
/* 循環配列ベースのキュー */\ntypedef struct {\n    int *nums;       // キュー要素を格納する配列\n    int front;       // 先頭ポインタ。先頭要素を指す\n    int queSize;     // 現在のキュー内の要素数\n    int queCapacity; // キューの容量\n} ArrayQueue;\n\n/* コンストラクタ */\nArrayQueue *newArrayQueue(int capacity) {\n    ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue));\n    // 配列を初期化\n    queue->queCapacity = capacity;\n    queue->nums = (int *)malloc(sizeof(int) * queue->queCapacity);\n    queue->front = queue->queSize = 0;\n    return queue;\n}\n\n/* デストラクタ */\nvoid delArrayQueue(ArrayQueue *queue) {\n    free(queue->nums);\n    free(queue);\n}\n\n/* キューの容量を取得 */\nint capacity(ArrayQueue *queue) {\n    return queue->queCapacity;\n}\n\n/* キューの長さを取得 */\nint size(ArrayQueue *queue) {\n    return queue->queSize;\n}\n\n/* キューが空かどうかを判定 */\nbool empty(ArrayQueue *queue) {\n    return queue->queSize == 0;\n}\n\n/* キュー先頭の要素にアクセス */\nint peek(ArrayQueue *queue) {\n    assert(size(queue) != 0);\n    return queue->nums[queue->front];\n}\n\n/* エンキュー */\nvoid push(ArrayQueue *queue, int num) {\n    if (size(queue) == capacity(queue)) {\n        printf(\"キューは満杯です\\r\\n\");\n        return;\n    }\n    // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n    // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n    int rear = (queue->front + queue->queSize) % queue->queCapacity;\n    // num をキュー末尾に追加\n    queue->nums[rear] = num;\n    queue->queSize++;\n}\n\n/* デキュー */\nint pop(ArrayQueue *queue) {\n    int num = peek(queue);\n    // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n    queue->front = (queue->front + 1) % queue->queCapacity;\n    queue->queSize--;\n    return num;\n}\n\n/* 出力用の配列を返す */\nint *toArray(ArrayQueue *queue, int *queSize) {\n    *queSize = queue->queSize;\n    int *res = (int *)calloc(queue->queSize, sizeof(int));\n    int j = queue->front;\n    for (int i = 0; i < queue->queSize; i++) {\n        res[i] = queue->nums[j % queue->queCapacity];\n        j++;\n    }\n    return res;\n}\n
array_queue.kt
/* 循環配列ベースのキュー */\nclass ArrayQueue(capacity: Int) {\n    private val nums: IntArray = IntArray(capacity) // キュー要素を格納する配列\n    private var front: Int = 0 // 先頭ポインタ。先頭要素を指す\n    private var queSize: Int = 0 // キューの長さ\n\n    /* キューの容量を取得 */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* キューの長さを取得 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* キューが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* エンキュー */\n    fun push(num: Int) {\n        if (queSize == capacity()) {\n            println(\"キューは満杯です\")\n            return\n        }\n        // 末尾ポインタを計算し、末尾インデックス + 1 を指す\n        // 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n        val rear = (front + queSize) % capacity()\n        // num をキュー末尾に追加\n        nums[rear] = num\n        queSize++\n    }\n\n    /* デキュー */\n    fun pop(): Int {\n        val num = peek()\n        // 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n        front = (front + 1) % capacity()\n        queSize--\n        return num\n    }\n\n    /* キュー先頭の要素にアクセス */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* 配列を返す */\n    fun toArray(): IntArray {\n        // 有効長の範囲内のリスト要素のみを変換\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[j % capacity()]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
array_queue.rb
### 循環配列で実装したキュー ###\nclass ArrayQueue\n  ### キューの長さを取得 ###\n  attr_reader :size\n\n  ### コンストラクタ ###\n  def initialize(size)\n    @nums = Array.new(size, 0) # キュー要素を格納する配列\n    @front = 0 # 先頭ポインタ。先頭要素を指す\n    @size = 0 # キューの長さ\n  end\n\n  ### キューの容量を取得 ###\n  def capacity\n    @nums.length\n  end\n\n  ### キューが空か判定 ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### エンキュー ###\n  def push(num)\n    raise IndexError, 'キューがいっぱいです' if size == capacity\n\n    # 末尾ポインタを計算し、末尾インデックス + 1 を指す\n    # 剰余演算により、rear が配列末尾を越えた後に先頭へ戻るようにする\n    rear = (@front + size) % capacity\n    # num をキュー末尾に追加\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### デキュー ###\n  def pop\n    num = peek\n    # 先頭ポインタを1つ後ろへ進め、末尾を越えたら配列先頭に戻す\n    @front = (@front + 1) % capacity\n    @size -= 1\n    num\n  end\n\n  ### 先頭要素にアクセス ###\n  def peek\n    raise IndexError, 'キューは空です' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### 表示用のリストを返す ###\n  def to_array\n    res = Array.new(size, 0)\n    j = @front\n\n    for i in 0...size\n      res[i] = @nums[j % capacity]\n      j += 1\n    end\n\n    res\n  end\nend\n
コードの可視化

全画面で見る >

上記の実装によるキューにも制約があり、長さを可変にできません。しかし、この問題の解決は難しくなく、配列を動的配列に置き換えれば容量拡張の仕組みを導入できます。興味があれば自分で実装してみてください。

2 つの実装の比較に関する結論はスタックの場合と同じなので、ここでは繰り返しません。

","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#523","level":2,"title":"5.2.3   キューの典型的な応用","text":"
  • 淘宝の注文。購入者が注文すると、その注文はキューに追加され、システムは順番に従って注文を処理します。ダブルイレブンの期間には短時間で膨大な注文が発生するため、高並行性がエンジニアにとって重点的に解決すべき課題になります。
  • 各種の待機事項。先着順の機能を実現する必要があるあらゆる場面、たとえばプリンターのジョブキューや飲食店の配膳キューなどでは、キューによって処理順序を効果的に維持できます。
","path":["第 5 章   スタックとキュー","5.2   キュー"],"tags":[]},{"location":"chapter_stack_and_queue/stack/","level":1,"title":"5.1   スタック","text":"

スタック(stack)は、後入れ先出しの論理に従う線形データ構造です。

スタックは机の上に積まれた皿の山にたとえられます。1回に1枚の皿しか動かせないとすると、いちばん下の皿を取り出すには、上にある皿を順番にどかす必要があります。この皿をさまざまな型の要素(整数、文字、オブジェクトなど)に置き換えたものが、スタックというデータ構造です。

下図のように、積み重なった要素の上端を「スタックトップ」、下端を「スタックボトム」と呼びます。要素をスタックトップに追加する操作を「プッシュ」、スタックトップの要素を削除する操作を「ポップ」と呼びます。

図 5-1   スタックの後入れ先出しの規則

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511","level":2,"title":"5.1.1   スタックの基本操作","text":"

スタックの基本操作を以下の表に示します。具体的なメソッド名は使用するプログラミング言語によって異なります。ここでは、一般的な push()pop()peek() を例に挙げます。

表 5-1   スタックの操作効率

メソッド 説明 時間計算量 push() 要素をプッシュする(スタックトップに追加) \\(O(1)\\) pop() スタックトップの要素をポップする \\(O(1)\\) peek() スタックトップの要素にアクセスする \\(O(1)\\)

通常は、プログラミング言語に組み込まれているスタッククラスをそのまま利用できます。ただし、専用のスタッククラスが用意されていない言語もあります。その場合は、その言語の「配列」や「連結リスト」をスタックとして用い、プログラムのロジック上でスタックに無関係な操作を無視します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby stack.py
# スタックを初期化\n# Python には組み込みのスタッククラスがないため、list をスタックとして使用できる\nstack: list[int] = []\n\n# 要素をプッシュ\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n# スタックトップの要素にアクセス\npeek: int = stack[-1]\n\n# 要素をポップ\npop: int = stack.pop()\n\n# スタックの長さを取得\nsize: int = len(stack)\n\n# 空かどうかを判定\nis_empty: bool = len(stack) == 0\n
stack.cpp
/* スタックを初期化 */\nstack<int> stack;\n\n/* 要素をプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックトップの要素にアクセス */\nint top = stack.top();\n\n/* 要素をポップ */\nstack.pop(); // 戻り値なし\n\n/* スタックの長さを取得 */\nint size = stack.size();\n\n/* 空かどうかを判定 */\nbool empty = stack.empty();\n
stack.java
/* スタックを初期化 */\nStack<Integer> stack = new Stack<>();\n\n/* 要素をプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックトップの要素にアクセス */\nint peek = stack.peek();\n\n/* 要素をポップ */\nint pop = stack.pop();\n\n/* スタックの長さを取得 */\nint size = stack.size();\n\n/* 空かどうかを判定 */\nboolean isEmpty = stack.isEmpty();\n
stack.cs
/* スタックを初期化 */\nStack<int> stack = new();\n\n/* 要素をプッシュ */\nstack.Push(1);\nstack.Push(3);\nstack.Push(2);\nstack.Push(5);\nstack.Push(4);\n\n/* スタックトップの要素にアクセス */\nint peek = stack.Peek();\n\n/* 要素をポップ */\nint pop = stack.Pop();\n\n/* スタックの長さを取得 */\nint size = stack.Count;\n\n/* 空かどうかを判定 */\nbool isEmpty = stack.Count == 0;\n
stack_test.go
/* スタックを初期化 */\n// Go では、Slice をスタックとして使うのが一般的\nvar stack []int\n\n/* 要素をプッシュ */\nstack = append(stack, 1)\nstack = append(stack, 3)\nstack = append(stack, 2)\nstack = append(stack, 5)\nstack = append(stack, 4)\n\n/* スタックトップの要素にアクセス */\npeek := stack[len(stack)-1]\n\n/* 要素をポップ */\npop := stack[len(stack)-1]\nstack = stack[:len(stack)-1]\n\n/* スタックの長さを取得 */\nsize := len(stack)\n\n/* 空かどうかを判定 */\nisEmpty := len(stack) == 0\n
stack.swift
/* スタックを初期化 */\n// Swift には組み込みのスタッククラスがないため、Array をスタックとして使用できる\nvar stack: [Int] = []\n\n/* 要素をプッシュ */\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n/* スタックトップの要素にアクセス */\nlet peek = stack.last!\n\n/* 要素をポップ */\nlet pop = stack.removeLast()\n\n/* スタックの長さを取得 */\nlet size = stack.count\n\n/* 空かどうかを判定 */\nlet isEmpty = stack.isEmpty\n
stack.js
/* スタックを初期化 */\n// JavaScript には組み込みのスタッククラスがないため、Array をスタックとして使用できる\nconst stack = [];\n\n/* 要素をプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックトップの要素にアクセス */\nconst peek = stack[stack.length-1];\n\n/* 要素をポップ */\nconst pop = stack.pop();\n\n/* スタックの長さを取得 */\nconst size = stack.length;\n\n/* 空かどうかを判定 */\nconst is_empty = stack.length === 0;\n
stack.ts
/* スタックを初期化 */\n// TypeScript には組み込みのスタッククラスがないため、Array をスタックとして使用できる\nconst stack: number[] = [];\n\n/* 要素をプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックトップの要素にアクセス */\nconst peek = stack[stack.length - 1];\n\n/* 要素をポップ */\nconst pop = stack.pop();\n\n/* スタックの長さを取得 */\nconst size = stack.length;\n\n/* 空かどうかを判定 */\nconst is_empty = stack.length === 0;\n
stack.dart
/* スタックを初期化 */\n// Dart には組み込みのスタッククラスがないため、List をスタックとして使用できる\nList<int> stack = [];\n\n/* 要素をプッシュ */\nstack.add(1);\nstack.add(3);\nstack.add(2);\nstack.add(5);\nstack.add(4);\n\n/* スタックトップの要素にアクセス */\nint peek = stack.last;\n\n/* 要素をポップ */\nint pop = stack.removeLast();\n\n/* スタックの長さを取得 */\nint size = stack.length;\n\n/* 空かどうかを判定 */\nbool isEmpty = stack.isEmpty;\n
stack.rs
/* スタックを初期化 */\n// Vec をスタックとして使用する\nlet mut stack: Vec<i32> = Vec::new();\n\n/* 要素をプッシュ */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* スタックトップの要素にアクセス */\nlet top = stack.last().unwrap();\n\n/* 要素をポップ */\nlet pop = stack.pop().unwrap();\n\n/* スタックの長さを取得 */\nlet size = stack.len();\n\n/* 空かどうかを判定 */\nlet is_empty = stack.is_empty();\n
stack.c
// C には組み込みのスタックがない\n
stack.kt
/* スタックを初期化 */\nval stack = Stack<Int>()\n\n/* 要素をプッシュ */\nstack.push(1)\nstack.push(3)\nstack.push(2)\nstack.push(5)\nstack.push(4)\n\n/* スタックトップの要素にアクセス */\nval peek = stack.peek()\n\n/* 要素をポップ */\nval pop = stack.pop()\n\n/* スタックの長さを取得 */\nval size = stack.size\n\n/* 空かどうかを判定 */\nval isEmpty = stack.isEmpty()\n
stack.rb
# スタックを初期化\n# Ruby には組み込みのスタッククラスがないため、Array をスタックとして使用できる\nstack = []\n\n# 要素をプッシュ\nstack << 1\nstack << 3\nstack << 2\nstack << 5\nstack << 4\n\n# スタックトップの要素にアクセス\npeek = stack.last\n\n# 要素をポップ\npop = stack.pop\n\n# スタックの長さを取得\nsize = stack.length\n\n# 空かどうかを判定\nis_empty = stack.empty?\n
実行の可視化

https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20%23%20Python%20%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%A0%88%E7%B1%BB%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8A%8A%20list%20%E5%BD%93%E4%BD%9C%E6%A0%88%E6%9D%A5%E4%BD%BF%E7%94%A8%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.append%281%29%0A%20%20%20%20stack.append%283%29%0A%20%20%20%20stack.append%282%29%0A%20%20%20%20stack.append%285%29%0A%20%20%20%20stack.append%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512","level":2,"title":"5.1.2   スタックの実装","text":"

スタックの動作の仕組みをより深く理解するために、自分でスタッククラスを実装してみましょう。

スタックは後入れ先出しの原則に従うため、要素の追加や削除はスタックトップでしか行えません。一方、配列や連結リストでは任意の位置で要素を追加・削除できます。つまり、スタックは制限付きの配列または連結リストとみなせます。 言い換えると、配列や連結リストのうち無関係な操作を「隠蔽」することで、外から見た振る舞いをスタックの特性に合わせられます。

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   連結リストによる実装","text":"

連結リストでスタックを実装する場合、連結リストの先頭ノードをスタックトップ、末尾ノードをスタックボトムとみなせます。

下図のように、プッシュ操作では要素を連結リストの先頭に挿入するだけでよく、このノード挿入方法は「頭部挿入法」と呼ばれます。ポップ操作では、先頭ノードを連結リストから削除するだけです。

LinkedListStackpush()pop()

図 5-2   連結リストによるスタック実装のプッシュ・ポップ操作

以下は、連結リストによってスタックを実装したコード例です:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_stack.py
class LinkedListStack:\n    \"\"\"連結リストベースのスタック\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self._peek: ListNode | None = None\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"スタックの長さを取得\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"スタックが空かどうかを判定\"\"\"\n        return self._size == 0\n\n    def push(self, val: int):\n        \"\"\"プッシュ\"\"\"\n        node = ListNode(val)\n        node.next = self._peek\n        self._peek = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"ポップ\"\"\"\n        num = self.peek()\n        self._peek = self._peek.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"スタックトップの要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"スタックが空です\")\n        return self._peek.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"表示用にリストへ変換\"\"\"\n        arr = []\n        node = self._peek\n        while node:\n            arr.append(node.val)\n            node = node.next\n        arr.reverse()\n        return arr\n
linkedlist_stack.cpp
/* 連結リストベースのスタック */\nclass LinkedListStack {\n  private:\n    ListNode *stackTop; // 先頭ノードをスタックトップとする\n    int stkSize;        // スタックの長さ\n\n  public:\n    LinkedListStack() {\n        stackTop = nullptr;\n        stkSize = 0;\n    }\n\n    ~LinkedListStack() {\n        // 連結リストを走査してノードを削除し、メモリを解放する\n        freeMemoryLinkedList(stackTop);\n    }\n\n    /* スタックの長さを取得 */\n    int size() {\n        return stkSize;\n    }\n\n    /* スタックが空かどうかを判定 */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* プッシュ */\n    void push(int num) {\n        ListNode *node = new ListNode(num);\n        node->next = stackTop;\n        stackTop = node;\n        stkSize++;\n    }\n\n    /* ポップ */\n    int pop() {\n        int num = top();\n        ListNode *tmp = stackTop;\n        stackTop = stackTop->next;\n        // メモリを解放する\n        delete tmp;\n        stkSize--;\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"スタックが空です\");\n        return stackTop->val;\n    }\n\n    /* List を Array に変換して返す */\n    vector<int> toVector() {\n        ListNode *node = stackTop;\n        vector<int> res(size());\n        for (int i = res.size() - 1; i >= 0; i--) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_stack.java
/* 連結リストベースのスタック */\nclass LinkedListStack {\n    private ListNode stackPeek; // 先頭ノードをスタックトップとする\n    private int stkSize = 0; // スタックの長さ\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* スタックの長さを取得 */\n    public int size() {\n        return stkSize;\n    }\n\n    /* スタックが空かどうかを判定 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* プッシュ */\n    public void push(int num) {\n        ListNode node = new ListNode(num);\n        node.next = stackPeek;\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* ポップ */\n    public int pop() {\n        int num = peek();\n        stackPeek = stackPeek.next;\n        stkSize--;\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stackPeek.val;\n    }\n\n    /* List を Array に変換して返す */\n    public int[] toArray() {\n        ListNode node = stackPeek;\n        int[] res = new int[size()];\n        for (int i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.cs
/* 連結リストベースのスタック */\nclass LinkedListStack {\n    ListNode? stackPeek;  // 先頭ノードをスタックトップとする\n    int stkSize = 0;   // スタックの長さ\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* スタックの長さを取得 */\n    public int Size() {\n        return stkSize;\n    }\n\n    /* スタックが空かどうかを判定 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* プッシュ */\n    public void Push(int num) {\n        ListNode node = new(num) {\n            next = stackPeek\n        };\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* ポップ */\n    public int Pop() {\n        int num = Peek();\n        stackPeek = stackPeek!.next;\n        stkSize--;\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stackPeek!.val;\n    }\n\n    /* List を Array に変換して返す */\n    public int[] ToArray() {\n        if (stackPeek == null)\n            return [];\n\n        ListNode? node = stackPeek;\n        int[] res = new int[Size()];\n        for (int i = res.Length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.go
/* 連結リストベースのスタック */\ntype linkedListStack struct {\n    // 組み込みパッケージ list でスタックを実装する\n    data *list.List\n}\n\n/* スタックを初期化 */\nfunc newLinkedListStack() *linkedListStack {\n    return &linkedListStack{\n        data: list.New(),\n    }\n}\n\n/* プッシュ */\nfunc (s *linkedListStack) push(value int) {\n    s.data.PushBack(value)\n}\n\n/* ポップ */\nfunc (s *linkedListStack) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* スタックトップの要素にアクセス */\nfunc (s *linkedListStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* スタックの長さを取得 */\nfunc (s *linkedListStack) size() int {\n    return s.data.Len()\n}\n\n/* スタックが空かどうかを判定 */\nfunc (s *linkedListStack) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* 表示用に List を取得 */\nfunc (s *linkedListStack) toList() *list.List {\n    return s.data\n}\n
linkedlist_stack.swift
/* 連結リストベースのスタック */\nclass LinkedListStack {\n    private var _peek: ListNode? // 先頭ノードをスタックトップとする\n    private var _size: Int // スタックの長さ\n\n    init() {\n        _size = 0\n    }\n\n    /* スタックの長さを取得 */\n    func size() -> Int {\n        _size\n    }\n\n    /* スタックが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* プッシュ */\n    func push(num: Int) {\n        let node = ListNode(x: num)\n        node.next = _peek\n        _peek = node\n        _size += 1\n    }\n\n    /* ポップ */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        _peek = _peek?.next\n        _size -= 1\n        return num\n    }\n\n    /* スタックトップの要素にアクセス */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"スタックが空です\")\n        }\n        return _peek!.val\n    }\n\n    /* List を Array に変換して返す */\n    func toArray() -> [Int] {\n        var node = _peek\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices.reversed() {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_stack.js
/* 連結リストベースのスタック */\nclass LinkedListStack {\n    #stackPeek; // 先頭ノードをスタックトップとする\n    #stkSize = 0; // スタックの長さ\n\n    constructor() {\n        this.#stackPeek = null;\n    }\n\n    /* スタックの長さを取得 */\n    get size() {\n        return this.#stkSize;\n    }\n\n    /* スタックが空かどうかを判定 */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* プッシュ */\n    push(num) {\n        const node = new ListNode(num);\n        node.next = this.#stackPeek;\n        this.#stackPeek = node;\n        this.#stkSize++;\n    }\n\n    /* ポップ */\n    pop() {\n        const num = this.peek();\n        this.#stackPeek = this.#stackPeek.next;\n        this.#stkSize--;\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    peek() {\n        if (!this.#stackPeek) throw new Error('スタックが空');\n        return this.#stackPeek.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    toArray() {\n        let node = this.#stackPeek;\n        const res = new Array(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.ts
/* 連結リストベースのスタック */\nclass LinkedListStack {\n    private stackPeek: ListNode | null; // 先頭ノードをスタックトップとする\n    private stkSize: number = 0; // スタックの長さ\n\n    constructor() {\n        this.stackPeek = null;\n    }\n\n    /* スタックの長さを取得 */\n    get size(): number {\n        return this.stkSize;\n    }\n\n    /* スタックが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* プッシュ */\n    push(num: number): void {\n        const node = new ListNode(num);\n        node.next = this.stackPeek;\n        this.stackPeek = node;\n        this.stkSize++;\n    }\n\n    /* ポップ */\n    pop(): number {\n        const num = this.peek();\n        if (!this.stackPeek) throw new Error('スタックが空です');\n        this.stackPeek = this.stackPeek.next;\n        this.stkSize--;\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    peek(): number {\n        if (!this.stackPeek) throw new Error('スタックが空です');\n        return this.stackPeek.val;\n    }\n\n    /* 連結リストを Array に変換して返す */\n    toArray(): number[] {\n        let node = this.stackPeek;\n        const res = new Array<number>(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.dart
/* 連結リストクラスに基づくスタック */\nclass LinkedListStack {\n  ListNode? _stackPeek; // 先頭ノードをスタックトップとする\n  int _stkSize = 0; // スタックの長さ\n\n  LinkedListStack() {\n    _stackPeek = null;\n  }\n\n  /* スタックの長さを取得 */\n  int size() {\n    return _stkSize;\n  }\n\n  /* スタックが空かどうかを判定 */\n  bool isEmpty() {\n    return _stkSize == 0;\n  }\n\n  /* プッシュ */\n  void push(int _num) {\n    final ListNode node = ListNode(_num);\n    node.next = _stackPeek;\n    _stackPeek = node;\n    _stkSize++;\n  }\n\n  /* ポップ */\n  int pop() {\n    final int _num = peek();\n    _stackPeek = _stackPeek!.next;\n    _stkSize--;\n    return _num;\n  }\n\n  /* スタックトップの要素にアクセス */\n  int peek() {\n    if (_stackPeek == null) {\n      throw Exception(\"スタックが空です\");\n    }\n    return _stackPeek!.val;\n  }\n\n  /* 連結リストを List に変換して返す */\n  List<int> toList() {\n    ListNode? node = _stackPeek;\n    List<int> list = [];\n    while (node != null) {\n      list.add(node.val);\n      node = node.next;\n    }\n    list = list.reversed.toList();\n    return list;\n  }\n}\n
linkedlist_stack.rs
/* 連結リストベースのスタック */\n#[allow(dead_code)]\npub struct LinkedListStack<T> {\n    stack_peek: Option<Rc<RefCell<ListNode<T>>>>, // 先頭ノードをスタックトップとする\n    stk_size: usize,                              // スタックの長さ\n}\n\nimpl<T: Copy> LinkedListStack<T> {\n    pub fn new() -> Self {\n        Self {\n            stack_peek: None,\n            stk_size: 0,\n        }\n    }\n\n    /* スタックの長さを取得 */\n    pub fn size(&self) -> usize {\n        return self.stk_size;\n    }\n\n    /* スタックが空かどうかを判定 */\n    pub fn is_empty(&self) -> bool {\n        return self.size() == 0;\n    }\n\n    /* プッシュ */\n    pub fn push(&mut self, num: T) {\n        let node = ListNode::new(num);\n        node.borrow_mut().next = self.stack_peek.take();\n        self.stack_peek = Some(node);\n        self.stk_size += 1;\n    }\n\n    /* ポップ */\n    pub fn pop(&mut self) -> Option<T> {\n        self.stack_peek.take().map(|old_head| {\n            self.stack_peek = old_head.borrow_mut().next.take();\n            self.stk_size -= 1;\n\n            old_head.borrow().val\n        })\n    }\n\n    /* スタックトップの要素にアクセス */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.stack_peek.as_ref()\n    }\n\n    /* List を Array に変換して返す */\n    pub fn to_array(&self) -> Vec<T> {\n        fn _to_array<T: Sized + Copy>(head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n            if let Some(node) = head {\n                let mut nums = _to_array(node.borrow().next.as_ref());\n                nums.push(node.borrow().val);\n                return nums;\n            }\n            return Vec::new();\n        }\n\n        _to_array(self.peek())\n    }\n}\n
linkedlist_stack.c
/* 連結リストベースのスタック */\ntypedef struct {\n    ListNode *top; // 先頭ノードをスタックトップとする\n    int size;      // スタックの長さ\n} LinkedListStack;\n\n/* コンストラクタ */\nLinkedListStack *newLinkedListStack() {\n    LinkedListStack *s = malloc(sizeof(LinkedListStack));\n    s->top = NULL;\n    s->size = 0;\n    return s;\n}\n\n/* デストラクタ */\nvoid delLinkedListStack(LinkedListStack *s) {\n    while (s->top) {\n        ListNode *n = s->top->next;\n        free(s->top);\n        s->top = n;\n    }\n    free(s);\n}\n\n/* スタックの長さを取得 */\nint size(LinkedListStack *s) {\n    return s->size;\n}\n\n/* スタックが空かどうかを判定 */\nbool isEmpty(LinkedListStack *s) {\n    return size(s) == 0;\n}\n\n/* プッシュ */\nvoid push(LinkedListStack *s, int num) {\n    ListNode *node = (ListNode *)malloc(sizeof(ListNode));\n    node->next = s->top; // 新しく追加したノードのポインタフィールドを更新\n    node->val = num;     // 新しく追加したノードのデータフィールドを更新\n    s->top = node;       // スタックトップを更新\n    s->size++;           // スタックサイズを更新\n}\n\n/* スタックトップの要素にアクセス */\nint peek(LinkedListStack *s) {\n    if (s->size == 0) {\n        printf(\"スタックは空です\\n\");\n        return INT_MAX;\n    }\n    return s->top->val;\n}\n\n/* ポップ */\nint pop(LinkedListStack *s) {\n    int val = peek(s);\n    ListNode *tmp = s->top;\n    s->top = s->top->next;\n    // メモリを解放する\n    free(tmp);\n    s->size--;\n    return val;\n}\n
linkedlist_stack.kt
/* 連結リストベースのスタック */\nclass LinkedListStack(\n    private var stackPeek: ListNode? = null, // 先頭ノードをスタックトップとする\n    private var stkSize: Int = 0 // スタックの長さ\n) {\n\n    /* スタックの長さを取得 */\n    fun size(): Int {\n        return stkSize\n    }\n\n    /* スタックが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* プッシュ */\n    fun push(num: Int) {\n        val node = ListNode(num)\n        node.next = stackPeek\n        stackPeek = node\n        stkSize++\n    }\n\n    /* ポップ */\n    fun pop(): Int? {\n        val num = peek()\n        stackPeek = stackPeek?.next\n        stkSize--\n        return num\n    }\n\n    /* スタックトップの要素にアクセス */\n    fun peek(): Int? {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stackPeek?._val\n    }\n\n    /* List を Array に変換して返す */\n    fun toArray(): IntArray {\n        var node = stackPeek\n        val res = IntArray(size())\n        for (i in res.size - 1 downTo 0) {\n            res[i] = node?._val!!\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_stack.rb
### 連結リストで実装したスタック ###\nclass LinkedListStack\n  attr_reader :size\n\n  ### コンストラクタ ###\n  def initialize\n    @size = 0\n  end\n\n  ### スタックが空か判定 ###\n  def is_empty?\n    @peek.nil?\n  end\n\n  ### プッシュ ###\n  def push(val)\n    node = ListNode.new(val)\n    node.next = @peek\n    @peek = node\n    @size += 1\n  end\n\n  ### ポップ ###\n  def pop\n    num = peek\n    @peek = @peek.next\n    @size -= 1\n    num\n  end\n\n  ### スタックトップ要素を参照 ###\n  def peek\n    raise IndexError, 'スタックは空です' if is_empty?\n\n    @peek.val\n  end\n\n  ### 連結リストを Array に変換して返す ###\n  def to_array\n    arr = []\n    node = @peek\n    while node\n      arr << node.val\n      node = node.next\n    end\n    arr.reverse\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#2","level":3,"title":"2.   配列による実装","text":"

配列でスタックを実装する場合、配列の末尾をスタックトップとして扱えます。下図のように、プッシュとポップはそれぞれ配列末尾への要素追加と削除に対応し、どちらの時間計算量も \\(O(1)\\) です。

ArrayStackpush()pop()

図 5-3   配列によるスタック実装のプッシュ・ポップ操作

プッシュされる要素は際限なく増える可能性があるため、動的配列を使えば、配列の拡張を自前で処理する必要がありません。以下にコード例を示します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_stack.py
class ArrayStack:\n    \"\"\"配列ベースのスタック\"\"\"\n\n    def __init__(self):\n        \"\"\"コンストラクタ\"\"\"\n        self._stack: list[int] = []\n\n    def size(self) -> int:\n        \"\"\"スタックの長さを取得\"\"\"\n        return len(self._stack)\n\n    def is_empty(self) -> bool:\n        \"\"\"スタックが空かどうかを判定\"\"\"\n        return self.size() == 0\n\n    def push(self, item: int):\n        \"\"\"プッシュ\"\"\"\n        self._stack.append(item)\n\n    def pop(self) -> int:\n        \"\"\"ポップ\"\"\"\n        if self.is_empty():\n            raise IndexError(\"スタックが空です\")\n        return self._stack.pop()\n\n    def peek(self) -> int:\n        \"\"\"スタックトップの要素にアクセス\"\"\"\n        if self.is_empty():\n            raise IndexError(\"スタックが空です\")\n        return self._stack[-1]\n\n    def to_list(self) -> list[int]:\n        \"\"\"表示用のリストを返す\"\"\"\n        return self._stack\n
array_stack.cpp
/* 配列ベースのスタック */\nclass ArrayStack {\n  private:\n    vector<int> stack;\n\n  public:\n    /* スタックの長さを取得 */\n    int size() {\n        return stack.size();\n    }\n\n    /* スタックが空かどうかを判定 */\n    bool isEmpty() {\n        return stack.size() == 0;\n    }\n\n    /* プッシュ */\n    void push(int num) {\n        stack.push_back(num);\n    }\n\n    /* ポップ */\n    int pop() {\n        int num = top();\n        stack.pop_back();\n        return num;\n    }\n\n    /* スタックトップの要素にアクセス */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"スタックが空です\");\n        return stack.back();\n    }\n\n    /* Vector を返す */\n    vector<int> toVector() {\n        return stack;\n    }\n};\n
array_stack.java
/* 配列ベースのスタック */\nclass ArrayStack {\n    private ArrayList<Integer> stack;\n\n    public ArrayStack() {\n        // リスト(動的配列)を初期化する\n        stack = new ArrayList<>();\n    }\n\n    /* スタックの長さを取得 */\n    public int size() {\n        return stack.size();\n    }\n\n    /* スタックが空かどうかを判定 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* プッシュ */\n    public void push(int num) {\n        stack.add(num);\n    }\n\n    /* ポップ */\n    public int pop() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.remove(size() - 1);\n    }\n\n    /* スタックトップの要素にアクセス */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.get(size() - 1);\n    }\n\n    /* List を Array に変換して返す */\n    public Object[] toArray() {\n        return stack.toArray();\n    }\n}\n
array_stack.cs
/* 配列ベースのスタック */\nclass ArrayStack {\n    List<int> stack;\n    public ArrayStack() {\n        // リスト(動的配列)を初期化する\n        stack = [];\n    }\n\n    /* スタックの長さを取得 */\n    public int Size() {\n        return stack.Count;\n    }\n\n    /* スタックが空かどうかを判定 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* プッシュ */\n    public void Push(int num) {\n        stack.Add(num);\n    }\n\n    /* ポップ */\n    public int Pop() {\n        if (IsEmpty())\n            throw new Exception();\n        var val = Peek();\n        stack.RemoveAt(Size() - 1);\n        return val;\n    }\n\n    /* スタックトップの要素にアクセス */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stack[Size() - 1];\n    }\n\n    /* List を Array に変換して返す */\n    public int[] ToArray() {\n        return [.. stack];\n    }\n}\n
array_stack.go
/* 配列ベースのスタック */\ntype arrayStack struct {\n    data []int // データ\n}\n\n/* スタックを初期化 */\nfunc newArrayStack() *arrayStack {\n    return &arrayStack{\n        // スタックの長さを 0、容量を 16 に設定\n        data: make([]int, 0, 16),\n    }\n}\n\n/* スタックの長さ */\nfunc (s *arrayStack) size() int {\n    return len(s.data)\n}\n\n/* スタックが空かどうか */\nfunc (s *arrayStack) isEmpty() bool {\n    return s.size() == 0\n}\n\n/* プッシュ */\nfunc (s *arrayStack) push(v int) {\n    // スライスは自動で拡張される\n    s.data = append(s.data, v)\n}\n\n/* ポップ */\nfunc (s *arrayStack) pop() any {\n    val := s.peek()\n    s.data = s.data[:len(s.data)-1]\n    return val\n}\n\n/* スタックトップ要素を取得する */\nfunc (s *arrayStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    val := s.data[len(s.data)-1]\n    return val\n}\n\n/* 表示用に Slice を取得 */\nfunc (s *arrayStack) toSlice() []int {\n    return s.data\n}\n
array_stack.swift
/* 配列ベースのスタック */\nclass ArrayStack {\n    private var stack: [Int]\n\n    init() {\n        // リスト(動的配列)を初期化する\n        stack = []\n    }\n\n    /* スタックの長さを取得 */\n    func size() -> Int {\n        stack.count\n    }\n\n    /* スタックが空かどうかを判定 */\n    func isEmpty() -> Bool {\n        stack.isEmpty\n    }\n\n    /* プッシュ */\n    func push(num: Int) {\n        stack.append(num)\n    }\n\n    /* ポップ */\n    @discardableResult\n    func pop() -> Int {\n        if isEmpty() {\n            fatalError(\"スタックが空です\")\n        }\n        return stack.removeLast()\n    }\n\n    /* スタックトップの要素にアクセス */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"スタックが空です\")\n        }\n        return stack.last!\n    }\n\n    /* List を Array に変換して返す */\n    func toArray() -> [Int] {\n        stack\n    }\n}\n
array_stack.js
/* 配列ベースのスタック */\nclass ArrayStack {\n    #stack;\n    constructor() {\n        this.#stack = [];\n    }\n\n    /* スタックの長さを取得 */\n    get size() {\n        return this.#stack.length;\n    }\n\n    /* スタックが空かどうかを判定 */\n    isEmpty() {\n        return this.#stack.length === 0;\n    }\n\n    /* プッシュ */\n    push(num) {\n        this.#stack.push(num);\n    }\n\n    /* ポップ */\n    pop() {\n        if (this.isEmpty()) throw new Error('スタックが空');\n        return this.#stack.pop();\n    }\n\n    /* スタックトップの要素にアクセス */\n    top() {\n        if (this.isEmpty()) throw new Error('スタックが空');\n        return this.#stack[this.#stack.length - 1];\n    }\n\n    /* Array を返す */\n    toArray() {\n        return this.#stack;\n    }\n}\n
array_stack.ts
/* 配列ベースのスタック */\nclass ArrayStack {\n    private stack: number[];\n    constructor() {\n        this.stack = [];\n    }\n\n    /* スタックの長さを取得 */\n    get size(): number {\n        return this.stack.length;\n    }\n\n    /* スタックが空かどうかを判定 */\n    isEmpty(): boolean {\n        return this.stack.length === 0;\n    }\n\n    /* プッシュ */\n    push(num: number): void {\n        this.stack.push(num);\n    }\n\n    /* ポップ */\n    pop(): number | undefined {\n        if (this.isEmpty()) throw new Error('スタックが空です');\n        return this.stack.pop();\n    }\n\n    /* スタックトップの要素にアクセス */\n    top(): number | undefined {\n        if (this.isEmpty()) throw new Error('スタックが空です');\n        return this.stack[this.stack.length - 1];\n    }\n\n    /* Array を返す */\n    toArray() {\n        return this.stack;\n    }\n}\n
array_stack.dart
/* 配列ベースのスタック */\nclass ArrayStack {\n  late List<int> _stack;\n  ArrayStack() {\n    _stack = [];\n  }\n\n  /* スタックの長さを取得 */\n  int size() {\n    return _stack.length;\n  }\n\n  /* スタックが空かどうかを判定 */\n  bool isEmpty() {\n    return _stack.isEmpty;\n  }\n\n  /* プッシュ */\n  void push(int _num) {\n    _stack.add(_num);\n  }\n\n  /* ポップ */\n  int pop() {\n    if (isEmpty()) {\n      throw Exception(\"スタックが空です\");\n    }\n    return _stack.removeLast();\n  }\n\n  /* スタックトップの要素にアクセス */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"スタックが空です\");\n    }\n    return _stack.last;\n  }\n\n  /* スタックを Array に変換して返す */\n  List<int> toArray() => _stack;\n}\n
array_stack.rs
/* 配列ベースのスタック */\nstruct ArrayStack<T> {\n    stack: Vec<T>,\n}\n\nimpl<T> ArrayStack<T> {\n    /* スタックを初期化 */\n    fn new() -> ArrayStack<T> {\n        ArrayStack::<T> {\n            stack: Vec::<T>::new(),\n        }\n    }\n\n    /* スタックの長さを取得 */\n    fn size(&self) -> usize {\n        self.stack.len()\n    }\n\n    /* スタックが空かどうかを判定 */\n    fn is_empty(&self) -> bool {\n        self.size() == 0\n    }\n\n    /* プッシュ */\n    fn push(&mut self, num: T) {\n        self.stack.push(num);\n    }\n\n    /* ポップ */\n    fn pop(&mut self) -> Option<T> {\n        self.stack.pop()\n    }\n\n    /* スタックトップの要素にアクセス */\n    fn peek(&self) -> Option<&T> {\n        if self.is_empty() {\n            panic!(\"スタックが空です\")\n        };\n        self.stack.last()\n    }\n\n    /* &Vec を返す */\n    fn to_array(&self) -> &Vec<T> {\n        &self.stack\n    }\n}\n
array_stack.c
/* 配列ベースのスタック */\ntypedef struct {\n    int *data;\n    int size;\n} ArrayStack;\n\n/* コンストラクタ */\nArrayStack *newArrayStack() {\n    ArrayStack *stack = malloc(sizeof(ArrayStack));\n    // 大きめの容量で初期化し、拡張を避ける\n    stack->data = malloc(sizeof(int) * MAX_SIZE);\n    stack->size = 0;\n    return stack;\n}\n\n/* デストラクタ */\nvoid delArrayStack(ArrayStack *stack) {\n    free(stack->data);\n    free(stack);\n}\n\n/* スタックの長さを取得 */\nint size(ArrayStack *stack) {\n    return stack->size;\n}\n\n/* スタックが空かどうかを判定 */\nbool isEmpty(ArrayStack *stack) {\n    return stack->size == 0;\n}\n\n/* プッシュ */\nvoid push(ArrayStack *stack, int num) {\n    if (stack->size == MAX_SIZE) {\n        printf(\"スタックは満杯です\\n\");\n        return;\n    }\n    stack->data[stack->size] = num;\n    stack->size++;\n}\n\n/* スタックトップの要素にアクセス */\nint peek(ArrayStack *stack) {\n    if (stack->size == 0) {\n        printf(\"スタックは空です\\n\");\n        return INT_MAX;\n    }\n    return stack->data[stack->size - 1];\n}\n\n/* ポップ */\nint pop(ArrayStack *stack) {\n    int val = peek(stack);\n    stack->size--;\n    return val;\n}\n
array_stack.kt
/* 配列ベースのスタック */\nclass ArrayStack {\n    // リスト(動的配列)を初期化する\n    private val stack = mutableListOf<Int>()\n\n    /* スタックの長さを取得 */\n    fun size(): Int {\n        return stack.size\n    }\n\n    /* スタックが空かどうかを判定 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* プッシュ */\n    fun push(num: Int) {\n        stack.add(num)\n    }\n\n    /* ポップ */\n    fun pop(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack.removeAt(size() - 1)\n    }\n\n    /* スタックトップの要素にアクセス */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack[size() - 1]\n    }\n\n    /* List を Array に変換して返す */\n    fun toArray(): Array<Any> {\n        return stack.toTypedArray()\n    }\n}\n
array_stack.rb
### 配列で実装したスタック ###\nclass ArrayStack\n  ### コンストラクタ ###\n  def initialize\n    @stack = []\n  end\n\n  ### スタックの長さを取得 ###\n  def size\n    @stack.length\n  end\n\n  ### スタックが空か判定 ###\n  def is_empty?\n    @stack.empty?\n  end\n\n  ### プッシュ ###\n  def push(item)\n    @stack << item\n  end\n\n  ### ポップ ###\n  def pop\n    raise IndexError, 'スタックは空です' if is_empty?\n\n    @stack.pop\n  end\n\n  ### スタックトップ要素を参照 ###\n  def peek\n    raise IndexError, 'スタックは空です' if is_empty?\n\n    @stack.last\n  end\n\n  ### 表示用のリストを返す ###\n  def to_array\n    @stack\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#513-2","level":2,"title":"5.1.3   2つの実装の比較","text":"

対応する操作

どちらの実装も、スタックの定義に含まれる各種操作をサポートします。配列ベースの実装はランダムアクセスも可能ですが、これはスタックの定義範囲を超えているため、通常は利用しません。

時間効率

配列ベースの実装では、プッシュとポップの両方があらかじめ確保された連続メモリ上で行われるため、キャッシュ局所性が高く、効率に優れます。ただし、プッシュ時に配列容量を超えると拡張処理が発生し、その1回のプッシュの時間計算量は \\(O(n)\\) になります。

連結リストベースの実装では、サイズ拡張が非常に柔軟であり、前述のような配列拡張による効率低下はありません。ただし、プッシュ時にはノードオブジェクトの初期化とポインタの更新が必要になるため、効率は相対的に低くなります。もっとも、プッシュする要素自体がノードオブジェクトであれば、初期化の手間を省けるため、効率を高められます。

以上を踏まえると、プッシュおよびポップの対象が intdouble のような基本データ型である場合、次の結論が得られます。

  • 配列ベースのスタックは拡張時に効率が低下しますが、拡張は低頻度の操作であるため、平均効率はより高くなります。
  • 連結リストベースのスタックは、より安定した効率を提供できます。

空間効率

リストを初期化するとき、システムは「初期容量」を割り当てますが、この容量は実際の必要量を上回ることがあります。また、拡張は通常、一定の倍率(たとえば2倍)で行われるため、拡張後の容量も実際の必要量を超える可能性があります。したがって、配列ベースのスタックは一定のメモリ浪費を招く可能性があります。

一方で、連結リストのノードはポインタを追加で保持する必要があるため、連結リストノードは相対的に大きな領域を占有します。

以上より、どちらの実装がより省メモリかを単純に断定することはできず、具体的な状況に応じて分析する必要があります。

","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   スタックの典型的な応用","text":"
  • ブラウザにおける戻ると進む、ソフトウェアにおける取り消しとやり直し。新しいWebページを開くたびに、ブラウザは直前のページをスタックにプッシュするため、戻る操作によって前のページに戻れます。戻る操作は実際にはポップに相当します。戻ると進むを同時にサポートするには、2つのスタックを組み合わせて実現する必要があります。
  • プログラムのメモリ管理。関数を呼び出すたびに、システムはスタックトップにスタックフレームを追加し、関数のコンテキスト情報を記録します。再帰関数では、下向きに再帰していく段階でプッシュが繰り返され、上向きにバックトラックする段階でポップが繰り返されます。
","path":["第 5 章   スタックとキュー","5.1   スタック"],"tags":[]},{"location":"chapter_stack_and_queue/summary/","level":1,"title":"5.4   まとめ","text":"","path":["第 5 章   スタックとキュー","5.4   まとめ"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • スタックは後入れ先出しの原則に従うデータ構造であり、配列または連結リストで実装できます。
  • 時間効率の面では、スタックの配列実装は平均効率が高い一方、拡張時には 1 回のプッシュ操作の時間計算量が \\(O(n)\\) まで悪化します。これに対して、スタックの連結リスト実装はより安定した効率を示します。
  • 空間効率の面では、スタックの配列実装はある程度の領域の無駄を生む可能性があります。ただし、連結リストのノードが占有するメモリは配列要素よりも大きい点に注意が必要です。
  • キューは先入れ先出しの原則に従うデータ構造であり、同様に配列または連結リストで実装できます。時間効率と空間効率の比較における結論は、前述のスタックの場合と似ています。
  • 両端キューはより高い自由度を持つキューであり、両端で要素の追加と削除を行えます。
","path":["第 5 章   スタックとキュー","5.4   まとめ"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:ブラウザの進む・戻るは双方向連結リストで実装されているのですか?

ブラウザの進む・戻る機能の本質は「スタック」の表れです。ユーザーが新しいページにアクセスすると、そのページはスタックの先頭に追加されます。ユーザーが戻るボタンをクリックすると、そのページはスタックの先頭から取り出されます。両端キューを使うといくつかの追加操作を簡単に実装でき、この点は「両端キュー」の章で触れています。

Q:ポップした後、そのノードのメモリを解放する必要はありますか?

後で取り出したノードを引き続き使うのであれば、メモリを解放する必要はありません。以降そのノードを使わない場合でも、JavaPython などの言語には自動ガベージコレクション機構があるため、手動でメモリを解放する必要はありません。一方、CC++ では手動でメモリを解放する必要があります。

Q:両端キューは 2 つのスタックをつなげたように見えますが、用途は何ですか?

両端キューは、スタックとキューの組み合わせ、あるいは 2 つのスタックをつなげたもののような構造です。表しているのはスタック + キューのロジックなので、スタックとキューのすべての応用を実現でき、しかもより柔軟です。

Q:取り消し(undo)とやり直し(redo)は具体的にどのように実装されますか?

2 つのスタックを使い、スタック A を取り消し用、スタック B をやり直し用に使います。

  1. ユーザーが操作を 1 つ実行するたびに、その操作をスタック A にプッシュし、スタック B を空にします。
  2. ユーザーが「取り消し」を実行したときは、スタック A から直近の操作をポップし、それをスタック B にプッシュします。
  3. ユーザーが「やり直し」を実行したときは、スタック B から直近の操作をポップし、それをスタック A にプッシュします。
","path":["第 5 章   スタックとキュー","5.4   まとめ"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"第 7 章   木","text":"

Abstract

大樹は生命力に満ち、根は深く葉は生い茂り、枝は豊かに広がる。

それはデータ分割統治の生き生きとした姿を私たちに示してくれる。

","path":["第 7 章   木"],"tags":[]},{"location":"chapter_tree/#_1","level":2,"title":"章の内容","text":"
  • 7.1   二分木
  • 7.2   二分木の走査
  • 7.3   二分木の配列表現
  • 7.4   二分探索木
  • 7.5   AVL 木 *
  • 7.6   まとめ
","path":["第 7 章   木"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/","level":1,"title":"7.3   二分木の配列表現","text":"

連結リスト表現では、二分木の記憶単位はノード TreeNode であり、ノード同士はポインタによって接続されます。前節では、連結リスト表現における二分木の各種基本操作を紹介しました。

では、配列で二分木を表現できるでしょうか?答えはもちろん可能です。

","path":["第 7 章   木","7.3   二分木の配列表現"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#731","level":2,"title":"7.3.1   充足二分木を表現する","text":"

まずは簡単な例を考えます。与えられた 1 本の充足二分木について、すべてのノードをレベル順走査の順に配列へ格納すると、各ノードは一意な配列インデックスに対応します。

レベル順走査の性質に基づくと、親ノードのインデックスと子ノードのインデックスの間にある「対応式」を導けます。あるノードのインデックスが \\(i\\) なら、その左子ノードのインデックスは \\(2i + 1\\) 、右子ノードのインデックスは \\(2i + 2\\) です。以下の図は、各ノードインデックス間の対応関係を示しています。

図 7-12   充足二分木の配列表現

対応式は、連結リストにおけるノード参照(ポインタ)と同じ役割を果たします。与えられた配列内の任意のノードについて、この対応式を使えばその左(右)子ノードにアクセスできます。

","path":["第 7 章   木","7.3   二分木の配列表現"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732","level":2,"title":"7.3.2   任意の二分木を表現する","text":"

充足二分木は特殊なケースであり、一般の二分木では中間層に多数の None が存在することがよくあります。レベル順走査の列にはこれらの None が含まれないため、その列だけから None の数や分布位置を推定することはできません。つまり、このレベル順走査列に一致する二分木構造は複数存在し得ます。

次の図のように、非充足二分木が与えられると、上記の配列表現はすでに成り立ちません。

図 7-13   レベル順走査列に対応する複数の二分木の可能性

この問題を解決するために、**レベル順走査列にすべての None を明示的に書き込む**ことを考えられます。次の図のように、このように処理すればレベル順走査列で二分木を一意に表現できます。コード例は以下のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# 二分木の配列表現\n# 空き位置を表すために None を使う\ntree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]\n
/* 二分木の配列表現 */\n// int の最大値 INT_MAX を使って空き位置を示す\nvector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
/* 二分木の配列表現 */\n// int のラッパークラス Integer を使えば、null で空き位置を示せる\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
/* 二分木の配列表現 */\n// nullable な int? 型を使えば、null で空き位置を示せる\nint?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二分木の配列表現 */\n// any 型のスライスを使えば、nil で空き位置を示せる\ntree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}\n
/* 二分木の配列表現 */\n// nullable な Int? 型を使えば、nil で空き位置を示せる\nlet tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n
/* 二分木の配列表現 */\n// null を使って空き位置を表す\nlet tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二分木の配列表現 */\n// null を使って空き位置を表す\nlet tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二分木の配列表現 */\n// nullable な int? 型を使えば、null で空き位置を示せる\nList<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二分木の配列表現 */\n// None を使って空き位置を示す\nlet tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];\n
/* 二分木の配列表現 */\n// int の最大値で空き位置を示すため、ノード値は INT_MAX であってはならない\nint tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
/* 二分木の配列表現 */\n// null を使って空き位置を表す\nval tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )\n
### 二分木の配列表現 ###\n# nil を使って空き位置を表す\ntree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n

図 7-14   任意の二分木の配列表現

補足すると、完全二分木は配列による表現に非常に適しています。完全二分木の定義を振り返ると、None は最下層の右側にしか現れないため、すべての None は必ずレベル順走査列の末尾に現れます。

つまり、完全二分木を配列で表す場合は、すべての None の格納を省略できるため、非常に便利です。次の図に例を示します。

図 7-15   完全二分木の配列表現

以下のコードでは、配列ベースで表現した二分木を実装しており、次の操作を含みます。

  • あるノードが与えられたとき、その値、左(右)子ノード、親ノードを取得する。
  • 前順走査、中順走査、後順走査、レベル順走査の列を取得する。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_binary_tree.py
class ArrayBinaryTree:\n    \"\"\"配列表現による二分木クラス\"\"\"\n\n    def __init__(self, arr: list[int | None]):\n        \"\"\"コンストラクタ\"\"\"\n        self._tree = list(arr)\n\n    def size(self):\n        \"\"\"リスト容量\"\"\"\n        return len(self._tree)\n\n    def val(self, i: int) -> int | None:\n        \"\"\"インデックス i のノードの値を取得\"\"\"\n        # インデックスが範囲外なら、空きを表す None を返す\n        if i < 0 or i >= self.size():\n            return None\n        return self._tree[i]\n\n    def left(self, i: int) -> int | None:\n        \"\"\"インデックス i のノードの左子ノードのインデックスを取得\"\"\"\n        return 2 * i + 1\n\n    def right(self, i: int) -> int | None:\n        \"\"\"インデックス i のノードの右子ノードのインデックスを取得\"\"\"\n        return 2 * i + 2\n\n    def parent(self, i: int) -> int | None:\n        \"\"\"インデックス i のノードの親ノードのインデックスを取得\"\"\"\n        return (i - 1) // 2\n\n    def level_order(self) -> list[int]:\n        \"\"\"レベル順走査\"\"\"\n        self.res = []\n        # 配列を直接走査する\n        for i in range(self.size()):\n            if self.val(i) is not None:\n                self.res.append(self.val(i))\n        return self.res\n\n    def dfs(self, i: int, order: str):\n        \"\"\"深さ優先探索\"\"\"\n        if self.val(i) is None:\n            return\n        # 先行順走査\n        if order == \"pre\":\n            self.res.append(self.val(i))\n        self.dfs(self.left(i), order)\n        # 中順走査\n        if order == \"in\":\n            self.res.append(self.val(i))\n        self.dfs(self.right(i), order)\n        # 後順走査\n        if order == \"post\":\n            self.res.append(self.val(i))\n\n    def pre_order(self) -> list[int]:\n        \"\"\"先行順走査\"\"\"\n        self.res = []\n        self.dfs(0, order=\"pre\")\n        return self.res\n\n    def in_order(self) -> list[int]:\n        \"\"\"中順走査\"\"\"\n        self.res = []\n        self.dfs(0, order=\"in\")\n        return self.res\n\n    def post_order(self) -> list[int]:\n        \"\"\"後順走査\"\"\"\n        self.res = []\n        self.dfs(0, order=\"post\")\n        return self.res\n
array_binary_tree.cpp
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n  public:\n    /* コンストラクタ */\n    ArrayBinaryTree(vector<int> arr) {\n        tree = arr;\n    }\n\n    /* リスト容量 */\n    int size() {\n        return tree.size();\n    }\n\n    /* インデックス i のノードの値を取得 */\n    int val(int i) {\n        // インデックスが範囲外なら、空きを表す INT_MAX を返す\n        if (i < 0 || i >= size())\n            return INT_MAX;\n        return tree[i];\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    int left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    int right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    int parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* レベル順走査 */\n    vector<int> levelOrder() {\n        vector<int> res;\n        // 配列を直接走査する\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != INT_MAX)\n                res.push_back(val(i));\n        }\n        return res;\n    }\n\n    /* 先行順走査 */\n    vector<int> preOrder() {\n        vector<int> res;\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* 中順走査 */\n    vector<int> inOrder() {\n        vector<int> res;\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* 後順走査 */\n    vector<int> postOrder() {\n        vector<int> res;\n        dfs(0, \"post\", res);\n        return res;\n    }\n\n  private:\n    vector<int> tree;\n\n    /* 深さ優先探索 */\n    void dfs(int i, string order, vector<int> &res) {\n        // 空きスロットなら返す\n        if (val(i) == INT_MAX)\n            return;\n        // 先行順走査\n        if (order == \"pre\")\n            res.push_back(val(i));\n        dfs(left(i), order, res);\n        // 中順走査\n        if (order == \"in\")\n            res.push_back(val(i));\n        dfs(right(i), order, res);\n        // 後順走査\n        if (order == \"post\")\n            res.push_back(val(i));\n    }\n};\n
array_binary_tree.java
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n    private List<Integer> tree;\n\n    /* コンストラクタ */\n    public ArrayBinaryTree(List<Integer> arr) {\n        tree = new ArrayList<>(arr);\n    }\n\n    /* リスト容量 */\n    public int size() {\n        return tree.size();\n    }\n\n    /* インデックス i のノードの値を取得 */\n    public Integer val(int i) {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if (i < 0 || i >= size())\n            return null;\n        return tree.get(i);\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    public Integer left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    public Integer right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    public Integer parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* レベル順走査 */\n    public List<Integer> levelOrder() {\n        List<Integer> res = new ArrayList<>();\n        // 配列を直接走査する\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != null)\n                res.add(val(i));\n        }\n        return res;\n    }\n\n    /* 深さ優先探索 */\n    private void dfs(Integer i, String order, List<Integer> res) {\n        // 空きスロットなら返す\n        if (val(i) == null)\n            return;\n        // 先行順走査\n        if (\"pre\".equals(order))\n            res.add(val(i));\n        dfs(left(i), order, res);\n        // 中順走査\n        if (\"in\".equals(order))\n            res.add(val(i));\n        dfs(right(i), order, res);\n        // 後順走査\n        if (\"post\".equals(order))\n            res.add(val(i));\n    }\n\n    /* 先行順走査 */\n    public List<Integer> preOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* 中順走査 */\n    public List<Integer> inOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* 後順走査 */\n    public List<Integer> postOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"post\", res);\n        return res;\n    }\n}\n
array_binary_tree.cs
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree(List<int?> arr) {\n    List<int?> tree = new(arr);\n\n    /* リスト容量 */\n    public int Size() {\n        return tree.Count;\n    }\n\n    /* インデックス i のノードの値を取得 */\n    public int? Val(int i) {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if (i < 0 || i >= Size())\n            return null;\n        return tree[i];\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    public int Left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    public int Right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    public int Parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* レベル順走査 */\n    public List<int> LevelOrder() {\n        List<int> res = [];\n        // 配列を直接走査する\n        for (int i = 0; i < Size(); i++) {\n            if (Val(i).HasValue)\n                res.Add(Val(i)!.Value);\n        }\n        return res;\n    }\n\n    /* 深さ優先探索 */\n    void DFS(int i, string order, List<int> res) {\n        // 空きスロットなら返す\n        if (!Val(i).HasValue)\n            return;\n        // 先行順走査\n        if (order == \"pre\")\n            res.Add(Val(i)!.Value);\n        DFS(Left(i), order, res);\n        // 中順走査\n        if (order == \"in\")\n            res.Add(Val(i)!.Value);\n        DFS(Right(i), order, res);\n        // 後順走査\n        if (order == \"post\")\n            res.Add(Val(i)!.Value);\n    }\n\n    /* 先行順走査 */\n    public List<int> PreOrder() {\n        List<int> res = [];\n        DFS(0, \"pre\", res);\n        return res;\n    }\n\n    /* 中順走査 */\n    public List<int> InOrder() {\n        List<int> res = [];\n        DFS(0, \"in\", res);\n        return res;\n    }\n\n    /* 後順走査 */\n    public List<int> PostOrder() {\n        List<int> res = [];\n        DFS(0, \"post\", res);\n        return res;\n    }\n}\n
array_binary_tree.go
/* 配列表現による二分木クラス */\ntype arrayBinaryTree struct {\n    tree []any\n}\n\n/* コンストラクタ */\nfunc newArrayBinaryTree(arr []any) *arrayBinaryTree {\n    return &arrayBinaryTree{\n        tree: arr,\n    }\n}\n\n/* リスト容量 */\nfunc (abt *arrayBinaryTree) size() int {\n    return len(abt.tree)\n}\n\n/* インデックス i のノードの値を取得 */\nfunc (abt *arrayBinaryTree) val(i int) any {\n    // インデックスが範囲外なら、空きを表す null を返す\n    if i < 0 || i >= abt.size() {\n        return nil\n    }\n    return abt.tree[i]\n}\n\n/* インデックス i のノードの左子ノードのインデックスを取得 */\nfunc (abt *arrayBinaryTree) left(i int) int {\n    return 2*i + 1\n}\n\n/* インデックス i のノードの右子ノードのインデックスを取得 */\nfunc (abt *arrayBinaryTree) right(i int) int {\n    return 2*i + 2\n}\n\n/* インデックス i のノードの親ノードのインデックスを取得 */\nfunc (abt *arrayBinaryTree) parent(i int) int {\n    return (i - 1) / 2\n}\n\n/* レベル順走査 */\nfunc (abt *arrayBinaryTree) levelOrder() []any {\n    var res []any\n    // 配列を直接走査する\n    for i := 0; i < abt.size(); i++ {\n        if abt.val(i) != nil {\n            res = append(res, abt.val(i))\n        }\n    }\n    return res\n}\n\n/* 深さ優先探索 */\nfunc (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) {\n    // 空きスロットなら返す\n    if abt.val(i) == nil {\n        return\n    }\n    // 先行順走査\n    if order == \"pre\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.left(i), order, res)\n    // 中順走査\n    if order == \"in\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.right(i), order, res)\n    // 後順走査\n    if order == \"post\" {\n        *res = append(*res, abt.val(i))\n    }\n}\n\n/* 先行順走査 */\nfunc (abt *arrayBinaryTree) preOrder() []any {\n    var res []any\n    abt.dfs(0, \"pre\", &res)\n    return res\n}\n\n/* 中順走査 */\nfunc (abt *arrayBinaryTree) inOrder() []any {\n    var res []any\n    abt.dfs(0, \"in\", &res)\n    return res\n}\n\n/* 後順走査 */\nfunc (abt *arrayBinaryTree) postOrder() []any {\n    var res []any\n    abt.dfs(0, \"post\", &res)\n    return res\n}\n
array_binary_tree.swift
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n    private var tree: [Int?]\n\n    /* コンストラクタ */\n    init(arr: [Int?]) {\n        tree = arr\n    }\n\n    /* リスト容量 */\n    func size() -> Int {\n        tree.count\n    }\n\n    /* インデックス i のノードの値を取得 */\n    func val(i: Int) -> Int? {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if i < 0 || i >= size() {\n            return nil\n        }\n        return tree[i]\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    func left(i: Int) -> Int {\n        2 * i + 1\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    func right(i: Int) -> Int {\n        2 * i + 2\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    func parent(i: Int) -> Int {\n        (i - 1) / 2\n    }\n\n    /* レベル順走査 */\n    func levelOrder() -> [Int] {\n        var res: [Int] = []\n        // 配列を直接走査する\n        for i in 0 ..< size() {\n            if let val = val(i: i) {\n                res.append(val)\n            }\n        }\n        return res\n    }\n\n    /* 深さ優先探索 */\n    private func dfs(i: Int, order: String, res: inout [Int]) {\n        // 空きスロットなら返す\n        guard let val = val(i: i) else {\n            return\n        }\n        // 先行順走査\n        if order == \"pre\" {\n            res.append(val)\n        }\n        dfs(i: left(i: i), order: order, res: &res)\n        // 中順走査\n        if order == \"in\" {\n            res.append(val)\n        }\n        dfs(i: right(i: i), order: order, res: &res)\n        // 後順走査\n        if order == \"post\" {\n            res.append(val)\n        }\n    }\n\n    /* 先行順走査 */\n    func preOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"pre\", res: &res)\n        return res\n    }\n\n    /* 中順走査 */\n    func inOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"in\", res: &res)\n        return res\n    }\n\n    /* 後順走査 */\n    func postOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"post\", res: &res)\n        return res\n    }\n}\n
array_binary_tree.js
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n    #tree;\n\n    /* コンストラクタ */\n    constructor(arr) {\n        this.#tree = arr;\n    }\n\n    /* リスト容量 */\n    size() {\n        return this.#tree.length;\n    }\n\n    /* インデックス i のノードの値を取得 */\n    val(i) {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    left(i) {\n        return 2 * i + 1;\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    right(i) {\n        return 2 * i + 2;\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    parent(i) {\n        return Math.floor((i - 1) / 2); // 切り捨て除算\n    }\n\n    /* レベル順走査 */\n    levelOrder() {\n        let res = [];\n        // 配列を直接走査する\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* 深さ優先探索 */\n    #dfs(i, order, res) {\n        // 空きスロットなら返す\n        if (this.val(i) === null) return;\n        // 先行順走査\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // 中順走査\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // 後順走査\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* 先行順走査 */\n    preOrder() {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* 中順走査 */\n    inOrder() {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* 後順走査 */\n    postOrder() {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
array_binary_tree.ts
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n    #tree: (number | null)[];\n\n    /* コンストラクタ */\n    constructor(arr: (number | null)[]) {\n        this.#tree = arr;\n    }\n\n    /* リスト容量 */\n    size(): number {\n        return this.#tree.length;\n    }\n\n    /* インデックス i のノードの値を取得 */\n    val(i: number): number | null {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    left(i: number): number {\n        return 2 * i + 1;\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    right(i: number): number {\n        return 2 * i + 2;\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    parent(i: number): number {\n        return Math.floor((i - 1) / 2); // 切り捨て除算\n    }\n\n    /* レベル順走査 */\n    levelOrder(): number[] {\n        let res = [];\n        // 配列を直接走査する\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* 深さ優先探索 */\n    #dfs(i: number, order: Order, res: (number | null)[]): void {\n        // 空きスロットなら返す\n        if (this.val(i) === null) return;\n        // 先行順走査\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // 中順走査\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // 後順走査\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* 先行順走査 */\n    preOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* 中順走査 */\n    inOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* 後順走査 */\n    postOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
array_binary_tree.dart
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree {\n  late List<int?> _tree;\n\n  /* コンストラクタ */\n  ArrayBinaryTree(this._tree);\n\n  /* リスト容量 */\n  int size() {\n    return _tree.length;\n  }\n\n  /* インデックス i のノードの値を取得 */\n  int? val(int i) {\n    // インデックスが範囲外なら、空きを表す null を返す\n    if (i < 0 || i >= size()) {\n      return null;\n    }\n    return _tree[i];\n  }\n\n  /* インデックス i のノードの左子ノードのインデックスを取得 */\n  int? left(int i) {\n    return 2 * i + 1;\n  }\n\n  /* インデックス i のノードの右子ノードのインデックスを取得 */\n  int? right(int i) {\n    return 2 * i + 2;\n  }\n\n  /* インデックス i のノードの親ノードのインデックスを取得 */\n  int? parent(int i) {\n    return (i - 1) ~/ 2;\n  }\n\n  /* レベル順走査 */\n  List<int> levelOrder() {\n    List<int> res = [];\n    for (int i = 0; i < size(); i++) {\n      if (val(i) != null) {\n        res.add(val(i)!);\n      }\n    }\n    return res;\n  }\n\n  /* 深さ優先探索 */\n  void dfs(int i, String order, List<int?> res) {\n    // 空きスロットなら返す\n    if (val(i) == null) {\n      return;\n    }\n    // 先行順走査\n    if (order == 'pre') {\n      res.add(val(i));\n    }\n    dfs(left(i)!, order, res);\n    // 中順走査\n    if (order == 'in') {\n      res.add(val(i));\n    }\n    dfs(right(i)!, order, res);\n    // 後順走査\n    if (order == 'post') {\n      res.add(val(i));\n    }\n  }\n\n  /* 先行順走査 */\n  List<int?> preOrder() {\n    List<int?> res = [];\n    dfs(0, 'pre', res);\n    return res;\n  }\n\n  /* 中順走査 */\n  List<int?> inOrder() {\n    List<int?> res = [];\n    dfs(0, 'in', res);\n    return res;\n  }\n\n  /* 後順走査 */\n  List<int?> postOrder() {\n    List<int?> res = [];\n    dfs(0, 'post', res);\n    return res;\n  }\n}\n
array_binary_tree.rs
/* 配列表現による二分木クラス */\nstruct ArrayBinaryTree {\n    tree: Vec<Option<i32>>,\n}\n\nimpl ArrayBinaryTree {\n    /* コンストラクタ */\n    fn new(arr: Vec<Option<i32>>) -> Self {\n        Self { tree: arr }\n    }\n\n    /* リスト容量 */\n    fn size(&self) -> i32 {\n        self.tree.len() as i32\n    }\n\n    /* インデックス i のノードの値を取得 */\n    fn val(&self, i: i32) -> Option<i32> {\n        // インデックスが範囲外なら、空きを表す None を返す\n        if i < 0 || i >= self.size() {\n            None\n        } else {\n            self.tree[i as usize]\n        }\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    fn left(&self, i: i32) -> i32 {\n        2 * i + 1\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    fn right(&self, i: i32) -> i32 {\n        2 * i + 2\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    fn parent(&self, i: i32) -> i32 {\n        (i - 1) / 2\n    }\n\n    /* レベル順走査 */\n    fn level_order(&self) -> Vec<i32> {\n        self.tree.iter().filter_map(|&x| x).collect()\n    }\n\n    /* 深さ優先探索 */\n    fn dfs(&self, i: i32, order: &'static str, res: &mut Vec<i32>) {\n        if self.val(i).is_none() {\n            return;\n        }\n        let val = self.val(i).unwrap();\n        // 先行順走査\n        if order == \"pre\" {\n            res.push(val);\n        }\n        self.dfs(self.left(i), order, res);\n        // 中順走査\n        if order == \"in\" {\n            res.push(val);\n        }\n        self.dfs(self.right(i), order, res);\n        // 後順走査\n        if order == \"post\" {\n            res.push(val);\n        }\n    }\n\n    /* 先行順走査 */\n    fn pre_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"pre\", &mut res);\n        res\n    }\n\n    /* 中順走査 */\n    fn in_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"in\", &mut res);\n        res\n    }\n\n    /* 後順走査 */\n    fn post_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"post\", &mut res);\n        res\n    }\n}\n
array_binary_tree.c
/* 配列表現による二分木の構造体 */\ntypedef struct {\n    int *tree;\n    int size;\n} ArrayBinaryTree;\n\n/* コンストラクタ */\nArrayBinaryTree *newArrayBinaryTree(int *arr, int arrSize) {\n    ArrayBinaryTree *abt = (ArrayBinaryTree *)malloc(sizeof(ArrayBinaryTree));\n    abt->tree = malloc(sizeof(int) * arrSize);\n    memcpy(abt->tree, arr, sizeof(int) * arrSize);\n    abt->size = arrSize;\n    return abt;\n}\n\n/* デストラクタ */\nvoid delArrayBinaryTree(ArrayBinaryTree *abt) {\n    free(abt->tree);\n    free(abt);\n}\n\n/* リスト容量 */\nint size(ArrayBinaryTree *abt) {\n    return abt->size;\n}\n\n/* インデックス i のノードの値を取得 */\nint val(ArrayBinaryTree *abt, int i) {\n    // インデックスが範囲外なら、空きを表す INT_MAX を返す\n    if (i < 0 || i >= size(abt))\n        return INT_MAX;\n    return abt->tree[i];\n}\n\n/* レベル順走査 */\nint *levelOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    // 配列を直接走査する\n    for (int i = 0; i < size(abt); i++) {\n        if (val(abt, i) != INT_MAX)\n            res[index++] = val(abt, i);\n    }\n    *returnSize = index;\n    return res;\n}\n\n/* 深さ優先探索 */\nvoid dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) {\n    // 空きスロットなら返す\n    if (val(abt, i) == INT_MAX)\n        return;\n    // 先行順走査\n    if (strcmp(order, \"pre\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, left(i), order, res, index);\n    // 中順走査\n    if (strcmp(order, \"in\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, right(i), order, res, index);\n    // 後順走査\n    if (strcmp(order, \"post\") == 0)\n        res[(*index)++] = val(abt, i);\n}\n\n/* 先行順走査 */\nint *preOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"pre\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* 中順走査 */\nint *inOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"in\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* 後順走査 */\nint *postOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"post\", res, &index);\n    *returnSize = index;\n    return res;\n}\n
array_binary_tree.kt
/* 配列表現による二分木クラス */\nclass ArrayBinaryTree(val tree: MutableList<Int?>) {\n    /* リスト容量 */\n    fun size(): Int {\n        return tree.size\n    }\n\n    /* インデックス i のノードの値を取得 */\n    fun _val(i: Int): Int? {\n        // インデックスが範囲外なら、空きを表す null を返す\n        if (i < 0 || i >= size()) return null\n        return tree[i]\n    }\n\n    /* インデックス i のノードの左子ノードのインデックスを取得 */\n    fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* インデックス i のノードの右子ノードのインデックスを取得 */\n    fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* インデックス i のノードの親ノードのインデックスを取得 */\n    fun parent(i: Int): Int {\n        return (i - 1) / 2\n    }\n\n    /* レベル順走査 */\n    fun levelOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        // 配列を直接走査する\n        for (i in 0..<size()) {\n            if (_val(i) != null)\n                res.add(_val(i))\n        }\n        return res\n    }\n\n    /* 深さ優先探索 */\n    fun dfs(i: Int, order: String, res: MutableList<Int?>) {\n        // 空きスロットなら返す\n        if (_val(i) == null)\n            return\n        // 先行順走査\n        if (\"pre\" == order)\n            res.add(_val(i))\n        dfs(left(i), order, res)\n        // 中順走査\n        if (\"in\" == order)\n            res.add(_val(i))\n        dfs(right(i), order, res)\n        // 後順走査\n        if (\"post\" == order)\n            res.add(_val(i))\n    }\n\n    /* 先行順走査 */\n    fun preOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"pre\", res)\n        return res\n    }\n\n    /* 中順走査 */\n    fun inOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"in\", res)\n        return res\n    }\n\n    /* 後順走査 */\n    fun postOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"post\", res)\n        return res\n    }\n}\n
array_binary_tree.rb
### 配列表現の二分木クラス ###\nclass ArrayBinaryTree\n  ### コンストラクタ ###\n  def initialize(arr)\n    @tree = arr.to_a\n  end\n\n  ### リスト容量 ###\n  def size\n    @tree.length\n  end\n\n  ### インデックス i のノードの値を取得 ###\n  def val(i)\n    # インデックスが範囲外なら `nil` を返し、空きスロットを表す\n    return if i < 0 || i >= size\n\n    @tree[i]\n  end\n\n  ### インデックス i のノードの左子ノードのインデックスを取得 ###\n  def left(i)\n    2 * i + 1\n  end\n\n  ### インデックス i のノードの右子ノードのインデックスを取得 ###\n  def right(i)\n    2 * i + 2\n  end\n\n  ### インデックス i のノードの親ノードのインデックスを取得 ###\n  def parent(i)\n    (i - 1) / 2\n  end\n\n  ### レベル順走査 ###\n  def level_order\n    @res = []\n\n    # 配列を直接走査する\n    for i in 0...size\n      @res << val(i) unless val(i).nil?\n    end\n\n    @res\n  end\n\n  ### 深さ優先探索 ###\n  def dfs(i, order)\n    return if val(i).nil?\n    # 先行順走査\n    @res << val(i) if order == :pre\n    dfs(left(i), order)\n    # 中順走査\n    @res << val(i) if order == :in\n    dfs(right(i), order)\n    # 後順走査\n    @res << val(i) if order == :post\n  end\n\n  ### 前順走査 ###\n  def pre_order\n    @res = []\n    dfs(0, :pre)\n    @res\n  end\n\n  ### 中順走査 ###\n  def in_order\n    @res = []\n    dfs(0, :in)\n    @res\n  end\n\n  ### 後順走査 ###\n  def post_order\n    @res = []\n    dfs(0, :post)\n    @res\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 7 章   木","7.3   二分木の配列表現"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#733","level":2,"title":"7.3.3   利点と制約","text":"

二分木の配列表現には主に次の利点があります。

  • 配列は連続したメモリ空間に格納されるため、キャッシュ効率が高く、アクセスと走査が速い。
  • ポインタを格納する必要がなく、比較的省スペースである。
  • ノードへのランダムアクセスが可能である。

ただし、配列表現にはいくつかの制約もあります。

  • 配列による格納には連続したメモリ空間が必要なため、データ量が大きすぎる木の格納には向かない。
  • ノードの追加と削除は配列の挿入・削除操作で実現する必要があり、効率は低い。
  • 二分木に大量の None が存在すると、配列に占める実ノードデータの比率が低くなり、空間利用率も低下する。
","path":["第 7 章   木","7.3   二分木の配列表現"],"tags":[]},{"location":"chapter_tree/avl_tree/","level":1,"title":"7.5   AVL 木 *","text":"

「二分探索木」章で述べたように、挿入と削除を何度も繰り返すと、二分探索木は連結リストへ退化する可能性があります。この場合、すべての操作の時間計算量は \\(O(\\log n)\\) から \\(O(n)\\) へ劣化します。

以下の図に示すように、ノード削除を 2 回行うと、この二分探索木は連結リストへ退化します。

図 7-24   AVL 木がノード削除後に退化する

別の例として、以下の図に示す完全二分木に 2 つのノードを挿入すると、木は大きく左に傾き、探索操作の時間計算量もそれに伴って劣化します。

図 7-25   AVL 木がノード挿入後に退化する

1962 年、G. M. Adelson-Velsky と E. M. Landis は論文“An algorithm for the organization of information”の中で AVL 木 を提案しました。論文では一連の操作が詳しく説明されており、ノードの追加と削除を続けても AVL 木が退化しないようにして、各種操作の時間計算量を \\(O(\\log n)\\) の水準に保ちます。言い換えると、追加・削除・探索・更新を頻繁に行う場面でも、AVL 木は常に高いデータ操作性能を維持でき、実用価値の高い構造です。

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#751-avl","level":2,"title":"7.5.1   AVL 木の基本用語","text":"

AVL 木は二分探索木であると同時に平衡二分木でもあり、これら 2 種類の二分木の性質をすべて満たします。したがって、平衡二分探索木(balanced binary search tree)の一種です。

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1","level":3,"title":"1.   ノードの高さ","text":"

AVL 木の操作ではノードの高さを取得する必要があるため、ノードクラスに height 変数を追加します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class TreeNode:\n    \"\"\"AVL 木ノードクラス\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                 # ノード値\n        self.height: int = 0                # ノードの高さ\n        self.left: TreeNode | None = None   # 左の子ノード参照\n        self.right: TreeNode | None = None  # 右の子ノード参照\n
/* AVL 木ノードクラス */\nstruct TreeNode {\n    int val{};          // ノード値\n    int height = 0;     // ノードの高さ\n    TreeNode *left{};   // 左の子ノード\n    TreeNode *right{};  // 右の子ノード\n    TreeNode() = default;\n    explicit TreeNode(int x) : val(x){}\n};\n
/* AVL 木ノードクラス */\nclass TreeNode {\n    public int val;        // ノード値\n    public int height;     // ノードの高さ\n    public TreeNode left;  // 左の子ノード\n    public TreeNode right; // 右の子ノード\n    public TreeNode(int x) { val = x; }\n}\n
/* AVL 木ノードクラス */\nclass TreeNode(int? x) {\n    public int? val = x;    // ノード値\n    public int height;      // ノードの高さ\n    public TreeNode? left;  // 左の子ノード参照\n    public TreeNode? right; // 右の子ノード参照\n}\n
/* AVL 木ノード構造体 */\ntype TreeNode struct {\n    Val    int       // ノード値\n    Height int       // ノードの高さ\n    Left   *TreeNode // 左の子ノード参照\n    Right  *TreeNode // 右の子ノード参照\n}\n
/* AVL 木ノードクラス */\nclass TreeNode {\n    var val: Int // ノード値\n    var height: Int // ノードの高さ\n    var left: TreeNode? // 左の子ノード\n    var right: TreeNode? // 右の子ノード\n\n    init(x: Int) {\n        val = x\n        height = 0\n    }\n}\n
/* AVL 木ノードクラス */\nclass TreeNode {\n    val; // ノード値\n    height; //ノードの高さ\n    left; // 左の子ノードポインタ\n    right; // 右の子ノードポインタ\n    constructor(val, left, right, height) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* AVL 木ノードクラス */\nclass TreeNode {\n    val: number;            // ノード値\n    height: number;         // ノードの高さ\n    left: TreeNode | null;  // 左の子ノードポインタ\n    right: TreeNode | null; // 右の子ノードポインタ\n    constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* AVL 木ノードクラス */\nclass TreeNode {\n  int val;         // ノード値\n  int height;      // ノードの高さ\n  TreeNode? left;  // 左の子ノード\n  TreeNode? right; // 右の子ノード\n  TreeNode(this.val, [this.height = 0, this.left, this.right]);\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* AVL 木ノード構造体 */\nstruct TreeNode {\n    val: i32,                               // ノード値\n    height: i32,                            // ノードの高さ\n    left: Option<Rc<RefCell<TreeNode>>>,    // 左の子ノード\n    right: Option<Rc<RefCell<TreeNode>>>,   // 右の子ノード\n}\n\nimpl TreeNode {\n    /* コンストラクタ */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            height: 0,\n            left: None,\n            right: None\n        }))\n    }\n}\n
/* AVL 木ノード構造体 */\ntypedef struct TreeNode {\n    int val;\n    int height;\n    struct TreeNode *left;\n    struct TreeNode *right;\n} TreeNode;\n\n/* コンストラクタ */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
/* AVL 木ノードクラス */\nclass TreeNode(val _val: Int) {  // ノード値\n    val height: Int = 0          // ノードの高さ\n    val left: TreeNode? = null   // 左の子ノード\n    val right: TreeNode? = null  // 右の子ノード\n}\n
### AVL 木ノードクラス ###\nclass TreeNode\n  attr_accessor :val    # ノード値\n  attr_accessor :height # ノードの高さ\n  attr_accessor :left   # 左の子ノード参照\n  attr_accessor :right  # 右の子ノード参照\n\n  def initialize(val)\n    @val = val\n    @height = 0\n  end\nend\n

「ノードの高さ」とは、そのノードから最も遠い葉ノードまでの距離、すなわち通過する「辺」の本数を指します。特に、葉ノードの高さは \\(0\\)、空ノードの高さは \\(-1\\) です。ここでは、ノードの高さを取得・更新するための 2 つの補助関数を用意します:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def height(self, node: TreeNode | None) -> int:\n    \"\"\"ノードの高さを取得\"\"\"\n    # 空ノードの高さは -1、葉ノードの高さは 0\n    if node is not None:\n        return node.height\n    return -1\n\ndef update_height(self, node: TreeNode | None):\n    \"\"\"ノードの高さを更新する\"\"\"\n    # ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node.height = max([self.height(node.left), self.height(node.right)]) + 1\n
avl_tree.cpp
/* ノードの高さを取得 */\nint height(TreeNode *node) {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node == nullptr ? -1 : node->height;\n}\n\n/* ノードの高さを更新する */\nvoid updateHeight(TreeNode *node) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node->height = max(height(node->left), height(node->right)) + 1;\n}\n
avl_tree.java
/* ノードの高さを取得 */\nint height(TreeNode node) {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node == null ? -1 : node.height;\n}\n\n/* ノードの高さを更新する */\nvoid updateHeight(TreeNode node) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node.height = Math.max(height(node.left), height(node.right)) + 1;\n}\n
avl_tree.cs
/* ノードの高さを取得 */\nint Height(TreeNode? node) {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node == null ? -1 : node.height;\n}\n\n/* ノードの高さを更新する */\nvoid UpdateHeight(TreeNode node) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node.height = Math.Max(Height(node.left), Height(node.right)) + 1;\n}\n
avl_tree.go
/* ノードの高さを取得 */\nfunc (t *aVLTree) height(node *TreeNode) int {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    if node != nil {\n        return node.Height\n    }\n    return -1\n}\n\n/* ノードの高さを更新する */\nfunc (t *aVLTree) updateHeight(node *TreeNode) {\n    lh := t.height(node.Left)\n    rh := t.height(node.Right)\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    if lh > rh {\n        node.Height = lh + 1\n    } else {\n        node.Height = rh + 1\n    }\n}\n
avl_tree.swift
/* ノードの高さを取得 */\nfunc height(node: TreeNode?) -> Int {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    node?.height ?? -1\n}\n\n/* ノードの高さを更新する */\nfunc updateHeight(node: TreeNode?) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node?.height = max(height(node: node?.left), height(node: node?.right)) + 1\n}\n
avl_tree.js
/* ノードの高さを取得 */\nheight(node) {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node === null ? -1 : node.height;\n}\n\n/* ノードの高さを更新する */\n#updateHeight(node) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
avl_tree.ts
/* ノードの高さを取得 */\nheight(node: TreeNode): number {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node === null ? -1 : node.height;\n}\n\n/* ノードの高さを更新する */\nupdateHeight(node: TreeNode): void {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
avl_tree.dart
/* ノードの高さを取得 */\nint height(TreeNode? node) {\n  // 空ノードの高さは -1、葉ノードの高さは 0\n  return node == null ? -1 : node.height;\n}\n\n/* ノードの高さを更新する */\nvoid updateHeight(TreeNode? node) {\n  // ノードの高さは最も高い部分木の高さ + 1 に等しい\n  node!.height = max(height(node.left), height(node.right)) + 1;\n}\n
avl_tree.rs
/* ノードの高さを取得 */\nfn height(node: OptionTreeNodeRc) -> i32 {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    match node {\n        Some(node) => node.borrow().height,\n        None => -1,\n    }\n}\n\n/* ノードの高さを更新する */\nfn update_height(node: OptionTreeNodeRc) {\n    if let Some(node) = node {\n        let left = node.borrow().left.clone();\n        let right = node.borrow().right.clone();\n        // ノードの高さは最も高い部分木の高さ + 1 に等しい\n        node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1;\n    }\n}\n
avl_tree.c
/* ノードの高さを取得 */\nint height(TreeNode *node) {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    if (node != NULL) {\n        return node->height;\n    }\n    return -1;\n}\n\n/* ノードの高さを更新する */\nvoid updateHeight(TreeNode *node) {\n    int lh = height(node->left);\n    int rh = height(node->right);\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    if (lh > rh) {\n        node->height = lh + 1;\n    } else {\n        node->height = rh + 1;\n    }\n}\n
avl_tree.kt
/* ノードの高さを取得 */\nfun height(node: TreeNode?): Int {\n    // 空ノードの高さは -1、葉ノードの高さは 0\n    return node?.height ?: -1\n}\n\n/* ノードの高さを更新する */\nfun updateHeight(node: TreeNode?) {\n    // ノードの高さは最も高い部分木の高さ + 1 に等しい\n    node?.height = max(height(node?.left), height(node?.right)) + 1\n}\n
avl_tree.rb
### ノードの高さを取得 ###\ndef height(node)\n  # 空ノードの高さは -1、葉ノードの高さは 0\n  return node.height unless node.nil?\n\n  -1\nend\n\n### ノードの高さを更新 ###\ndef update_height(node)\n  # ノードの高さは最も高い部分木の高さ + 1 に等しい\n  node.height = [height(node.left), height(node.right)].max + 1\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2","level":3,"title":"2.   ノードの平衡係数","text":"

ノードの平衡係数(balance factor)は、左部分木の高さから右部分木の高さを引いた値と定義し、空ノードの平衡係数は \\(0\\) とします。同様に、ノードの平衡係数を取得する機能も関数にカプセル化して、後続で使いやすくします:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def balance_factor(self, node: TreeNode | None) -> int:\n    \"\"\"平衡係数を取得\"\"\"\n    # 空ノードの平衡係数は 0\n    if node is None:\n        return 0\n    # ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return self.height(node.left) - self.height(node.right)\n
avl_tree.cpp
/* 平衡係数を取得 */\nint balanceFactor(TreeNode *node) {\n    // 空ノードの平衡係数は 0\n    if (node == nullptr)\n        return 0;\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return height(node->left) - height(node->right);\n}\n
avl_tree.java
/* 平衡係数を取得 */\nint balanceFactor(TreeNode node) {\n    // 空ノードの平衡係数は 0\n    if (node == null)\n        return 0;\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return height(node.left) - height(node.right);\n}\n
avl_tree.cs
/* 平衡係数を取得 */\nint BalanceFactor(TreeNode? node) {\n    // 空ノードの平衡係数は 0\n    if (node == null) return 0;\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return Height(node.left) - Height(node.right);\n}\n
avl_tree.go
/* 平衡係数を取得 */\nfunc (t *aVLTree) balanceFactor(node *TreeNode) int {\n    // 空ノードの平衡係数は 0\n    if node == nil {\n        return 0\n    }\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return t.height(node.Left) - t.height(node.Right)\n}\n
avl_tree.swift
/* 平衡係数を取得 */\nfunc balanceFactor(node: TreeNode?) -> Int {\n    // 空ノードの平衡係数は 0\n    guard let node = node else { return 0 }\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return height(node: node.left) - height(node: node.right)\n}\n
avl_tree.js
/* 平衡係数を取得 */\nbalanceFactor(node) {\n    // 空ノードの平衡係数は 0\n    if (node === null) return 0;\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return this.height(node.left) - this.height(node.right);\n}\n
avl_tree.ts
/* 平衡係数を取得 */\nbalanceFactor(node: TreeNode): number {\n    // 空ノードの平衡係数は 0\n    if (node === null) return 0;\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return this.height(node.left) - this.height(node.right);\n}\n
avl_tree.dart
/* 平衡係数を取得 */\nint balanceFactor(TreeNode? node) {\n  // 空ノードの平衡係数は 0\n  if (node == null) return 0;\n  // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n  return height(node.left) - height(node.right);\n}\n
avl_tree.rs
/* 平衡係数を取得 */\nfn balance_factor(node: OptionTreeNodeRc) -> i32 {\n    match node {\n        // 空ノードの平衡係数は 0\n        None => 0,\n        // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n        Some(node) => {\n            Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone())\n        }\n    }\n}\n
avl_tree.c
/* 平衡係数を取得 */\nint balanceFactor(TreeNode *node) {\n    // 空ノードの平衡係数は 0\n    if (node == NULL) {\n        return 0;\n    }\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return height(node->left) - height(node->right);\n}\n
avl_tree.kt
/* 平衡係数を取得 */\nfun balanceFactor(node: TreeNode?): Int {\n    // 空ノードの平衡係数は 0\n    if (node == null) return 0\n    // ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n    return height(node.left) - height(node.right)\n}\n
avl_tree.rb
### 平衡係数を取得 ###\ndef balance_factor(node)\n  # 空ノードの平衡係数は 0\n  return 0 if node.nil?\n\n  # ノードの平衡係数 = 左部分木の高さ - 右部分木の高さ\n  height(node.left) - height(node.right)\nend\n

Tip

平衡係数を \\(f\\) とすると、AVL 木の任意のノードの平衡係数は常に \\(-1 \\le f \\le 1\\) を満たします。

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#752-avl","level":2,"title":"7.5.2   AVL 木の回転","text":"

AVL 木の特徴は「回転」操作にあり、二分木の中順走査列を変えずに、不平衡ノードを再び平衡に戻せます。言い換えると、回転操作は「二分探索木」の性質を保ちながら、木を再び「平衡二分木」に戻すことができます。

平衡係数の絶対値が \\(> 1\\) のノードを「不平衡ノード」と呼びます。ノードの不平衡の形に応じて、回転操作は 4 種類に分かれます。右回転、左回転、右回転してから左回転、左回転してから右回転です。以下でこれらを順に説明します。

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_1","level":3,"title":"1.   右回転","text":"

以下の図では、ノードの下に平衡係数を示しています。下から上へ見ると、二分木で最初に不平衡になるのは「ノード 3」です。この不平衡ノードを根とする部分木に注目し、そのノードを node、左の子ノードを child として、「右回転」を行います。右回転後、部分木は平衡を回復し、なおかつ二分探索木の性質も保たれます。

<1><2><3><4>

図 7-26   右回転の手順

以下の図に示すように、ノード child に右の子ノード(grand_child と記す)がある場合、右回転には 1 ステップ追加する必要があります。すなわち、grand_childnode の左の子ノードにします。

図 7-27   grand_child を持つ右回転

「右に回転する」というのはあくまでイメージしやすい表現であり、実際にはノードポインタを変更して実現します。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def right_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"右回転\"\"\"\n    child = node.left\n    grand_child = child.right\n    # child を支点として node を右回転させる\n    child.right = node\n    node.left = grand_child\n    # ノードの高さを更新する\n    self.update_height(node)\n    self.update_height(child)\n    # 回転後の部分木の根ノードを返す\n    return child\n
avl_tree.cpp
/* 右回転 */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child = node->left;\n    TreeNode *grandChild = child->right;\n    // child を支点として node を右回転させる\n    child->right = node;\n    node->left = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.java
/* 右回転 */\nTreeNode rightRotate(TreeNode node) {\n    TreeNode child = node.left;\n    TreeNode grandChild = child.right;\n    // child を支点として node を右回転させる\n    child.right = node;\n    node.left = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.cs
/* 右回転 */\nTreeNode? RightRotate(TreeNode? node) {\n    TreeNode? child = node?.left;\n    TreeNode? grandChild = child?.right;\n    // child を支点として node を右回転させる\n    child.right = node;\n    node.left = grandChild;\n    // ノードの高さを更新する\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.go
/* 右回転 */\nfunc (t *aVLTree) rightRotate(node *TreeNode) *TreeNode {\n    child := node.Left\n    grandChild := child.Right\n    // child を支点として node を右回転させる\n    child.Right = node\n    node.Left = grandChild\n    // ノードの高さを更新する\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.swift
/* 右回転 */\nfunc rightRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.left\n    let grandChild = child?.right\n    // child を支点として node を右回転させる\n    child?.right = node\n    node?.left = grandChild\n    // ノードの高さを更新する\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.js
/* 右回転 */\n#rightRotate(node) {\n    const child = node.left;\n    const grandChild = child.right;\n    // child を支点として node を右回転させる\n    child.right = node;\n    node.left = grandChild;\n    // ノードの高さを更新する\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.ts
/* 右回転 */\nrightRotate(node: TreeNode): TreeNode {\n    const child = node.left;\n    const grandChild = child.right;\n    // child を支点として node を右回転させる\n    child.right = node;\n    node.left = grandChild;\n    // ノードの高さを更新する\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.dart
/* 右回転 */\nTreeNode? rightRotate(TreeNode? node) {\n  TreeNode? child = node!.left;\n  TreeNode? grandChild = child!.right;\n  // child を支点として node を右回転させる\n  child.right = node;\n  node.left = grandChild;\n  // ノードの高さを更新する\n  updateHeight(node);\n  updateHeight(child);\n  // 回転後の部分木の根ノードを返す\n  return child;\n}\n
avl_tree.rs
/* 右回転 */\nfn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().left.clone().unwrap();\n            let grand_child = child.borrow().right.clone();\n            // child を支点として node を右回転させる\n            child.borrow_mut().right = Some(node.clone());\n            node.borrow_mut().left = grand_child;\n            // ノードの高さを更新する\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // 回転後の部分木の根ノードを返す\n            Some(child)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* 右回転 */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->left;\n    grandChild = child->right;\n    // child を支点として node を右回転させる\n    child->right = node;\n    node->left = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.kt
/* 右回転 */\nfun rightRotate(node: TreeNode?): TreeNode {\n    val child = node!!.left\n    val grandChild = child!!.right\n    // child を支点として node を右回転させる\n    child.right = node\n    node.left = grandChild\n    // ノードの高さを更新する\n    updateHeight(node)\n    updateHeight(child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.rb
### 右回転操作 ###\ndef right_rotate(node)\n  child = node.left\n  grand_child = child.right\n  # child を支点として node を右回転させる\n  child.right = node\n  node.left = grand_child\n  # ノードの高さを更新する\n  update_height(node)\n  update_height(child)\n  # 回転後の部分木の根ノードを返す\n  child\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_1","level":3,"title":"2.   左回転","text":"

対応する鏡像として、上記の不平衡二分木を左右反転して考えると、以下の図に示す「左回転」が必要になります。

図 7-28   左回転

同様に、以下の図に示すように、ノード child に左の子ノード(grand_child と記す)がある場合、左回転にも 1 ステップ追加する必要があります。すなわち、grand_childnode の右の子ノードにします。

図 7-29   grand_child を持つ左回転

分かるように、右回転と左回転は論理的に鏡像対称であり、それぞれが解決する 2 種類の不平衡も対称です。この対称性に基づけば、右回転の実装コードにあるすべての leftright に、すべての rightleft に置き換えるだけで、左回転の実装コードが得られます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def left_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"左回転\"\"\"\n    child = node.right\n    grand_child = child.left\n    # child を支点として node を左回転させる\n    child.left = node\n    node.right = grand_child\n    # ノードの高さを更新する\n    self.update_height(node)\n    self.update_height(child)\n    # 回転後の部分木の根ノードを返す\n    return child\n
avl_tree.cpp
/* 左回転 */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child = node->right;\n    TreeNode *grandChild = child->left;\n    // child を支点として node を左回転させる\n    child->left = node;\n    node->right = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.java
/* 左回転 */\nTreeNode leftRotate(TreeNode node) {\n    TreeNode child = node.right;\n    TreeNode grandChild = child.left;\n    // child を支点として node を左回転させる\n    child.left = node;\n    node.right = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.cs
/* 左回転 */\nTreeNode? LeftRotate(TreeNode? node) {\n    TreeNode? child = node?.right;\n    TreeNode? grandChild = child?.left;\n    // child を支点として node を左回転させる\n    child.left = node;\n    node.right = grandChild;\n    // ノードの高さを更新する\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.go
/* 左回転 */\nfunc (t *aVLTree) leftRotate(node *TreeNode) *TreeNode {\n    child := node.Right\n    grandChild := child.Left\n    // child を支点として node を左回転させる\n    child.Left = node\n    node.Right = grandChild\n    // ノードの高さを更新する\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.swift
/* 左回転 */\nfunc leftRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.right\n    let grandChild = child?.left\n    // child を支点として node を左回転させる\n    child?.left = node\n    node?.right = grandChild\n    // ノードの高さを更新する\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.js
/* 左回転 */\n#leftRotate(node) {\n    const child = node.right;\n    const grandChild = child.left;\n    // child を支点として node を左回転させる\n    child.left = node;\n    node.right = grandChild;\n    // ノードの高さを更新する\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.ts
/* 左回転 */\nleftRotate(node: TreeNode): TreeNode {\n    const child = node.right;\n    const grandChild = child.left;\n    // child を支点として node を左回転させる\n    child.left = node;\n    node.right = grandChild;\n    // ノードの高さを更新する\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.dart
/* 左回転 */\nTreeNode? leftRotate(TreeNode? node) {\n  TreeNode? child = node!.right;\n  TreeNode? grandChild = child!.left;\n  // child を支点として node を左回転させる\n  child.left = node;\n  node.right = grandChild;\n  // ノードの高さを更新する\n  updateHeight(node);\n  updateHeight(child);\n  // 回転後の部分木の根ノードを返す\n  return child;\n}\n
avl_tree.rs
/* 左回転 */\nfn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().right.clone().unwrap();\n            let grand_child = child.borrow().left.clone();\n            // child を支点として node を左回転させる\n            child.borrow_mut().left = Some(node.clone());\n            node.borrow_mut().right = grand_child;\n            // ノードの高さを更新する\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // 回転後の部分木の根ノードを返す\n            Some(child)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* 左回転 */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->right;\n    grandChild = child->left;\n    // child を支点として node を左回転させる\n    child->left = node;\n    node->right = grandChild;\n    // ノードの高さを更新する\n    updateHeight(node);\n    updateHeight(child);\n    // 回転後の部分木の根ノードを返す\n    return child;\n}\n
avl_tree.kt
/* 左回転 */\nfun leftRotate(node: TreeNode?): TreeNode {\n    val child = node!!.right\n    val grandChild = child!!.left\n    // child を支点として node を左回転させる\n    child.left = node\n    node.right = grandChild\n    // ノードの高さを更新する\n    updateHeight(node)\n    updateHeight(child)\n    // 回転後の部分木の根ノードを返す\n    return child\n}\n
avl_tree.rb
### 左回転操作 ###\ndef left_rotate(node)\n  child = node.right\n  grand_child = child.left\n  # child を支点として node を左回転させる\n  child.left = node\n  node.right = grand_child\n  # ノードの高さを更新する\n  update_height(node)\n  update_height(child)\n  # 回転後の部分木の根ノードを返す\n  child\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3","level":3,"title":"3.   左回転してから右回転","text":"

以下の図の不平衡ノード 3 では、左回転だけでも右回転だけでも部分木を平衡に戻せません。この場合は、まず child に「左回転」を行い、次に node に「右回転」を行います。

図 7-30   左回転してから右回転

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4","level":3,"title":"4.   右回転してから左回転","text":"

以下の図に示すように、上記の不平衡二分木の鏡像のケースでは、まず child に「右回転」を行い、次に node に「左回転」を行います。

図 7-31   右回転してから左回転

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5","level":3,"title":"5.   回転の選択","text":"

以下の図に示す 4 種類の不平衡は、上の各ケースにそれぞれ対応しており、必要な操作は順に右回転、左回転してから右回転、右回転してから左回転、左回転です。

図 7-32   AVL 木の 4 つの回転ケース

以下の表に示すように、不平衡ノードの平衡係数と、高い側の子ノードの平衡係数の符号を判定することで、その不平衡ノードが上図のどのケースに属するかを判断できます。

表 7-3   4 種類の回転ケースの選択条件

不平衡ノードの平衡係数 子ノードの平衡係数 採用すべき回転方法 \\(> 1\\) (左に偏った木) \\(\\geq 0\\) 右回転 \\(> 1\\) (左に偏った木) \\(<0\\) 左回転してから右回転 \\(< -1\\) (右に偏った木) \\(\\leq 0\\) 左回転 \\(< -1\\) (右に偏った木) \\(>0\\) 右回転してから左回転

使いやすくするために、回転操作を 1 つの関数にカプセル化します。この関数があれば、さまざまな不平衡ケースに対して回転を行い、不平衡ノードを再び平衡に戻せます。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"回転操作を行い、この部分木の平衡を回復する\"\"\"\n    # ノード node の平衡係数を取得\n    balance_factor = self.balance_factor(node)\n    # 左に偏った木\n    if balance_factor > 1:\n        if self.balance_factor(node.left) >= 0:\n            # 右回転\n            return self.right_rotate(node)\n        else:\n            # 左回転してから右回転\n            node.left = self.left_rotate(node.left)\n            return self.right_rotate(node)\n    # 右に偏った木\n    elif balance_factor < -1:\n        if self.balance_factor(node.right) <= 0:\n            # 左回転\n            return self.left_rotate(node)\n        else:\n            # 右回転してから左回転\n            node.right = self.right_rotate(node.right)\n            return self.left_rotate(node)\n    # 平衡木なので回転不要、そのまま返す\n    return node\n
avl_tree.cpp
/* 回転操作を行い、この部分木の平衡を回復する */\nTreeNode *rotate(TreeNode *node) {\n    // ノード node の平衡係数を取得\n    int _balanceFactor = balanceFactor(node);\n    // 左に偏った木\n    if (_balanceFactor > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // 右回転\n            return rightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (_balanceFactor < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // 左回転\n            return leftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.java
/* 回転操作を行い、この部分木の平衡を回復する */\nTreeNode rotate(TreeNode node) {\n    // ノード node の平衡係数を取得\n    int balanceFactor = balanceFactor(node);\n    // 左に偏った木\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // 右回転\n            return rightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node.left = leftRotate(node.left);\n            return rightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // 左回転\n            return leftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node.right = rightRotate(node.right);\n            return leftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.cs
/* 回転操作を行い、この部分木の平衡を回復する */\nTreeNode? Rotate(TreeNode? node) {\n    // ノード node の平衡係数を取得\n    int balanceFactorInt = BalanceFactor(node);\n    // 左に偏った木\n    if (balanceFactorInt > 1) {\n        if (BalanceFactor(node?.left) >= 0) {\n            // 右回転\n            return RightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node!.left = LeftRotate(node!.left);\n            return RightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (balanceFactorInt < -1) {\n        if (BalanceFactor(node?.right) <= 0) {\n            // 左回転\n            return LeftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node!.right = RightRotate(node!.right);\n            return LeftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.go
/* 回転操作を行い、この部分木の平衡を回復する */\nfunc (t *aVLTree) rotate(node *TreeNode) *TreeNode {\n    // ノード `node` の平衡係数を取得する\n    // Go では短い変数名が推奨されるため、ここで `bf` は `t.balanceFactor` を表す\n    bf := t.balanceFactor(node)\n    // 左に偏った木\n    if bf > 1 {\n        if t.balanceFactor(node.Left) >= 0 {\n            // 右回転\n            return t.rightRotate(node)\n        } else {\n            // 左回転してから右回転\n            node.Left = t.leftRotate(node.Left)\n            return t.rightRotate(node)\n        }\n    }\n    // 右に偏った木\n    if bf < -1 {\n        if t.balanceFactor(node.Right) <= 0 {\n            // 左回転\n            return t.leftRotate(node)\n        } else {\n            // 右回転してから左回転\n            node.Right = t.rightRotate(node.Right)\n            return t.leftRotate(node)\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node\n}\n
avl_tree.swift
/* 回転操作を行い、この部分木の平衡を回復する */\nfunc rotate(node: TreeNode?) -> TreeNode? {\n    // ノード node の平衡係数を取得\n    let balanceFactor = balanceFactor(node: node)\n    // 左に偏った木\n    if balanceFactor > 1 {\n        if self.balanceFactor(node: node?.left) >= 0 {\n            // 右回転\n            return rightRotate(node: node)\n        } else {\n            // 左回転してから右回転\n            node?.left = leftRotate(node: node?.left)\n            return rightRotate(node: node)\n        }\n    }\n    // 右に偏った木\n    if balanceFactor < -1 {\n        if self.balanceFactor(node: node?.right) <= 0 {\n            // 左回転\n            return leftRotate(node: node)\n        } else {\n            // 右回転してから左回転\n            node?.right = rightRotate(node: node?.right)\n            return leftRotate(node: node)\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node\n}\n
avl_tree.js
/* 回転操作を行い、この部分木の平衡を回復する */\n#rotate(node) {\n    // ノード node の平衡係数を取得\n    const balanceFactor = this.balanceFactor(node);\n    // 左に偏った木\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // 右回転\n            return this.#rightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node.left = this.#leftRotate(node.left);\n            return this.#rightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // 左回転\n            return this.#leftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node.right = this.#rightRotate(node.right);\n            return this.#leftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.ts
/* 回転操作を行い、この部分木の平衡を回復する */\nrotate(node: TreeNode): TreeNode {\n    // ノード node の平衡係数を取得\n    const balanceFactor = this.balanceFactor(node);\n    // 左に偏った木\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // 右回転\n            return this.rightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node.left = this.leftRotate(node.left);\n            return this.rightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // 左回転\n            return this.leftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node.right = this.rightRotate(node.right);\n            return this.leftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.dart
/* 回転操作を行い、この部分木の平衡を回復する */\nTreeNode? rotate(TreeNode? node) {\n  // ノード node の平衡係数を取得\n  int factor = balanceFactor(node);\n  // 左に偏った木\n  if (factor > 1) {\n    if (balanceFactor(node!.left) >= 0) {\n      // 右回転\n      return rightRotate(node);\n    } else {\n      // 左回転してから右回転\n      node.left = leftRotate(node.left);\n      return rightRotate(node);\n    }\n  }\n  // 右に偏った木\n  if (factor < -1) {\n    if (balanceFactor(node!.right) <= 0) {\n      // 左回転\n      return leftRotate(node);\n    } else {\n      // 右回転してから左回転\n      node.right = rightRotate(node.right);\n      return leftRotate(node);\n    }\n  }\n  // 平衡木なので回転不要、そのまま返す\n  return node;\n}\n
avl_tree.rs
/* 回転操作を行い、この部分木の平衡を回復する */\nfn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    // ノード node の平衡係数を取得\n    let balance_factor = Self::balance_factor(node.clone());\n    // 左に偏った木\n    if balance_factor > 1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().left.clone()) >= 0 {\n            // 右回転\n            Self::right_rotate(Some(node))\n        } else {\n            // 左回転してから右回転\n            let left = node.borrow().left.clone();\n            node.borrow_mut().left = Self::left_rotate(left);\n            Self::right_rotate(Some(node))\n        }\n    }\n    // 右に偏った木\n    else if balance_factor < -1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().right.clone()) <= 0 {\n            // 左回転\n            Self::left_rotate(Some(node))\n        } else {\n            // 右回転してから左回転\n            let right = node.borrow().right.clone();\n            node.borrow_mut().right = Self::right_rotate(right);\n            Self::left_rotate(Some(node))\n        }\n    } else {\n        // 平衡木なので回転不要、そのまま返す\n        node\n    }\n}\n
avl_tree.c
/* 回転操作を行い、この部分木の平衡を回復する */\nTreeNode *rotate(TreeNode *node) {\n    // ノード node の平衡係数を取得\n    int bf = balanceFactor(node);\n    // 左に偏った木\n    if (bf > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // 右回転\n            return rightRotate(node);\n        } else {\n            // 左回転してから右回転\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // 右に偏った木\n    if (bf < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // 左回転\n            return leftRotate(node);\n        } else {\n            // 右回転してから左回転\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node;\n}\n
avl_tree.kt
/* 回転操作を行い、この部分木の平衡を回復する */\nfun rotate(node: TreeNode): TreeNode {\n    // ノード node の平衡係数を取得\n    val balanceFactor = balanceFactor(node)\n    // 左に偏った木\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // 右回転\n            return rightRotate(node)\n        } else {\n            // 左回転してから右回転\n            node.left = leftRotate(node.left)\n            return rightRotate(node)\n        }\n    }\n    // 右に偏った木\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // 左回転\n            return leftRotate(node)\n        } else {\n            // 右回転してから左回転\n            node.right = rightRotate(node.right)\n            return leftRotate(node)\n        }\n    }\n    // 平衡木なので回転不要、そのまま返す\n    return node\n}\n
avl_tree.rb
### 回転操作を行い、この部分木の平衡を回復する ###\ndef rotate(node)\n  # ノード node の平衡係数を取得\n  balance_factor = balance_factor(node)\n  # 左部分木をたどる\n  if balance_factor > 1\n    if balance_factor(node.left) >= 0\n      # 右回転\n      return right_rotate(node)\n    else\n      # 左回転してから右回転\n      node.left = left_rotate(node.left)\n      return right_rotate(node)\n    end\n  # 右に偏った木\n  elsif balance_factor < -1\n    if balance_factor(node.right) <= 0\n      # 左回転\n      return left_rotate(node)\n    else\n      # 右回転してから左回転\n      node.right = right_rotate(node.right)\n      return left_rotate(node)\n    end\n  end\n  # 平衡木なので回転不要、そのまま返す\n  node\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#753-avl","level":2,"title":"7.5.3   AVL 木の基本操作","text":"","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_2","level":3,"title":"1.   ノードの挿入","text":"

AVL 木のノード挿入は、基本的には二分探索木と同じです。唯一の違いは、AVL 木ではノード挿入後に、そのノードから根ノードまでの経路上に複数の不平衡ノードが現れる可能性があることです。したがって、このノードから開始して、下から上へ回転操作を行い、すべての不平衡ノードを平衡に戻す必要があります。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def insert(self, val):\n    \"\"\"ノードを挿入\"\"\"\n    self._root = self.insert_helper(self._root, val)\n\ndef insert_helper(self, node: TreeNode | None, val: int) -> TreeNode:\n    \"\"\"ノードを再帰的に挿入する(補助メソッド)\"\"\"\n    if node is None:\n        return TreeNode(val)\n    # 1. 挿入位置を探索してノードを挿入\n    if val < node.val:\n        node.left = self.insert_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.insert_helper(node.right, val)\n    else:\n        # 重複ノードは挿入せず、そのまま返す\n        return node\n    # ノードの高さを更新する\n    self.update_height(node)\n    # 2. 回転操作を行い、部分木の平衡を回復する\n    return self.rotate(node)\n
avl_tree.cpp
/* ノードを挿入 */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return new TreeNode(val);\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node->val)\n        node->left = insertHelper(node->left, val);\n    else if (val > node->val)\n        node->right = insertHelper(node->right, val);\n    else\n        return node;    // 重複ノードは挿入せず、そのまま返す\n    updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.java
/* ノードを挿入 */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nTreeNode insertHelper(TreeNode node, int val) {\n    if (node == null)\n        return new TreeNode(val);\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node.val)\n        node.left = insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = insertHelper(node.right, val);\n    else\n        return node; // 重複ノードは挿入せず、そのまま返す\n    updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.cs
/* ノードを挿入 */\nvoid Insert(int val) {\n    root = InsertHelper(root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nTreeNode? InsertHelper(TreeNode? node, int val) {\n    if (node == null) return new TreeNode(val);\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node.val)\n        node.left = InsertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = InsertHelper(node.right, val);\n    else\n        return node;     // 重複ノードは挿入せず、そのまま返す\n    UpdateHeight(node);  // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = Rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.go
/* ノードを挿入 */\nfunc (t *aVLTree) insert(val int) {\n    t.root = t.insertHelper(t.root, val)\n}\n\n/* ノードを再帰的に挿入する(補助関数) */\nfunc (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return NewTreeNode(val)\n    }\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if val < node.Val.(int) {\n        node.Left = t.insertHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.insertHelper(node.Right, val)\n    } else {\n        // 重複ノードは挿入せず、そのまま返す\n        return node\n    }\n    // ノードの高さを更新する\n    t.updateHeight(node)\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = t.rotate(node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.swift
/* ノードを挿入 */\nfunc insert(val: Int) {\n    root = insertHelper(node: root, val: val)\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nfunc insertHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return TreeNode(x: val)\n    }\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if val < node!.val {\n        node?.left = insertHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = insertHelper(node: node?.right, val: val)\n    } else {\n        return node // 重複ノードは挿入せず、そのまま返す\n    }\n    updateHeight(node: node) // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node: node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.js
/* ノードを挿入 */\ninsert(val) {\n    this.root = this.#insertHelper(this.root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\n#insertHelper(node, val) {\n    if (node === null) return new TreeNode(val);\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node.val) node.left = this.#insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#insertHelper(node.right, val);\n    else return node; // 重複ノードは挿入せず、そのまま返す\n    this.#updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = this.#rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.ts
/* ノードを挿入 */\ninsert(val: number): void {\n    this.root = this.insertHelper(this.root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\ninsertHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return new TreeNode(val);\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node.val) {\n        node.left = this.insertHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.insertHelper(node.right, val);\n    } else {\n        return node; // 重複ノードは挿入せず、そのまま返す\n    }\n    this.updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = this.rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.dart
/* ノードを挿入 */\nvoid insert(int val) {\n  root = insertHelper(root, val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nTreeNode? insertHelper(TreeNode? node, int val) {\n  if (node == null) return TreeNode(val);\n  /* 1. 挿入位置を探索してノードを挿入 */\n  if (val < node.val)\n    node.left = insertHelper(node.left, val);\n  else if (val > node.val)\n    node.right = insertHelper(node.right, val);\n  else\n    return node; // 重複ノードは挿入せず、そのまま返す\n  updateHeight(node); // ノードの高さを更新する\n  /* 2. 回転操作を行い、部分木の平衡を回復する */\n  node = rotate(node);\n  // 部分木の根ノードを返す\n  return node;\n}\n
avl_tree.rs
/* ノードを挿入 */\nfn insert(&mut self, val: i32) {\n    self.root = Self::insert_helper(self.root.clone(), val);\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nfn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. 挿入位置を探索してノードを挿入 */\n            match {\n                let node_val = node.borrow().val;\n                node_val\n            }\n            .cmp(&val)\n            {\n                Ordering::Greater => {\n                    let left = node.borrow().left.clone();\n                    node.borrow_mut().left = Self::insert_helper(left, val);\n                }\n                Ordering::Less => {\n                    let right = node.borrow().right.clone();\n                    node.borrow_mut().right = Self::insert_helper(right, val);\n                }\n                Ordering::Equal => {\n                    return Some(node); // 重複ノードは挿入せず、そのまま返す\n                }\n            }\n            Self::update_height(Some(node.clone())); // ノードの高さを更新する\n\n            /* 2. 回転操作を行い、部分木の平衡を回復する */\n            node = Self::rotate(Some(node)).unwrap();\n            // 部分木の根ノードを返す\n            Some(node)\n        }\n        None => Some(TreeNode::new(val)),\n    }\n}\n
avl_tree.c
/* ノードを挿入 */\nvoid insert(AVLTree *tree, int val) {\n    tree->root = insertHelper(tree->root, val);\n}\n\n/* ノードを再帰的に挿入する(補助関数) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == NULL) {\n        return newTreeNode(val);\n    }\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (val < node->val) {\n        node->left = insertHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = insertHelper(node->right, val);\n    } else {\n        // 重複ノードは挿入せず、そのまま返す\n        return node;\n    }\n    // ノードの高さを更新する\n    updateHeight(node);\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.kt
/* ノードを挿入 */\nfun insert(_val: Int) {\n    root = insertHelper(root, _val)\n}\n\n/* ノードを再帰的に挿入する(補助メソッド) */\nfun insertHelper(n: TreeNode?, _val: Int): TreeNode {\n    if (n == null)\n        return TreeNode(_val)\n    var node = n\n    /* 1. 挿入位置を探索してノードを挿入 */\n    if (_val < node._val)\n        node.left = insertHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = insertHelper(node.right, _val)\n    else\n        return node // 重複ノードは挿入せず、そのまま返す\n    updateHeight(node) // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.rb
### ノードを挿入 ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n### ノードを挿入 ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n# ## ノードを再帰的に挿入(補助メソッド)###\ndef insert_helper(node, val)\n  return TreeNode.new(val) if node.nil?\n  # 1. 挿入位置を探索してノードを挿入\n  if val < node.val\n    node.left = insert_helper(node.left, val)\n  elsif val > node.val\n    node.right = insert_helper(node.right, val)\n  else\n    # 重複ノードは挿入せず、そのまま返す\n    return node\n  end\n  # ノードの高さを更新する\n  update_height(node)\n  # 2. 回転操作を行い、部分木の平衡を回復する\n  rotate(node)\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_2","level":3,"title":"2.   ノードの削除","text":"

同様に、二分探索木のノード削除メソッドを土台として、下から上へ回転操作を行い、すべての不平衡ノードを平衡に戻す必要があります。コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def remove(self, val: int):\n    \"\"\"ノードを削除\"\"\"\n    self._root = self.remove_helper(self._root, val)\n\ndef remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None:\n    \"\"\"ノードを再帰的に削除する(補助メソッド)\"\"\"\n    if node is None:\n        return None\n    # 1. ノードを探索して削除\n    if val < node.val:\n        node.left = self.remove_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.remove_helper(node.right, val)\n    else:\n        if node.left is None or node.right is None:\n            child = node.left or node.right\n            # 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if child is None:\n                return None\n            # 子ノード数 = 1 の場合、node をそのまま削除する\n            else:\n                node = child\n        else:\n            # 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            temp = node.right\n            while temp.left is not None:\n                temp = temp.left\n            node.right = self.remove_helper(node.right, temp.val)\n            node.val = temp.val\n    # ノードの高さを更新する\n    self.update_height(node)\n    # 2. 回転操作を行い、部分木の平衡を回復する\n    return self.rotate(node)\n
avl_tree.cpp
/* ノードを削除 */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return nullptr;\n    /* 1. ノードを探索して削除 */\n    if (val < node->val)\n        node->left = removeHelper(node->left, val);\n    else if (val > node->val)\n        node->right = removeHelper(node->right, val);\n    else {\n        if (node->left == nullptr || node->right == nullptr) {\n            TreeNode *child = node->left != nullptr ? node->left : node->right;\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child == nullptr) {\n                delete node;\n                return nullptr;\n            }\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else {\n                delete node;\n                node = child;\n            }\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            TreeNode *temp = node->right;\n            while (temp->left != nullptr) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.java
/* ノードを削除 */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nTreeNode removeHelper(TreeNode node, int val) {\n    if (node == null)\n        return null;\n    /* 1. ノードを探索して削除 */\n    if (val < node.val)\n        node.left = removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = removeHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode child = node.left != null ? node.left : node.right;\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child == null)\n                return null;\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else\n                node = child;\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            TreeNode temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.cs
/* ノードを削除 */\nvoid Remove(int val) {\n    root = RemoveHelper(root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nTreeNode? RemoveHelper(TreeNode? node, int val) {\n    if (node == null) return null;\n    /* 1. ノードを探索して削除 */\n    if (val < node.val)\n        node.left = RemoveHelper(node.left, val);\n    else if (val > node.val)\n        node.right = RemoveHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode? child = node.left ?? node.right;\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child == null)\n                return null;\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else\n                node = child;\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            TreeNode? temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = RemoveHelper(node.right, temp.val!.Value);\n            node.val = temp.val;\n        }\n    }\n    UpdateHeight(node);  // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = Rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.go
/* ノードを削除 */\nfunc (t *aVLTree) remove(val int) {\n    t.root = t.removeHelper(t.root, val)\n}\n\n/* ノードを再帰的に削除する(補助関数) */\nfunc (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return nil\n    }\n    /* 1. ノードを探索して削除 */\n    if val < node.Val.(int) {\n        node.Left = t.removeHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.removeHelper(node.Right, val)\n    } else {\n        if node.Left == nil || node.Right == nil {\n            child := node.Left\n            if node.Right != nil {\n                child = node.Right\n            }\n            if child == nil {\n                // 子ノード数 = 0 の場合、node をそのまま削除して返す\n                return nil\n            } else {\n                // 子ノード数 = 1 の場合、node をそのまま削除する\n                node = child\n            }\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            temp := node.Right\n            for temp.Left != nil {\n                temp = temp.Left\n            }\n            node.Right = t.removeHelper(node.Right, temp.Val.(int))\n            node.Val = temp.Val\n        }\n    }\n    // ノードの高さを更新する\n    t.updateHeight(node)\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = t.rotate(node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.swift
/* ノードを削除 */\nfunc remove(val: Int) {\n    root = removeHelper(node: root, val: val)\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nfunc removeHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return nil\n    }\n    /* 1. ノードを探索して削除 */\n    if val < node!.val {\n        node?.left = removeHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = removeHelper(node: node?.right, val: val)\n    } else {\n        if node?.left == nil || node?.right == nil {\n            let child = node?.left ?? node?.right\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if child == nil {\n                return nil\n            }\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else {\n                node = child\n            }\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            var temp = node?.right\n            while temp?.left != nil {\n                temp = temp?.left\n            }\n            node?.right = removeHelper(node: node?.right, val: temp!.val)\n            node?.val = temp!.val\n        }\n    }\n    updateHeight(node: node) // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node: node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.js
/* ノードを削除 */\nremove(val) {\n    this.root = this.#removeHelper(this.root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\n#removeHelper(node, val) {\n    if (node === null) return null;\n    /* 1. ノードを探索して削除 */\n    if (val < node.val) node.left = this.#removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#removeHelper(node.right, val);\n    else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child === null) return null;\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else node = child;\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.#removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.#updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = this.#rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.ts
/* ノードを削除 */\nremove(val: number): void {\n    this.root = this.removeHelper(this.root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nremoveHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return null;\n    /* 1. ノードを探索して削除 */\n    if (val < node.val) {\n        node.left = this.removeHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.removeHelper(node.right, val);\n    } else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child === null) {\n                return null;\n            } else {\n                // 子ノード数 = 1 の場合、node をそのまま削除する\n                node = child;\n            }\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.updateHeight(node); // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = this.rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.dart
/* ノードを削除 */\nvoid remove(int val) {\n  root = removeHelper(root, val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nTreeNode? removeHelper(TreeNode? node, int val) {\n  if (node == null) return null;\n  /* 1. ノードを探索して削除 */\n  if (val < node.val)\n    node.left = removeHelper(node.left, val);\n  else if (val > node.val)\n    node.right = removeHelper(node.right, val);\n  else {\n    if (node.left == null || node.right == null) {\n      TreeNode? child = node.left ?? node.right;\n      // 子ノード数 = 0 の場合、node をそのまま削除して返す\n      if (child == null)\n        return null;\n      // 子ノード数 = 1 の場合、node をそのまま削除する\n      else\n        node = child;\n    } else {\n      // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n      TreeNode? temp = node.right;\n      while (temp!.left != null) {\n        temp = temp.left;\n      }\n      node.right = removeHelper(node.right, temp.val);\n      node.val = temp.val;\n    }\n  }\n  updateHeight(node); // ノードの高さを更新する\n  /* 2. 回転操作を行い、部分木の平衡を回復する */\n  node = rotate(node);\n  // 部分木の根ノードを返す\n  return node;\n}\n
avl_tree.rs
/* ノードを削除 */\nfn remove(&self, val: i32) {\n    Self::remove_helper(self.root.clone(), val);\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nfn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. ノードを探索して削除 */\n            if val < node.borrow().val {\n                let left = node.borrow().left.clone();\n                node.borrow_mut().left = Self::remove_helper(left, val);\n            } else if val > node.borrow().val {\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, val);\n            } else if node.borrow().left.is_none() || node.borrow().right.is_none() {\n                let child = if node.borrow().left.is_some() {\n                    node.borrow().left.clone()\n                } else {\n                    node.borrow().right.clone()\n                };\n                match child {\n                    // 子ノード数 = 0 の場合、node をそのまま削除して返す\n                    None => {\n                        return None;\n                    }\n                    // 子ノード数 = 1 の場合、node をそのまま削除する\n                    Some(child) => node = child,\n                }\n            } else {\n                // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n                let mut temp = node.borrow().right.clone().unwrap();\n                loop {\n                    let temp_left = temp.borrow().left.clone();\n                    if temp_left.is_none() {\n                        break;\n                    }\n                    temp = temp_left.unwrap();\n                }\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val);\n                node.borrow_mut().val = temp.borrow().val;\n            }\n            Self::update_height(Some(node.clone())); // ノードの高さを更新する\n\n            /* 2. 回転操作を行い、部分木の平衡を回復する */\n            node = Self::rotate(Some(node)).unwrap();\n            // 部分木の根ノードを返す\n            Some(node)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* ノードを削除 */\n// stdio.h を導入しているため、ここでは remove 識別子を使えない\nvoid removeItem(AVLTree *tree, int val) {\n    TreeNode *root = removeHelper(tree->root, val);\n}\n\n/* ノードを再帰的に削除する(補助関数) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    TreeNode *child, *grandChild;\n    if (node == NULL) {\n        return NULL;\n    }\n    /* 1. ノードを探索して削除 */\n    if (val < node->val) {\n        node->left = removeHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = removeHelper(node->right, val);\n    } else {\n        if (node->left == NULL || node->right == NULL) {\n            child = node->left;\n            if (node->right != NULL) {\n                child = node->right;\n            }\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child == NULL) {\n                return NULL;\n            } else {\n                // 子ノード数 = 1 の場合、node をそのまま削除する\n                node = child;\n            }\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            TreeNode *temp = node->right;\n            while (temp->left != NULL) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    // ノードの高さを更新する\n    updateHeight(node);\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node);\n    // 部分木の根ノードを返す\n    return node;\n}\n
avl_tree.kt
/* ノードを削除 */\nfun remove(_val: Int) {\n    root = removeHelper(root, _val)\n}\n\n/* ノードを再帰的に削除する(補助メソッド) */\nfun removeHelper(n: TreeNode?, _val: Int): TreeNode? {\n    var node = n ?: return null\n    /* 1. ノードを探索して削除 */\n    if (_val < node._val)\n        node.left = removeHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = removeHelper(node.right, _val)\n    else {\n        if (node.left == null || node.right == null) {\n            val child = if (node.left != null)\n                node.left\n            else\n                node.right\n            // 子ノード数 = 0 の場合、node をそのまま削除して返す\n            if (child == null)\n                return null\n            // 子ノード数 = 1 の場合、node をそのまま削除する\n            else\n                node = child\n        } else {\n            // 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n            var temp = node.right\n            while (temp!!.left != null) {\n                temp = temp.left\n            }\n            node.right = removeHelper(node.right, temp._val)\n            node._val = temp._val\n        }\n    }\n    updateHeight(node) // ノードの高さを更新する\n    /* 2. 回転操作を行い、部分木の平衡を回復する */\n    node = rotate(node)\n    // 部分木の根ノードを返す\n    return node\n}\n
avl_tree.rb
### ノードを削除 ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n### ノードを削除 ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n# ## ノードを再帰的に削除(補助メソッド)###\ndef remove_helper(node, val)\n  return if node.nil?\n  # 1. ノードを探索して削除\n  if val < node.val\n    node.left = remove_helper(node.left, val)\n  elsif val > node.val\n    node.right = remove_helper(node.right, val)\n  else\n    if node.left.nil? || node.right.nil?\n      child = node.left || node.right\n      # 子ノード数 = 0 の場合、node をそのまま削除して返す\n      return if child.nil?\n      # 子ノード数 = 1 の場合、node をそのまま削除する\n      node = child\n    else\n      # 子ノード数 = 2 の場合、中順走査の次のノードを削除し、そのノードで現在のノードを置き換える\n      temp = node.right\n      while !temp.left.nil?\n        temp = temp.left\n      end\n      node.right = remove_helper(node.right, temp.val)\n      node.val = temp.val\n    end\n  end\n  # ノードの高さを更新する\n  update_height(node)\n  # 2. 回転操作を行い、部分木の平衡を回復する\n  rotate(node)\nend\n
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3_1","level":3,"title":"3.   ノードの探索","text":"

AVL 木のノード探索操作は二分探索木と同じなので、ここでは繰り返しません。

","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#754-avl","level":2,"title":"7.5.4   AVL 木の代表的な応用","text":"
  • 大規模データの整理・格納に用いられ、高頻度の探索と低頻度の追加・削除に適しています。
  • データベースのインデックスシステムの構築に使われます。
  • 赤黒木も代表的な平衡二分探索木の一つです。AVL 木と比べると、赤黒木は平衡条件がより緩く、ノードの挿入・削除に必要な回転操作が少ないため、平均的な更新効率はより高くなります。
","path":["第 7 章   木","7.5   AVL 木 *"],"tags":[]},{"location":"chapter_tree/binary_search_tree/","level":1,"title":"7.4   二分探索木","text":"

以下の図に示すように、二分探索木(binary search tree)は次の条件を満たします。

  1. 根ノードについて、左部分木のすべてのノードの値 \\(<\\) 根ノードの値 \\(<\\) 右部分木のすべてのノードの値。
  2. 任意のノードの左部分木と右部分木も二分探索木であり、すなわち条件 1. も満たします。

図 7-16   二分探索木

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#741","level":2,"title":"7.4.1   二分探索木の操作","text":"

二分探索木をクラス BinarySearchTree としてカプセル化し、木の根ノードを指すメンバ変数 root を宣言します。

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#1","level":3,"title":"1.   ノードの探索","text":"

目標ノードの値 num が与えられたら、二分探索木の性質に基づいて探索できます。以下の図に示すように、ノード cur を宣言し、二分木の根ノード root から出発して、ノード値 cur.valnum の大小関係を繰り返し比較します。

  • cur.val < num の場合、目標ノードは cur の右部分木にあるため、cur = cur.right を実行します。
  • cur.val > num の場合、目標ノードは cur の左部分木にあるため、cur = cur.left を実行します。
  • cur.val = num の場合、目標ノードが見つかったことを表し、ループを抜けてそのノードを返します。
<1><2><3><4>

図 7-17   二分探索木のノード探索例

二分探索木の探索操作は二分探索アルゴリズムと同じ原理で動作し、各ラウンドで半分の候補を除外します。ループ回数の上限は二分木の高さであり、二分木が平衡であれば \\(O(\\log n)\\) 時間です。コード例は次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def search(self, num: int) -> TreeNode | None:\n    \"\"\"ノードを探索\"\"\"\n    cur = self._root\n    # ループで探索し、葉ノードを越えたら抜ける\n    while cur is not None:\n        # 目標ノードは cur の右部分木にある\n        if cur.val < num:\n            cur = cur.right\n        # 目標ノードは cur の左部分木にある\n        elif cur.val > num:\n            cur = cur.left\n        # 目標ノードが見つかったらループを抜ける\n        else:\n            break\n    return cur\n
binary_search_tree.cpp
/* ノードを探索 */\nTreeNode *search(int num) {\n    TreeNode *cur = root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != nullptr) {\n        // 目標ノードは cur の右部分木にある\n        if (cur->val < num)\n            cur = cur->right;\n        // 目標ノードは cur の左部分木にある\n        else if (cur->val > num)\n            cur = cur->left;\n        // 目標ノードが見つかったらループを抜ける\n        else\n            break;\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.java
/* ノードを探索 */\nTreeNode search(int num) {\n    TreeNode cur = root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 目標ノードは cur の右部分木にある\n        if (cur.val < num)\n            cur = cur.right;\n        // 目標ノードは cur の左部分木にある\n        else if (cur.val > num)\n            cur = cur.left;\n        // 目標ノードが見つかったらループを抜ける\n        else\n            break;\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.cs
/* ノードを探索 */\nTreeNode? Search(int num) {\n    TreeNode? cur = root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 目標ノードは cur の右部分木にある\n        if (cur.val < num) cur =\n            cur.right;\n        // 目標ノードは cur の左部分木にある\n        else if (cur.val > num)\n            cur = cur.left;\n        // 目標ノードが見つかったらループを抜ける\n        else\n            break;\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.go
/* ノードを探索 */\nfunc (bst *binarySearchTree) search(num int) *TreeNode {\n    node := bst.root\n    // ループで探索し、葉ノードを越えたら抜ける\n    for node != nil {\n        if node.Val.(int) < num {\n            // 目標ノードは cur の右部分木にある\n            node = node.Right\n        } else if node.Val.(int) > num {\n            // 目標ノードは cur の左部分木にある\n            node = node.Left\n        } else {\n            // 目標ノードが見つかったらループを抜ける\n            break\n        }\n    }\n    // 目標ノードを返す\n    return node\n}\n
binary_search_tree.swift
/* ノードを探索 */\nfunc search(num: Int) -> TreeNode? {\n    var cur = root\n    // ループで探索し、葉ノードを越えたら抜ける\n    while cur != nil {\n        // 目標ノードは cur の右部分木にある\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // 目標ノードは cur の左部分木にある\n        else if cur!.val > num {\n            cur = cur?.left\n        }\n        // 目標ノードが見つかったらループを抜ける\n        else {\n            break\n        }\n    }\n    // 目標ノードを返す\n    return cur\n}\n
binary_search_tree.js
/* ノードを探索 */\nsearch(num) {\n    let cur = this.root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 目標ノードは cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 目標ノードは cur の左部分木にある\n        else if (cur.val > num) cur = cur.left;\n        // 目標ノードが見つかったらループを抜ける\n        else break;\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.ts
/* ノードを探索 */\nsearch(num: number): TreeNode | null {\n    let cur = this.root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 目標ノードは cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 目標ノードは cur の左部分木にある\n        else if (cur.val > num) cur = cur.left;\n        // 目標ノードが見つかったらループを抜ける\n        else break;\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.dart
/* ノードを探索 */\nTreeNode? search(int _num) {\n  TreeNode? cur = _root;\n  // ループで探索し、葉ノードを越えたら抜ける\n  while (cur != null) {\n    // 目標ノードは cur の右部分木にある\n    if (cur.val < _num)\n      cur = cur.right;\n    // 目標ノードは cur の左部分木にある\n    else if (cur.val > _num)\n      cur = cur.left;\n    // 目標ノードが見つかったらループを抜ける\n    else\n      break;\n  }\n  // 目標ノードを返す\n  return cur;\n}\n
binary_search_tree.rs
/* ノードを探索 */\npub fn search(&self, num: i32) -> OptionTreeNodeRc {\n    let mut cur = self.root.clone();\n    // ループで探索し、葉ノードを越えたら抜ける\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // 目標ノードは cur の右部分木にある\n            Ordering::Greater => cur = node.borrow().right.clone(),\n            // 目標ノードは cur の左部分木にある\n            Ordering::Less => cur = node.borrow().left.clone(),\n            // 目標ノードが見つかったらループを抜ける\n            Ordering::Equal => break,\n        }\n    }\n\n    // 目標ノードを返す\n    cur\n}\n
binary_search_tree.c
/* ノードを探索 */\nTreeNode *search(BinarySearchTree *bst, int num) {\n    TreeNode *cur = bst->root;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != NULL) {\n        if (cur->val < num) {\n            // 目標ノードは cur の右部分木にある\n            cur = cur->right;\n        } else if (cur->val > num) {\n            // 目標ノードは cur の左部分木にある\n            cur = cur->left;\n        } else {\n            // 目標ノードが見つかったらループを抜ける\n            break;\n        }\n    }\n    // 目標ノードを返す\n    return cur;\n}\n
binary_search_tree.kt
/* ノードを探索 */\nfun search(num: Int): TreeNode? {\n    var cur = root\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 目標ノードは cur の右部分木にある\n        cur = if (cur._val < num)\n            cur.right\n        // 目標ノードは cur の左部分木にある\n        else if (cur._val > num)\n            cur.left\n        // 目標ノードが見つかったらループを抜ける\n        else\n            break\n    }\n    // 目標ノードを返す\n    return cur\n}\n
binary_search_tree.rb
### ノードを検索 ###\ndef search(num)\n  cur = @root\n\n  # ループで探索し、葉ノードを越えたら抜ける\n  while !cur.nil?\n    # 目標ノードは cur の右部分木にある\n    if cur.val < num\n      cur = cur.right\n    # 目標ノードは cur の左部分木にある\n    elsif cur.val > num\n      cur = cur.left\n    # 目標ノードが見つかったらループを抜ける\n    else\n      break\n    end\n  end\n\n  cur\nend\n
コードの可視化

全画面で見る >

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#2","level":3,"title":"2.   ノードの挿入","text":"

挿入する要素 num が与えられたとき、二分探索木の「左部分木 < 根ノード < 右部分木」という性質を保つため、挿入操作の流れは以下の図のようになります。

  1. 挿入位置を探索する:探索操作と同様に、根ノードから出発し、現在のノード値と num の大小関係に基づいて下方向へ探索を繰り返し、葉ノードを越えて(None まで到達して)ループを抜けます。
  2. その位置にノードを挿入する:ノード num を初期化し、そのノードを None の位置に置きます。

図 7-18   二分探索木にノードを挿入する

コード実装では、次の 2 点に注意が必要です。

  • 二分探索木では重複ノードを許可しません。そうでないと定義に反するためです。したがって、挿入対象のノードが木内にすでに存在する場合は、挿入を行わずそのまま返します。
  • ノード挿入を実現するために、ノード pre を用いて前回のループのノードを保持する必要があります。これにより、None までたどり着いたときにその親ノードを取得でき、ノード挿入を完了できます。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def insert(self, num: int):\n    \"\"\"ノードを挿入\"\"\"\n    # 木が空なら、根ノードを初期化する\n    if self._root is None:\n        self._root = TreeNode(num)\n        return\n    # ループで探索し、葉ノードを越えたら抜ける\n    cur, pre = self._root, None\n    while cur is not None:\n        # 重複ノードが見つかったら、直ちに返す\n        if cur.val == num:\n            return\n        pre = cur\n        # 挿入位置は cur の右部分木にある\n        if cur.val < num:\n            cur = cur.right\n        # 挿入位置は cur の左部分木にある\n        else:\n            cur = cur.left\n    # ノードを挿入\n    node = TreeNode(num)\n    if pre.val < num:\n        pre.right = node\n    else:\n        pre.left = node\n
binary_search_tree.cpp
/* ノードを挿入 */\nvoid insert(int num) {\n    // 木が空なら、根ノードを初期化する\n    if (root == nullptr) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode *cur = root, *pre = nullptr;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != nullptr) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur->val == num)\n            return;\n        pre = cur;\n        // 挿入位置は cur の右部分木にある\n        if (cur->val < num)\n            cur = cur->right;\n        // 挿入位置は cur の左部分木にある\n        else\n            cur = cur->left;\n    }\n    // ノードを挿入\n    TreeNode *node = new TreeNode(num);\n    if (pre->val < num)\n        pre->right = node;\n    else\n        pre->left = node;\n}\n
binary_search_tree.java
/* ノードを挿入 */\nvoid insert(int num) {\n    // 木が空なら、根ノードを初期化する\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode cur = root, pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // 挿入位置は cur の右部分木にある\n        if (cur.val < num)\n            cur = cur.right;\n        // 挿入位置は cur の左部分木にある\n        else\n            cur = cur.left;\n    }\n    // ノードを挿入\n    TreeNode node = new TreeNode(num);\n    if (pre.val < num)\n        pre.right = node;\n    else\n        pre.left = node;\n}\n
binary_search_tree.cs
/* ノードを挿入 */\nvoid Insert(int num) {\n    // 木が空なら、根ノードを初期化する\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode? cur = root, pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // 挿入位置は cur の右部分木にある\n        if (cur.val < num)\n            cur = cur.right;\n        // 挿入位置は cur の左部分木にある\n        else\n            cur = cur.left;\n    }\n\n    // ノードを挿入\n    TreeNode node = new(num);\n    if (pre != null) {\n        if (pre.val < num)\n            pre.right = node;\n        else\n            pre.left = node;\n    }\n}\n
binary_search_tree.go
/* ノードを挿入 */\nfunc (bst *binarySearchTree) insert(num int) {\n    cur := bst.root\n    // 木が空なら、根ノードを初期化する\n    if cur == nil {\n        bst.root = NewTreeNode(num)\n        return\n    }\n    // 挿入対象ノードの直前のノード位置\n    var pre *TreeNode = nil\n    // ループで探索し、葉ノードを越えたら抜ける\n    for cur != nil {\n        if cur.Val == num {\n            return\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            cur = cur.Right\n        } else {\n            cur = cur.Left\n        }\n    }\n    // ノードを挿入\n    node := NewTreeNode(num)\n    if pre.Val.(int) < num {\n        pre.Right = node\n    } else {\n        pre.Left = node\n    }\n}\n
binary_search_tree.swift
/* ノードを挿入 */\nfunc insert(num: Int) {\n    // 木が空なら、根ノードを初期化する\n    if root == nil {\n        root = TreeNode(x: num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // ループで探索し、葉ノードを越えたら抜ける\n    while cur != nil {\n        // 重複ノードが見つかったら、直ちに返す\n        if cur!.val == num {\n            return\n        }\n        pre = cur\n        // 挿入位置は cur の右部分木にある\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // 挿入位置は cur の左部分木にある\n        else {\n            cur = cur?.left\n        }\n    }\n    // ノードを挿入\n    let node = TreeNode(x: num)\n    if pre!.val < num {\n        pre?.right = node\n    } else {\n        pre?.left = node\n    }\n}\n
binary_search_tree.js
/* ノードを挿入 */\ninsert(num) {\n    // 木が空なら、根ノードを初期化する\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur = this.root,\n        pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur.val === num) return;\n        pre = cur;\n        // 挿入位置は cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 挿入位置は cur の左部分木にある\n        else cur = cur.left;\n    }\n    // ノードを挿入\n    const node = new TreeNode(num);\n    if (pre.val < num) pre.right = node;\n    else pre.left = node;\n}\n
binary_search_tree.ts
/* ノードを挿入 */\ninsert(num: number): void {\n    // 木が空なら、根ノードを初期化する\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur.val === num) return;\n        pre = cur;\n        // 挿入位置は cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 挿入位置は cur の左部分木にある\n        else cur = cur.left;\n    }\n    // ノードを挿入\n    const node = new TreeNode(num);\n    if (pre!.val < num) pre!.right = node;\n    else pre!.left = node;\n}\n
binary_search_tree.dart
/* ノードを挿入 */\nvoid insert(int _num) {\n  // 木が空なら、根ノードを初期化する\n  if (_root == null) {\n    _root = TreeNode(_num);\n    return;\n  }\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // ループで探索し、葉ノードを越えたら抜ける\n  while (cur != null) {\n    // 重複ノードが見つかったら、直ちに返す\n    if (cur.val == _num) return;\n    pre = cur;\n    // 挿入位置は cur の右部分木にある\n    if (cur.val < _num)\n      cur = cur.right;\n    // 挿入位置は cur の左部分木にある\n    else\n      cur = cur.left;\n  }\n  // ノードを挿入\n  TreeNode? node = TreeNode(_num);\n  if (pre!.val < _num)\n    pre.right = node;\n  else\n    pre.left = node;\n}\n
binary_search_tree.rs
/* ノードを挿入 */\npub fn insert(&mut self, num: i32) {\n    // 木が空なら、根ノードを初期化する\n    if self.root.is_none() {\n        self.root = Some(TreeNode::new(num));\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // 重複ノードが見つかったら、直ちに返す\n            Ordering::Equal => return,\n            // 挿入位置は cur の右部分木にある\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // 挿入位置は cur の左部分木にある\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // ノードを挿入\n    let pre = pre.unwrap();\n    let node = Some(TreeNode::new(num));\n    if num > pre.borrow().val {\n        pre.borrow_mut().right = node;\n    } else {\n        pre.borrow_mut().left = node;\n    }\n}\n
binary_search_tree.c
/* ノードを挿入 */\nvoid insert(BinarySearchTree *bst, int num) {\n    // 木が空なら、根ノードを初期化する\n    if (bst->root == NULL) {\n        bst->root = newTreeNode(num);\n        return;\n    }\n    TreeNode *cur = bst->root, *pre = NULL;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != NULL) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur->val == num) {\n            return;\n        }\n        pre = cur;\n        if (cur->val < num) {\n            // 挿入位置は cur の右部分木にある\n            cur = cur->right;\n        } else {\n            // 挿入位置は cur の左部分木にある\n            cur = cur->left;\n        }\n    }\n    // ノードを挿入\n    TreeNode *node = newTreeNode(num);\n    if (pre->val < num) {\n        pre->right = node;\n    } else {\n        pre->left = node;\n    }\n}\n
binary_search_tree.kt
/* ノードを挿入 */\nfun insert(num: Int) {\n    // 木が空なら、根ノードを初期化する\n    if (root == null) {\n        root = TreeNode(num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode? = null\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 重複ノードが見つかったら、直ちに返す\n        if (cur._val == num)\n            return\n        pre = cur\n        // 挿入位置は cur の右部分木にある\n        cur = if (cur._val < num)\n            cur.right\n        // 挿入位置は cur の左部分木にある\n        else\n            cur.left\n    }\n    // ノードを挿入\n    val node = TreeNode(num)\n    if (pre?._val!! < num)\n        pre.right = node\n    else\n        pre.left = node\n}\n
binary_search_tree.rb
### ノードを挿入 ###\ndef insert(num)\n  # 木が空なら、根ノードを初期化する\n  if @root.nil?\n    @root = TreeNode.new(num)\n    return\n  end\n\n  # ループで探索し、葉ノードを越えたら抜ける\n  cur, pre = @root, nil\n  while !cur.nil?\n    # 重複ノードが見つかったら、直ちに返す\n    return if cur.val == num\n\n    pre = cur\n    # 挿入位置は cur の右部分木にある\n    if cur.val < num\n      cur = cur.right\n    # 挿入位置は cur の左部分木にある\n    else\n      cur = cur.left\n    end\n  end\n\n  # ノードを挿入\n  node = TreeNode.new(num)\n  if pre.val < num\n    pre.right = node\n  else\n    pre.left = node\n  end\nend\n
コードの可視化

全画面で見る >

ノード探索と同様に、ノード挿入には \\(O(\\log n)\\) 時間を要します。

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#3","level":3,"title":"3.   ノードの削除","text":"

まず二分木内で目標ノードを見つけ、その後で削除します。ノード挿入と同様に、削除操作の完了後も二分探索木の「左部分木 < 根ノード < 右部分木」という性質が保たれる必要があります。そのため、目標ノードの子ノード数に応じて、0、1、2 の 3 つのケースに分けて対応する削除操作を行います。

以下の図に示すように、削除対象ノードの次数が \\(0\\) のとき、そのノードは葉ノードであり、直接削除できます。

図 7-19   二分探索木でノードを削除する(次数 0 )

以下の図に示すように、削除対象ノードの次数が \\(1\\) のとき、削除対象ノードをその子ノードで置き換えれば十分です。

図 7-20   二分探索木でノードを削除する(次数 1 )

削除対象ノードの次数が \\(2\\) のときは、直接削除できず、別のノードでそのノードを置き換える必要があります。二分探索木の「左部分木 \\(<\\) 根ノード \\(<\\) 右部分木」という性質を保つ必要があるため、このノードには右部分木の最小ノードまたは左部分木の最大ノードを使えます。

右部分木の最小ノード(中順走査で次のノード)を選ぶと仮定すると、削除操作の流れは以下の図のようになります。

  1. 削除対象ノードの「中順走査列」における次のノードを見つけ、tmp と記します。
  2. tmp の値で削除対象ノードの値を上書きし、木の中でノード tmp を再帰的に削除します。
<1><2><3><4>

図 7-21   二分探索木でノードを削除する(次数 2 )

ノード削除操作も同様に \\(O(\\log n)\\) 時間を要します。削除対象ノードの探索に \\(O(\\log n)\\) 時間、中順走査の後続ノードの取得に \\(O(\\log n)\\) 時間が必要です。コード例は次のとおりです。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def remove(self, num: int):\n    \"\"\"ノードを削除\"\"\"\n    # 木が空なら、そのまま早期リターンする\n    if self._root is None:\n        return\n    # ループで探索し、葉ノードを越えたら抜ける\n    cur, pre = self._root, None\n    while cur is not None:\n        # 削除対象のノードが見つかったら、ループを抜ける\n        if cur.val == num:\n            break\n        pre = cur\n        # 削除対象ノードは cur の右部分木にある\n        if cur.val < num:\n            cur = cur.right\n        # 削除対象ノードは cur の左部分木にある\n        else:\n            cur = cur.left\n    # 削除対象ノードがなければそのまま返す\n    if cur is None:\n        return\n\n    # 子ノード数 = 0 or 1\n    if cur.left is None or cur.right is None:\n        # 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        child = cur.left or cur.right\n        # ノード cur を削除する\n        if cur != self._root:\n            if pre.left == cur:\n                pre.left = child\n            else:\n                pre.right = child\n        else:\n            # 削除ノードが根ノードなら、根ノードを再設定\n            self._root = child\n    # 子ノード数 = 2\n    else:\n        # 中順走査における cur の次ノードを取得\n        tmp: TreeNode = cur.right\n        while tmp.left is not None:\n            tmp = tmp.left\n        # ノード tmp を再帰的に削除\n        self.remove(tmp.val)\n        # tmp で cur を上書きする\n        cur.val = tmp.val\n
binary_search_tree.cpp
/* ノードを削除 */\nvoid remove(int num) {\n    // 木が空なら、そのまま早期リターンする\n    if (root == nullptr)\n        return;\n    TreeNode *cur = root, *pre = nullptr;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != nullptr) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur->val == num)\n            break;\n        pre = cur;\n        // 削除対象ノードは cur の右部分木にある\n        if (cur->val < num)\n            cur = cur->right;\n        // 削除対象ノードは cur の左部分木にある\n        else\n            cur = cur->left;\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur == nullptr)\n        return;\n    // 子ノード数 = 0 or 1\n    if (cur->left == nullptr || cur->right == nullptr) {\n        // 子ノード数 = 0 / 1 のとき、child = nullptr / その子ノード\n        TreeNode *child = cur->left != nullptr ? cur->left : cur->right;\n        // ノード cur を削除する\n        if (cur != root) {\n            if (pre->left == cur)\n                pre->left = child;\n            else\n                pre->right = child;\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            root = child;\n        }\n        // メモリを解放する\n        delete cur;\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        TreeNode *tmp = cur->right;\n        while (tmp->left != nullptr) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // ノード tmp を再帰的に削除\n        remove(tmp->val);\n        // tmp で cur を上書きする\n        cur->val = tmpVal;\n    }\n}\n
binary_search_tree.java
/* ノードを削除 */\nvoid remove(int num) {\n    // 木が空なら、そのまま早期リターンする\n    if (root == null)\n        return;\n    TreeNode cur = root, pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // 削除対象ノードは cur の右部分木にある\n        if (cur.val < num)\n            cur = cur.right;\n        // 削除対象ノードは cur の左部分木にある\n        else\n            cur = cur.left;\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur == null)\n        return;\n    // 子ノード数 = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        TreeNode child = cur.left != null ? cur.left : cur.right;\n        // ノード cur を削除する\n        if (cur != root) {\n            if (pre.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            root = child;\n        }\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        TreeNode tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // ノード tmp を再帰的に削除\n        remove(tmp.val);\n        // tmp で cur を上書きする\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.cs
/* ノードを削除 */\nvoid Remove(int num) {\n    // 木が空なら、そのまま早期リターンする\n    if (root == null)\n        return;\n    TreeNode? cur = root, pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // 削除対象ノードは cur の右部分木にある\n        if (cur.val < num)\n            cur = cur.right;\n        // 削除対象ノードは cur の左部分木にある\n        else\n            cur = cur.left;\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur == null)\n        return;\n    // 子ノード数 = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        TreeNode? child = cur.left ?? cur.right;\n        // ノード cur を削除する\n        if (cur != root) {\n            if (pre!.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            root = child;\n        }\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        TreeNode? tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // ノード tmp を再帰的に削除\n        Remove(tmp.val!.Value);\n        // tmp で cur を上書きする\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.go
/* ノードを削除 */\nfunc (bst *binarySearchTree) remove(num int) {\n    cur := bst.root\n    // 木が空なら、そのまま早期リターンする\n    if cur == nil {\n        return\n    }\n    // 削除対象ノードの直前のノード位置\n    var pre *TreeNode = nil\n    // ループで探索し、葉ノードを越えたら抜ける\n    for cur != nil {\n        if cur.Val == num {\n            break\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            // 削除対象ノードは右部分木にある\n            cur = cur.Right\n        } else {\n            // 削除対象ノードは左部分木にある\n            cur = cur.Left\n        }\n    }\n    // 削除対象ノードがなければそのまま返す\n    if cur == nil {\n        return\n    }\n    // 子ノード数は 0 または 1\n    if cur.Left == nil || cur.Right == nil {\n        var child *TreeNode = nil\n        // 削除対象ノードの子ノードを取り出す\n        if cur.Left != nil {\n            child = cur.Left\n        } else {\n            child = cur.Right\n        }\n        // ノード cur を削除する\n        if cur != bst.root {\n            if pre.Left == cur {\n                pre.Left = child\n            } else {\n                pre.Right = child\n            }\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            bst.root = child\n        }\n        // 子ノード数は 2\n    } else {\n        // 中順走査で削除対象ノード `cur` の次のノードを取得する\n        tmp := cur.Right\n        for tmp.Left != nil {\n            tmp = tmp.Left\n        }\n        // ノード tmp を再帰的に削除\n        bst.remove(tmp.Val.(int))\n        // tmp で cur を上書きする\n        cur.Val = tmp.Val\n    }\n}\n
binary_search_tree.swift
/* ノードを削除 */\nfunc remove(num: Int) {\n    // 木が空なら、そのまま早期リターンする\n    if root == nil {\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // ループで探索し、葉ノードを越えたら抜ける\n    while cur != nil {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if cur!.val == num {\n            break\n        }\n        pre = cur\n        // 削除対象ノードは cur の右部分木にある\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // 削除対象ノードは cur の左部分木にある\n        else {\n            cur = cur?.left\n        }\n    }\n    // 削除対象ノードがなければそのまま返す\n    if cur == nil {\n        return\n    }\n    // 子ノード数 = 0 or 1\n    if cur?.left == nil || cur?.right == nil {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        let child = cur?.left ?? cur?.right\n        // ノード cur を削除する\n        if cur !== root {\n            if pre?.left === cur {\n                pre?.left = child\n            } else {\n                pre?.right = child\n            }\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            root = child\n        }\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        var tmp = cur?.right\n        while tmp?.left != nil {\n            tmp = tmp?.left\n        }\n        // ノード tmp を再帰的に削除\n        remove(num: tmp!.val)\n        // tmp で cur を上書きする\n        cur?.val = tmp!.val\n    }\n}\n
binary_search_tree.js
/* ノードを削除 */\nremove(num) {\n    // 木が空なら、そのまま早期リターンする\n    if (this.root === null) return;\n    let cur = this.root,\n        pre = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur.val === num) break;\n        pre = cur;\n        // 削除対象ノードは cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 削除対象ノードは cur の左部分木にある\n        else cur = cur.left;\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur === null) return;\n    // 子ノード数 = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        const child = cur.left !== null ? cur.left : cur.right;\n        // ノード cur を削除する\n        if (cur !== this.root) {\n            if (pre.left === cur) pre.left = child;\n            else pre.right = child;\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            this.root = child;\n        }\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        let tmp = cur.right;\n        while (tmp.left !== null) {\n            tmp = tmp.left;\n        }\n        // ノード tmp を再帰的に削除\n        this.remove(tmp.val);\n        // tmp で cur を上書きする\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.ts
/* ノードを削除 */\nremove(num: number): void {\n    // 木が空なら、そのまま早期リターンする\n    if (this.root === null) return;\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur !== null) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur.val === num) break;\n        pre = cur;\n        // 削除対象ノードは cur の右部分木にある\n        if (cur.val < num) cur = cur.right;\n        // 削除対象ノードは cur の左部分木にある\n        else cur = cur.left;\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur === null) return;\n    // 子ノード数 = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        const child: TreeNode | null =\n            cur.left !== null ? cur.left : cur.right;\n        // ノード cur を削除する\n        if (cur !== this.root) {\n            if (pre!.left === cur) pre!.left = child;\n            else pre!.right = child;\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            this.root = child;\n        }\n    }\n    // 子ノード数 = 2\n    else {\n        // 中順走査における cur の次ノードを取得\n        let tmp: TreeNode | null = cur.right;\n        while (tmp!.left !== null) {\n            tmp = tmp!.left;\n        }\n        // ノード tmp を再帰的に削除\n        this.remove(tmp!.val);\n        // tmp で cur を上書きする\n        cur.val = tmp!.val;\n    }\n}\n
binary_search_tree.dart
/* ノードを削除 */\nvoid remove(int _num) {\n  // 木が空なら、そのまま早期リターンする\n  if (_root == null) return;\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // ループで探索し、葉ノードを越えたら抜ける\n  while (cur != null) {\n    // 削除対象のノードが見つかったら、ループを抜ける\n    if (cur.val == _num) break;\n    pre = cur;\n    // 削除対象ノードは cur の右部分木にある\n    if (cur.val < _num)\n      cur = cur.right;\n    // 削除対象ノードは cur の左部分木にある\n    else\n      cur = cur.left;\n  }\n  // 削除対象ノードがない場合は、そのまま返す\n  if (cur == null) return;\n  // 子ノード数 = 0 or 1\n  if (cur.left == null || cur.right == null) {\n    // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n    TreeNode? child = cur.left ?? cur.right;\n    // ノード cur を削除する\n    if (cur != _root) {\n      if (pre!.left == cur)\n        pre.left = child;\n      else\n        pre.right = child;\n    } else {\n      // 削除ノードが根ノードなら、根ノードを再設定\n      _root = child;\n    }\n  } else {\n    // 子ノード数 = 2\n    // 中順走査における cur の次のノードを取得\n    TreeNode? tmp = cur.right;\n    while (tmp!.left != null) {\n      tmp = tmp.left;\n    }\n    // ノード tmp を再帰的に削除\n    remove(tmp.val);\n    // tmp で cur を上書きする\n    cur.val = tmp.val;\n  }\n}\n
binary_search_tree.rs
/* ノードを削除 */\npub fn remove(&mut self, num: i32) {\n    // 木が空なら、そのまま早期リターンする\n    if self.root.is_none() {\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // 削除対象のノードが見つかったら、ループを抜ける\n            Ordering::Equal => break,\n            // 削除対象ノードは cur の右部分木にある\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // 削除対象ノードは cur の左部分木にある\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // 削除対象ノードがなければそのまま返す\n    if cur.is_none() {\n        return;\n    }\n    let cur = cur.unwrap();\n    let (left_child, right_child) = (cur.borrow().left.clone(), cur.borrow().right.clone());\n    match (left_child.clone(), right_child.clone()) {\n        // 子ノード数 = 0 or 1\n        (None, None) | (Some(_), None) | (None, Some(_)) => {\n            // 子ノード数 = 0 / 1 のとき、child = nullptr / その子ノード\n            let child = left_child.or(right_child);\n            let pre = pre.unwrap();\n            // ノード cur を削除する\n            if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) {\n                let left = pre.borrow().left.clone();\n                if left.is_some() && Rc::ptr_eq(left.as_ref().unwrap(), &cur) {\n                    pre.borrow_mut().left = child;\n                } else {\n                    pre.borrow_mut().right = child;\n                }\n            } else {\n                // 削除ノードが根ノードなら、根ノードを再設定\n                self.root = child;\n            }\n        }\n        // 子ノード数 = 2\n        (Some(_), Some(_)) => {\n            // 中順走査における cur の次ノードを取得\n            let mut tmp = cur.borrow().right.clone();\n            while let Some(node) = tmp.clone() {\n                if node.borrow().left.is_some() {\n                    tmp = node.borrow().left.clone();\n                } else {\n                    break;\n                }\n            }\n            let tmp_val = tmp.unwrap().borrow().val;\n            // ノード tmp を再帰的に削除\n            self.remove(tmp_val);\n            // tmp で cur を上書きする\n            cur.borrow_mut().val = tmp_val;\n        }\n    }\n}\n
binary_search_tree.c
/* ノードを削除 */\n// stdio.h を導入しているため、ここでは remove 識別子を使えない\nvoid removeItem(BinarySearchTree *bst, int num) {\n    // 木が空なら、そのまま早期リターンする\n    if (bst->root == NULL)\n        return;\n    TreeNode *cur = bst->root, *pre = NULL;\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != NULL) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur->val == num)\n            break;\n        pre = cur;\n        if (cur->val < num) {\n            // 削除対象ノードは root の右部分木にある\n            cur = cur->right;\n        } else {\n            // 削除対象ノードは root の左部分木にある\n            cur = cur->left;\n        }\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur == NULL)\n        return;\n    // 削除対象ノードに子ノードがあるかを判定する\n    if (cur->left == NULL || cur->right == NULL) {\n        /* 子ノード数 = 0 or 1 */\n        // 子ノード数 = 0 / 1 のとき、child = nullptr / その子ノード\n        TreeNode *child = cur->left != NULL ? cur->left : cur->right;\n        // ノード cur を削除する\n        if (pre->left == cur) {\n            pre->left = child;\n        } else {\n            pre->right = child;\n        }\n        // メモリを解放する\n        free(cur);\n    } else {\n        /* 子ノード数 = 2 */\n        // 中順走査における cur の次ノードを取得\n        TreeNode *tmp = cur->right;\n        while (tmp->left != NULL) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // ノード tmp を再帰的に削除\n        removeItem(bst, tmp->val);\n        // tmp で cur を上書きする\n        cur->val = tmpVal;\n    }\n}\n
binary_search_tree.kt
/* ノードを削除 */\nfun remove(num: Int) {\n    // 木が空なら、そのまま早期リターンする\n    if (root == null)\n        return\n    var cur = root\n    var pre: TreeNode? = null\n    // ループで探索し、葉ノードを越えたら抜ける\n    while (cur != null) {\n        // 削除対象のノードが見つかったら、ループを抜ける\n        if (cur._val == num)\n            break\n        pre = cur\n        // 削除対象ノードは cur の右部分木にある\n        cur = if (cur._val < num)\n            cur.right\n        // 削除対象ノードは cur の左部分木にある\n        else\n            cur.left\n    }\n    // 削除対象ノードがなければそのまま返す\n    if (cur == null)\n        return\n    // 子ノード数 = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // 子ノード数が 0 / 1 のとき、child = null / その子ノード\n        val child = if (cur.left != null)\n            cur.left\n        else\n            cur.right\n        // ノード cur を削除する\n        if (cur != root) {\n            if (pre!!.left == cur)\n                pre.left = child\n            else\n                pre.right = child\n        } else {\n            // 削除ノードが根ノードなら、根ノードを再設定\n            root = child\n        }\n        // 子ノード数 = 2\n    } else {\n        // 中順走査における cur の次ノードを取得\n        var tmp = cur.right\n        while (tmp!!.left != null) {\n            tmp = tmp.left\n        }\n        // ノード tmp を再帰的に削除\n        remove(tmp._val)\n        // tmp で cur を上書きする\n        cur._val = tmp._val\n    }\n}\n
binary_search_tree.rb
### ノードを削除 ###\ndef remove(num)\n  # 木が空なら、そのまま早期リターンする\n  return if @root.nil?\n\n  # ループで探索し、葉ノードを越えたら抜ける\n  cur, pre = @root, nil\n  while !cur.nil?\n    # 削除対象のノードが見つかったら、ループを抜ける\n    break if cur.val == num\n\n    pre = cur\n    # 削除対象ノードは cur の右部分木にある\n    if cur.val < num\n      cur = cur.right\n    # 削除対象ノードは cur の左部分木にある\n    else\n      cur = cur.left\n    end\n  end\n  # 削除対象ノードがなければそのまま返す\n  return if cur.nil?\n\n  # 子ノード数 = 0 or 1\n  if cur.left.nil? || cur.right.nil?\n    # 子ノード数が 0 / 1 のとき、child = null / その子ノード\n    child = cur.left || cur.right\n    # ノード cur を削除する\n    if cur != @root\n      if pre.left == cur\n        pre.left = child\n      else\n        pre.right = child\n      end\n    else\n      # 削除ノードが根ノードなら、根ノードを再設定\n      @root = child\n    end\n  # 子ノード数 = 2\n  else\n    # 中順走査における cur の次ノードを取得\n    tmp = cur.right\n    while !tmp.left.nil?\n      tmp = tmp.left\n    end\n    # ノード tmp を再帰的に削除\n    remove(tmp.val)\n    # tmp で cur を上書きする\n    cur.val = tmp.val\n  end\nend\n
コードの可視化

全画面で見る >

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#4","level":3,"title":"4.   中順走査は昇順","text":"

以下の図に示すように、二分木の中順走査は「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」という順序に従い、二分探索木は「左子ノード \\(<\\) 根ノード \\(<\\) 右子ノード」という大小関係を満たします。

これは、二分探索木で中順走査を行うと常に次の最小ノードが優先して走査されることを意味し、そこから重要な性質が導かれます。二分探索木の中順走査列は昇順です。

中順走査が昇順になる性質を利用すれば、二分探索木から整列済みデータを取得するのに必要な時間は \\(O(n)\\) のみで、追加のソート操作は不要です。非常に効率的です。

図 7-22   二分探索木の中順走査列

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#742","level":2,"title":"7.4.2   二分探索木の効率","text":"

あるデータ集合が与えられたとき、配列または二分探索木で格納する場合を考えます。次の表を見ると、二分探索木の各操作の時間計算量はいずれも対数オーダーであり、安定して高効率です。高頻度の追加と低頻度の探索・削除という場面でのみ、配列のほうが二分探索木より効率的です。

表 7-2   配列と探索木の効率比較

無秩序配列 二分探索木 要素の探索 \\(O(n)\\) \\(O(\\log n)\\) 要素の挿入 \\(O(1)\\) \\(O(\\log n)\\) 要素の削除 \\(O(n)\\) \\(O(\\log n)\\)

理想的な状況では、二分探索木は「平衡」しており、その場合は \\(\\log n\\) 回のループ内で任意のノードを探索できます。

しかし、二分探索木でノードの挿入と削除を繰り返すと、二分木が以下の図のような連結リストへ退化する可能性があり、このとき各操作の時間計算量も \\(O(n)\\) に退化します。

図 7-23   二分探索木の退化

","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#743","level":2,"title":"7.4.3   二分探索木の代表的な応用","text":"
  • システム内の多段インデックスとして用いられ、効率的な探索、挿入、削除操作を実現します。
  • 一部の探索アルゴリズムの基盤データ構造として使われます。
  • データストリームを格納し、その順序状態を保つために使われます。
","path":["第 7 章   木","7.4   二分探索木"],"tags":[]},{"location":"chapter_tree/binary_tree/","level":1,"title":"7.1   二分木","text":"

二分木(binary tree)は非線形データ構造の一種であり、「祖先」と「子孫」の派生関係を表し、「一つを二つに分ける」分割統治の考え方を体現しています。連結リストと同様に、二分木の基本単位はノードであり、各ノードは値、左子ノードへの参照、右子ノードへの参照を含みます。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class TreeNode:\n    \"\"\"二分木ノードクラス\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # ノード値\n        self.left: TreeNode | None = None  # 左子ノード参照\n        self.right: TreeNode | None = None # 右子ノード参照\n
/* 二分木ノード構造体 */\nstruct TreeNode {\n    int val;          // ノード値\n    TreeNode *left;   // 左子ノードポインタ\n    TreeNode *right;  // 右子ノードポインタ\n    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}\n};\n
/* 二分木ノードクラス */\nclass TreeNode {\n    int val;         // ノード値\n    TreeNode left;   // 左子ノード参照\n    TreeNode right;  // 右子ノード参照\n    TreeNode(int x) { val = x; }\n}\n
/* 二分木ノードクラス */\nclass TreeNode(int? x) {\n    public int? val = x;    // ノード値\n    public TreeNode? left;  // 左子ノード参照\n    public TreeNode? right; // 右子ノード参照\n}\n
/* 二分木ノード構造体 */\ntype TreeNode struct {\n    Val   int\n    Left  *TreeNode\n    Right *TreeNode\n}\n/* コンストラクタ */\nfunc NewTreeNode(v int) *TreeNode {\n    return &TreeNode{\n        Left:  nil, // 左子ノードポインタ\n        Right: nil, // 右子ノードポインタ\n        Val:   v,   // ノード値\n    }\n}\n
/* 二分木ノードクラス */\nclass TreeNode {\n    var val: Int // ノード値\n    var left: TreeNode? // 左子ノード参照\n    var right: TreeNode? // 右子ノード参照\n\n    init(x: Int) {\n        val = x\n    }\n}\n
/* 二分木ノードクラス */\nclass TreeNode {\n    val; // ノード値\n    left; // 左子ノードポインタ\n    right; // 右子ノードポインタ\n    constructor(val, left, right) {\n        this.val = val === undefined ? 0 : val;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* 二分木ノードクラス */\nclass TreeNode {\n    val: number;\n    left: TreeNode | null;\n    right: TreeNode | null;\n\n    constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val; // ノード値\n        this.left = left === undefined ? null : left; // 左子ノード参照\n        this.right = right === undefined ? null : right; // 右子ノード参照\n    }\n}\n
/* 二分木ノードクラス */\nclass TreeNode {\n  int val;         // ノード値\n  TreeNode? left;  // 左子ノード参照\n  TreeNode? right; // 右子ノード参照\n  TreeNode(this.val, [this.left, this.right]);\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 二分木ノード構造体 */\nstruct TreeNode {\n    val: i32,                               // ノード値\n    left: Option<Rc<RefCell<TreeNode>>>,    // 左子ノード参照\n    right: Option<Rc<RefCell<TreeNode>>>,   // 右子ノード参照\n}\n\nimpl TreeNode {\n    /* コンストラクタ */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            left: None,\n            right: None\n        }))\n    }\n}\n
/* 二分木ノード構造体 */\ntypedef struct TreeNode {\n    int val;                // ノード値\n    int height;             // ノードの高さ\n    struct TreeNode *left;  // 左子ノードポインタ\n    struct TreeNode *right; // 右子ノードポインタ\n} TreeNode;\n\n/* コンストラクタ */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
/* 二分木ノードクラス */\nclass TreeNode(val _val: Int) {  // ノード値\n    val left: TreeNode? = null   // 左子ノード参照\n    val right: TreeNode? = null  // 右子ノード参照\n}\n
### 二分木ノードクラス ###\nclass TreeNode\n  attr_accessor :val    # ノード値\n  attr_accessor :left   # 左子ノード参照\n  attr_accessor :right  # 右子ノード参照\n\n  def initialize(val)\n    @val = val\n  end\nend\n

各ノードは 2 つの参照(ポインタ)を持ち、それぞれ左子ノード(left-child node)と右子ノード(right-child node)を指します。このノードはこれら 2 つの子ノードの親ノード(parent node)と呼ばれます。二分木のあるノードが与えられたとき、そのノードの左子ノードとその配下のノードからなる木をそのノードの左部分木(left subtree)と呼び、同様に右部分木(right subtree)が定義されます。

二分木では、葉ノードを除くすべてのノードが子ノードと空でない部分木を持ちます。以下の図に示すように、「ノード 2」を親ノードとみなすと、その左子ノードと右子ノードはそれぞれ「ノード 4」と「ノード 5」であり、左部分木は「ノード 4 とその配下のノードからなる木」、右部分木は「ノード 5 とその配下のノードからなる木」です。

図 7-1   親ノード、子ノード、部分木

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#711","level":2,"title":"7.1.1   二分木のよく使われる用語","text":"

二分木でよく使われる用語を以下の図に示します。

  • 根ノード(root node):二分木の最上位にあるノードで、親ノードを持ちません。
  • 葉ノード(leaf node):子ノードを持たないノードで、2 本のポインタはいずれも None を指します。
  • 辺(edge):2 つのノードを結ぶ線分、すなわちノード参照(ポインタ)です。
  • ノードが属するレベル(level):上から下へ向かって増加し、根ノードのレベルは 1 です。
  • ノードの次数(degree):ノードの子ノードの数。二分木では次数の取り得る値は 0、1、2 です。
  • 二分木の高さ(height):根ノードから最も遠い葉ノードまでに通る辺の数。
  • ノードの深さ(depth):根ノードからそのノードまでに通る辺の数。
  • ノードの高さ(height):そのノードから最も遠い葉ノードまでに通る辺の数。

図 7-2   二分木のよく使われる用語

Tip

なお、通常「高さ」と「深さ」は「通過した辺の数」と定義しますが、問題や教材によっては「通過したノードの数」と定義する場合もあります。その場合、高さと深さはいずれも 1 を加える必要があります。

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#712","level":2,"title":"7.1.2   二分木の基本操作","text":"","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#1","level":3,"title":"1.   二分木を初期化する","text":"

連結リストと同様に、まずノードを初期化し、その後で参照(ポインタ)を構築します。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
# 二分木を初期化する\n# ノードを初期化する\nn1 = TreeNode(val=1)\nn2 = TreeNode(val=2)\nn3 = TreeNode(val=3)\nn4 = TreeNode(val=4)\nn5 = TreeNode(val=5)\n# ノード間の参照(ポインタ)を構築する\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.cpp
/* 二分木を初期化する */\n// ノードを初期化する\nTreeNode* n1 = new TreeNode(1);\nTreeNode* n2 = new TreeNode(2);\nTreeNode* n3 = new TreeNode(3);\nTreeNode* n4 = new TreeNode(4);\nTreeNode* n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
binary_tree.java
// ノードを初期化する\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.cs
/* 二分木を初期化する */\n// ノードを初期化する\nTreeNode n1 = new(1);\nTreeNode n2 = new(2);\nTreeNode n3 = new(3);\nTreeNode n4 = new(4);\nTreeNode n5 = new(5);\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.go
/* 二分木を初期化する */\n// ノードを初期化する\nn1 := NewTreeNode(1)\nn2 := NewTreeNode(2)\nn3 := NewTreeNode(3)\nn4 := NewTreeNode(4)\nn5 := NewTreeNode(5)\n// ノード間の参照(ポインタ)を構築する\nn1.Left = n2\nn1.Right = n3\nn2.Left = n4\nn2.Right = n5\n
binary_tree.swift
// ノードを初期化する\nlet n1 = TreeNode(x: 1)\nlet n2 = TreeNode(x: 2)\nlet n3 = TreeNode(x: 3)\nlet n4 = TreeNode(x: 4)\nlet n5 = TreeNode(x: 5)\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.js
/* 二分木を初期化する */\n// ノードを初期化する\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.ts
/* 二分木を初期化する */\n// ノードを初期化する\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.dart
/* 二分木を初期化する */\n// ノードを初期化する\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.rs
// ノードを初期化する\nlet n1 = TreeNode::new(1);\nlet n2 = TreeNode::new(2);\nlet n3 = TreeNode::new(3);\nlet n4 = TreeNode::new(4);\nlet n5 = TreeNode::new(5);\n// ノード間の参照(ポインタ)を構築する\nn1.borrow_mut().left = Some(n2.clone());\nn1.borrow_mut().right = Some(n3);\nn2.borrow_mut().left = Some(n4);\nn2.borrow_mut().right = Some(n5);\n
binary_tree.c
/* 二分木を初期化する */\n// ノードを初期化する\nTreeNode *n1 = newTreeNode(1);\nTreeNode *n2 = newTreeNode(2);\nTreeNode *n3 = newTreeNode(3);\nTreeNode *n4 = newTreeNode(4);\nTreeNode *n5 = newTreeNode(5);\n// ノード間の参照(ポインタ)を構築する\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
binary_tree.kt
// ノードを初期化する\nval n1 = TreeNode(1)\nval n2 = TreeNode(2)\nval n3 = TreeNode(3)\nval n4 = TreeNode(4)\nval n5 = TreeNode(5)\n// ノード間の参照(ポインタ)を構築する\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.rb
# 二分木を初期化する\n# ノードを初期化する\nn1 = TreeNode.new(1)\nn2 = TreeNode.new(2)\nn3 = TreeNode.new(3)\nn4 = TreeNode.new(4)\nn5 = TreeNode.new(5)\n# ノード間の参照(ポインタ)を構築する\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
実行の可視化

https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#2","level":3,"title":"2.   ノードの挿入と削除","text":"

連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。以下の図に 1 つの例を示します。

図 7-3   二分木でノードを挿入・削除する

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
# ノードの挿入と削除\np = TreeNode(0)\n# n1 -> n2 の間にノード P を挿入する\nn1.left = p\np.left = n2\n# ノード P を削除する\nn1.left = n2\n
binary_tree.cpp
/* ノードの挿入と削除 */\nTreeNode* P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1->left = P;\nP->left = n2;\n// ノード P を削除する\nn1->left = n2;\n// メモリを解放する\ndelete P;\n
binary_tree.java
TreeNode P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
binary_tree.cs
/* ノードの挿入と削除 */\nTreeNode P = new(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
binary_tree.go
/* ノードの挿入と削除 */\n// n1 -> n2 の間にノード P を挿入する\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// ノード P を削除する\nn1.Left = n2\n
binary_tree.swift
let P = TreeNode(x: 0)\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P\nP.left = n2\n// ノード P を削除する\nn1.left = n2\n
binary_tree.js
/* ノードの挿入と削除 */\nlet P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
binary_tree.ts
/* ノードの挿入と削除 */\nconst P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
binary_tree.dart
/* ノードの挿入と削除 */\nTreeNode P = new TreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P;\nP.left = n2;\n// ノード P を削除する\nn1.left = n2;\n
binary_tree.rs
let p = TreeNode::new(0);\n// n1 -> n2 の間にノード P を挿入する\nn1.borrow_mut().left = Some(p.clone());\np.borrow_mut().left = Some(n2.clone());\n// ノード p を削除する\nn1.borrow_mut().left = Some(n2);\n
binary_tree.c
/* ノードの挿入と削除 */\nTreeNode *P = newTreeNode(0);\n// n1 -> n2 の間にノード P を挿入する\nn1->left = P;\nP->left = n2;\n// ノード P を削除する\nn1->left = n2;\n// メモリを解放する\nfree(P);\n
binary_tree.kt
val P = TreeNode(0)\n// n1 -> n2 の間にノード P を挿入する\nn1.left = P\nP.left = n2\n// ノード P を削除する\nn1.left = n2\n
binary_tree.rb
# ノードの挿入と削除\n_p = TreeNode.new(0)\n# n1 -> n2 の間にノード _p を挿入する\nn1.left = _p\n_p.left = n2\n# ノードを削除する\nn1.left = n2\n
実行の可視化

https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20n2&cumulative=false&curInstr=37&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

Tip

注意すべき点として、ノードの挿入は二分木の元の論理構造を変える可能性があり、ノードの削除は通常、そのノードと配下のすべての部分木の削除を意味します。そのため、二分木における挿入と削除は、実際に意味のある操作を実現するために、通常は一連の操作を組み合わせて行います。

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#713","level":2,"title":"7.1.3   一般的な二分木の種類","text":"","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#1_1","level":3,"title":"1.   充足二分木","text":"

以下の図に示すように、充足二分木(perfect binary tree)ではすべてのレベルのノードが完全に埋まっています。充足二分木では、葉ノードの次数は \\(0\\) で、それ以外のすべてのノードの次数は \\(2\\) です。木の高さを \\(h\\) とすると、ノード総数は \\(2^{h+1} - 1\\) となり、標準的な指数関係を示して、自然界でよく見られる細胞分裂の現象を反映しています。

Tip

なお、中国語圏では充足二分木は満二分木と呼ばれることもあります。

図 7-4   充足二分木

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#2_1","level":3,"title":"2.   完全二分木","text":"

以下の図に示すように、完全二分木(complete binary tree)では最下層のノードだけが完全に埋まっていなくてもよく、しかも最下層のノードは左から右へ連続して詰められていなければなりません。なお、充足二分木も完全二分木の一種です。

図 7-5   完全二分木

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#3","level":3,"title":"3.   充満二分木","text":"

以下の図に示すように、充満二分木(full binary tree)では、葉ノードを除くすべてのノードが 2 つの子ノードを持ちます。

図 7-6   充満二分木

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#4","level":3,"title":"4.   平衡二分木","text":"

以下の図に示すように、平衡二分木(balanced binary tree)では、任意のノードについて左部分木と右部分木の高さの差の絶対値が 1 を超えません。

図 7-7   平衡二分木

","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree/#714","level":2,"title":"7.1.4   二分木の退化","text":"

以下の図は、二分木の理想的な構造と退化した構造を示しています。二分木の各レベルのノードがすべて埋まっていると「充足二分木」となり、すべてのノードが片側に偏ると二分木は「連結リスト」へ退化します。

  • 充足二分木は理想的なケースであり、二分木の「分割統治」の利点を十分に発揮できます。
  • 連結リストはその対極にあり、各種操作はすべて線形操作となり、時間計算量は \\(O(n)\\) まで退化します。

図 7-8   二分木の最良構造と最悪構造

以下の表に示すように、最良構造と最悪構造では、二分木の葉ノード数、ノード総数、高さなどが極大または極小になります。

表 7-1   二分木の最良構造と最悪構造

充足二分木 連結リスト 第 \\(i\\) レベルのノード数 \\(2^{i-1}\\) \\(1\\) 高さ \\(h\\) の木の葉ノード数 \\(2^h\\) \\(1\\) 高さ \\(h\\) の木のノード総数 \\(2^{h+1} - 1\\) \\(h + 1\\) ノード総数 \\(n\\) の木の高さ \\(\\log_2 (n+1) - 1\\) \\(n - 1\\)","path":["第 7 章   木","7.1   二分木"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/","level":1,"title":"7.2   二分木の走査","text":"

物理構造の観点から見ると、木は連結リストを基盤としたデータ構造であり、その走査はポインタを通じてノードへ順にアクセスすることで行われます。しかし、木は非線形データ構造であるため、木の走査は連結リストの走査よりも複雑であり、検索アルゴリズムを用いて実現する必要があります。

二分木の一般的な走査方法には、レベル順走査、先行順走査、中間順走査、後行順走査などがあります。

","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#721","level":2,"title":"7.2.1   レベル順走査","text":"

次の図に示すように、レベル順走査(level-order traversal)では、二分木を上から下へ層ごとに走査し、各層では左から右の順にノードへアクセスします。

レベル順走査は本質的に幅優先走査(breadth-first traversal)に属し、幅優先探索(breadth-first search, BFS)とも呼ばれます。これは「同心円状に外側へ広がる」ような、層ごとの走査方法を表しています。

図 7-9   二分木のレベル順走査

","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1","level":3,"title":"1.   コードの実装","text":"

幅優先走査は通常「キュー」を用いて実装します。キューは「先入れ先出し」の規則に従い、幅優先走査は「層ごとに進む」という規則に従います。両者の背後にある考え方は一致しています。実装コードは次のとおりです:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_bfs.py
def level_order(root: TreeNode | None) -> list[int]:\n    \"\"\"レベル順走査\"\"\"\n    # キューを初期化し、ルートノードを追加する\n    queue: deque[TreeNode] = deque()\n    queue.append(root)\n    # 走査順序を保存するためのリストを初期化する\n    res = []\n    while queue:\n        node: TreeNode = queue.popleft()  # デキュー\n        res.append(node.val)  # ノードの値を保存する\n        if node.left is not None:\n            queue.append(node.left)  # 左子ノードをキューに追加\n        if node.right is not None:\n            queue.append(node.right)  # 右子ノードをキューに追加\n    return res\n
binary_tree_bfs.cpp
/* レベル順走査 */\nvector<int> levelOrder(TreeNode *root) {\n    // キューを初期化し、ルートノードを追加する\n    queue<TreeNode *> queue;\n    queue.push(root);\n    // 走査順序を保存するためのリストを初期化する\n    vector<int> vec;\n    while (!queue.empty()) {\n        TreeNode *node = queue.front();\n        queue.pop();              // デキュー\n        vec.push_back(node->val); // ノードの値を保存する\n        if (node->left != nullptr)\n            queue.push(node->left); // 左子ノードをキューに追加\n        if (node->right != nullptr)\n            queue.push(node->right); // 右子ノードをキューに追加\n    }\n    return vec;\n}\n
binary_tree_bfs.java
/* レベル順走査 */\nList<Integer> levelOrder(TreeNode root) {\n    // キューを初期化し、ルートノードを追加する\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.add(root);\n    // 走査順序を保存するためのリストを初期化する\n    List<Integer> list = new ArrayList<>();\n    while (!queue.isEmpty()) {\n        TreeNode node = queue.poll(); // デキュー\n        list.add(node.val);           // ノードの値を保存する\n        if (node.left != null)\n            queue.offer(node.left);   // 左子ノードをキューに追加\n        if (node.right != null)\n            queue.offer(node.right);  // 右子ノードをキューに追加\n    }\n    return list;\n}\n
binary_tree_bfs.cs
/* レベル順走査 */\nList<int> LevelOrder(TreeNode root) {\n    // キューを初期化し、ルートノードを追加する\n    Queue<TreeNode> queue = new();\n    queue.Enqueue(root);\n    // 走査順序を保存するためのリストを初期化する\n    List<int> list = [];\n    while (queue.Count != 0) {\n        TreeNode node = queue.Dequeue(); // デキュー\n        list.Add(node.val!.Value);       // ノードの値を保存する\n        if (node.left != null)\n            queue.Enqueue(node.left);    // 左子ノードをキューに追加\n        if (node.right != null)\n            queue.Enqueue(node.right);   // 右子ノードをキューに追加\n    }\n    return list;\n}\n
binary_tree_bfs.go
/* レベル順走査 */\nfunc levelOrder(root *TreeNode) []any {\n    // キューを初期化し、ルートノードを追加する\n    queue := list.New()\n    queue.PushBack(root)\n    // 走査順を保存するためのスライスを初期化する\n    nums := make([]any, 0)\n    for queue.Len() > 0 {\n        // デキュー\n        node := queue.Remove(queue.Front()).(*TreeNode)\n        // ノードの値を保存する\n        nums = append(nums, node.Val)\n        if node.Left != nil {\n            // 左子ノードをキューに追加\n            queue.PushBack(node.Left)\n        }\n        if node.Right != nil {\n            // 右子ノードをキューに追加\n            queue.PushBack(node.Right)\n        }\n    }\n    return nums\n}\n
binary_tree_bfs.swift
/* レベル順走査 */\nfunc levelOrder(root: TreeNode) -> [Int] {\n    // キューを初期化し、ルートノードを追加する\n    var queue: [TreeNode] = [root]\n    // 走査順序を保存するためのリストを初期化する\n    var list: [Int] = []\n    while !queue.isEmpty {\n        let node = queue.removeFirst() // デキュー\n        list.append(node.val) // ノードの値を保存する\n        if let left = node.left {\n            queue.append(left) // 左子ノードをキューに追加\n        }\n        if let right = node.right {\n            queue.append(right) // 右子ノードをキューに追加\n        }\n    }\n    return list\n}\n
binary_tree_bfs.js
/* レベル順走査 */\nfunction levelOrder(root) {\n    // キューを初期化し、ルートノードを追加する\n    const queue = [root];\n    // 走査順序を保存するためのリストを初期化する\n    const list = [];\n    while (queue.length) {\n        let node = queue.shift(); // デキュー\n        list.push(node.val); // ノードの値を保存する\n        if (node.left) queue.push(node.left); // 左子ノードをキューに追加\n        if (node.right) queue.push(node.right); // 右子ノードをキューに追加\n    }\n    return list;\n}\n
binary_tree_bfs.ts
/* レベル順走査 */\nfunction levelOrder(root: TreeNode | null): number[] {\n    // キューを初期化し、ルートノードを追加する\n    const queue = [root];\n    // 走査順序を保存するためのリストを初期化する\n    const list: number[] = [];\n    while (queue.length) {\n        let node = queue.shift() as TreeNode; // デキュー\n        list.push(node.val); // ノードの値を保存する\n        if (node.left) {\n            queue.push(node.left); // 左子ノードをキューに追加\n        }\n        if (node.right) {\n            queue.push(node.right); // 右子ノードをキューに追加\n        }\n    }\n    return list;\n}\n
binary_tree_bfs.dart
/* レベル順走査 */\nList<int> levelOrder(TreeNode? root) {\n  // キューを初期化し、ルートノードを追加する\n  Queue<TreeNode?> queue = Queue();\n  queue.add(root);\n  // 走査順序を保存するためのリストを初期化する\n  List<int> res = [];\n  while (queue.isNotEmpty) {\n    TreeNode? node = queue.removeFirst(); // デキュー\n    res.add(node!.val); // ノードの値を保存する\n    if (node.left != null) queue.add(node.left); // 左子ノードをキューに追加\n    if (node.right != null) queue.add(node.right); // 右子ノードをキューに追加\n  }\n  return res;\n}\n
binary_tree_bfs.rs
/* レベル順走査 */\nfn level_order(root: &Rc<RefCell<TreeNode>>) -> Vec<i32> {\n    // キューを初期化し、ルートノードを追加する\n    let mut que = VecDeque::new();\n    que.push_back(root.clone());\n    // 走査順序を保存するためのリストを初期化する\n    let mut vec = Vec::new();\n\n    while let Some(node) = que.pop_front() {\n        // デキュー\n        vec.push(node.borrow().val); // ノードの値を保存する\n        if let Some(left) = node.borrow().left.as_ref() {\n            que.push_back(left.clone()); // 左子ノードをキューに追加\n        }\n        if let Some(right) = node.borrow().right.as_ref() {\n            que.push_back(right.clone()); // 右子ノードをキューに追加\n        };\n    }\n    vec\n}\n
binary_tree_bfs.c
/* レベル順走査 */\nint *levelOrder(TreeNode *root, int *size) {\n    /* 補助キュー */\n    int front, rear;\n    int index, *arr;\n    TreeNode *node;\n    TreeNode **queue;\n\n    /* 補助キュー */\n    queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE);\n    // キューへのポインタ\n    front = 0, rear = 0;\n    // 根ノードを追加する\n    queue[rear++] = root;\n    // 走査順序を保存するためのリストを初期化する\n    /* 補助配列 */\n    arr = (int *)malloc(sizeof(int) * MAX_SIZE);\n    // 配列ポインタ\n    index = 0;\n    while (front < rear) {\n        // デキュー\n        node = queue[front++];\n        // ノードの値を保存する\n        arr[index++] = node->val;\n        if (node->left != NULL) {\n            // 左子ノードをキューに追加\n            queue[rear++] = node->left;\n        }\n        if (node->right != NULL) {\n            // 右子ノードをキューに追加\n            queue[rear++] = node->right;\n        }\n    }\n    // 配列長の値を更新\n    *size = index;\n    arr = realloc(arr, sizeof(int) * (*size));\n\n    // 補助配列の領域を解放する\n    free(queue);\n    return arr;\n}\n
binary_tree_bfs.kt
/* レベル順走査 */\nfun levelOrder(root: TreeNode?): MutableList<Int> {\n    // キューを初期化し、ルートノードを追加する\n    val queue = LinkedList<TreeNode?>()\n    queue.add(root)\n    // 走査順序を保存するためのリストを初期化する\n    val list = mutableListOf<Int>()\n    while (queue.isNotEmpty()) {\n        val node = queue.poll()      // デキュー\n        list.add(node?._val!!)       // ノードの値を保存する\n        if (node.left != null)\n            queue.offer(node.left)   // 左子ノードをキューに追加\n        if (node.right != null)\n            queue.offer(node.right)  // 右子ノードをキューに追加\n    }\n    return list\n}\n
binary_tree_bfs.rb
### レベル順走査 ###\ndef level_order(root)\n  # キューを初期化し、ルートノードを追加する\n  queue = [root]\n  # 走査順序を保存するためのリストを初期化する\n  res = []\n  while !queue.empty?\n    node = queue.shift # デキュー\n    res << node.val # ノードの値を保存する\n    queue << node.left unless node.left.nil? # 左子ノードをキューに追加\n    queue << node.right unless node.right.nil? # 右子ノードをキューに追加\n  end\n  res\nend\n
コードの可視化

全画面で見る >

","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2","level":3,"title":"2.   計算量","text":"
  • 時間計算量は \\(O(n)\\) :すべてのノードを1回ずつ訪問するため、計算量は \\(O(n)\\) です。ここで、\\(n\\) はノード数です。
  • 空間計算量は \\(O(n)\\) :最悪の場合、すなわち完全二分木では、最下層に到達する前に、キュー内には最大で同時に \\((n + 1) / 2\\) 個のノードが存在し、\\(O(n)\\) の空間を使用します。
","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#722","level":2,"title":"7.2.2   先行順・中間順・後行順走査","text":"

同様に、先行順・中間順・後行順走査はいずれも深度優先走査(depth-first traversal)に属し、深度優先探索(depth-first search, DFS)とも呼ばれます。これは「まず行き止まりまで進み、その後で戻って続ける」という走査方法を表しています。

次の図は、二分木に対して深度優先走査を行う仕組みを示しています。深度優先走査は、二分木全体の外周をぐるりと「一周する」ようなものです。各ノードでは3つの位置に出会い、それぞれが先行順走査・中間順走査・後行順走査に対応します。

図 7-10   二分探索木の先行順・中間順・後行順走査

","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1_1","level":3,"title":"1.   コードの実装","text":"

深度優先探索は通常、再帰に基づいて実装されます:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_dfs.py
def pre_order(root: TreeNode | None):\n    \"\"\"先行順走査\"\"\"\n    if root is None:\n        return\n    # 訪問順序:根ノード -> 左部分木 -> 右部分木\n    res.append(root.val)\n    pre_order(root=root.left)\n    pre_order(root=root.right)\n\ndef in_order(root: TreeNode | None):\n    \"\"\"中順走査\"\"\"\n    if root is None:\n        return\n    # 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    in_order(root=root.left)\n    res.append(root.val)\n    in_order(root=root.right)\n\ndef post_order(root: TreeNode | None):\n    \"\"\"後順走査\"\"\"\n    if root is None:\n        return\n    # 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    post_order(root=root.left)\n    post_order(root=root.right)\n    res.append(root.val)\n
binary_tree_dfs.cpp
/* 先行順走査 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    vec.push_back(root->val);\n    preOrder(root->left);\n    preOrder(root->right);\n}\n\n/* 中順走査 */\nvoid inOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root->left);\n    vec.push_back(root->val);\n    inOrder(root->right);\n}\n\n/* 後順走査 */\nvoid postOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root->left);\n    postOrder(root->right);\n    vec.push_back(root->val);\n}\n
binary_tree_dfs.java
/* 先行順走査 */\nvoid preOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.add(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* 中順走査 */\nvoid inOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root.left);\n    list.add(root.val);\n    inOrder(root.right);\n}\n\n/* 後順走査 */\nvoid postOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root.left);\n    postOrder(root.right);\n    list.add(root.val);\n}\n
binary_tree_dfs.cs
/* 先行順走査 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) return;\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.Add(root.val!.Value);\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n\n/* 中順走査 */\nvoid InOrder(TreeNode? root) {\n    if (root == null) return;\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    InOrder(root.left);\n    list.Add(root.val!.Value);\n    InOrder(root.right);\n}\n\n/* 後順走査 */\nvoid PostOrder(TreeNode? root) {\n    if (root == null) return;\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    PostOrder(root.left);\n    PostOrder(root.right);\n    list.Add(root.val!.Value);\n}\n
binary_tree_dfs.go
/* 先行順走査 */\nfunc preOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    nums = append(nums, node.Val)\n    preOrder(node.Left)\n    preOrder(node.Right)\n}\n\n/* 中順走査 */\nfunc inOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(node.Left)\n    nums = append(nums, node.Val)\n    inOrder(node.Right)\n}\n\n/* 後順走査 */\nfunc postOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(node.Left)\n    postOrder(node.Right)\n    nums = append(nums, node.Val)\n}\n
binary_tree_dfs.swift
/* 先行順走査 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.append(root.val)\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n\n/* 中順走査 */\nfunc inOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root: root.left)\n    list.append(root.val)\n    inOrder(root: root.right)\n}\n\n/* 後順走査 */\nfunc postOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root: root.left)\n    postOrder(root: root.right)\n    list.append(root.val)\n}\n
binary_tree_dfs.js
/* 先行順走査 */\nfunction preOrder(root) {\n    if (root === null) return;\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* 中順走査 */\nfunction inOrder(root) {\n    if (root === null) return;\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* 後順走査 */\nfunction postOrder(root) {\n    if (root === null) return;\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
binary_tree_dfs.ts
/* 先行順走査 */\nfunction preOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* 中順走査 */\nfunction inOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* 後順走査 */\nfunction postOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
binary_tree_dfs.dart
/* 先行順走査 */\nvoid preOrder(TreeNode? node) {\n  if (node == null) return;\n  // 訪問順序:根ノード -> 左部分木 -> 右部分木\n  list.add(node.val);\n  preOrder(node.left);\n  preOrder(node.right);\n}\n\n/* 中順走査 */\nvoid inOrder(TreeNode? node) {\n  if (node == null) return;\n  // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n  inOrder(node.left);\n  list.add(node.val);\n  inOrder(node.right);\n}\n\n/* 後順走査 */\nvoid postOrder(TreeNode? node) {\n  if (node == null) return;\n  // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n  postOrder(node.left);\n  postOrder(node.right);\n  list.add(node.val);\n}\n
binary_tree_dfs.rs
/* 先行順走査 */\nfn pre_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // 訪問順序:根ノード -> 左部分木 -> 右部分木\n            let node = node.borrow();\n            res.push(node.val);\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* 中順走査 */\nfn in_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            res.push(node.val);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* 後順走査 */\nfn post_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n            res.push(node.val);\n        }\n    }\n\n    dfs(root, &mut result);\n\n    result\n}\n
binary_tree_dfs.c
/* 先行順走査 */\nvoid preOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    arr[(*size)++] = root->val;\n    preOrder(root->left, size);\n    preOrder(root->right, size);\n}\n\n/* 中順走査 */\nvoid inOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root->left, size);\n    arr[(*size)++] = root->val;\n    inOrder(root->right, size);\n}\n\n/* 後順走査 */\nvoid postOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root->left, size);\n    postOrder(root->right, size);\n    arr[(*size)++] = root->val;\n}\n
binary_tree_dfs.kt
/* 先行順走査 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) return\n    // 訪問順序:根ノード -> 左部分木 -> 右部分木\n    list.add(root._val)\n    preOrder(root.left)\n    preOrder(root.right)\n}\n\n/* 中順走査 */\nfun inOrder(root: TreeNode?) {\n    if (root == null) return\n    // 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n    inOrder(root.left)\n    list.add(root._val)\n    inOrder(root.right)\n}\n\n/* 後順走査 */\nfun postOrder(root: TreeNode?) {\n    if (root == null) return\n    // 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n    postOrder(root.left)\n    postOrder(root.right)\n    list.add(root._val)\n}\n
binary_tree_dfs.rb
### 前順走査 ###\ndef pre_order(root)\n  return if root.nil?\n\n  # 訪問順序:根ノード -> 左部分木 -> 右部分木\n  $res << root.val\n  pre_order(root.left)\n  pre_order(root.right)\nend\n\n### 中順走査 ###\ndef in_order(root)\n  return if root.nil?\n\n  # 訪問優先順: 左部分木 -> 根ノード -> 右部分木\n  in_order(root.left)\n  $res << root.val\n  in_order(root.right)\nend\n\n### 後順走査 ###\ndef post_order(root)\n  return if root.nil?\n\n  # 訪問優先順: 左部分木 -> 右部分木 -> 根ノード\n  post_order(root.left)\n  post_order(root.right)\n  $res << root.val\nend\n
コードの可視化

全画面で見る >

Tip

深度優先探索は反復によって実装することもできます。興味のある読者は自身で調べてみてください。

次の図は、二分木の先行順走査における再帰の過程を示しており、「行き」と「帰り」という2つの逆向きの部分に分けられます。

  1. 「行き」は新しいメソッドの開始を表し、この過程でプログラムは次のノードにアクセスします。
  2. 「帰り」は関数の復帰を表し、現在のノードへのアクセスが完了したことを意味します。
<1><2><3><4><5><6><7><8><9><10><11>

図 7-11   先行順走査の再帰過程

","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2_1","level":3,"title":"2.   計算量","text":"
  • 時間計算量は \\(O(n)\\) :すべてのノードを1回ずつ訪問するため、計算量は \\(O(n)\\) です。
  • 空間計算量は \\(O(n)\\) :最悪の場合、すなわち木が連結リストに退化したとき、再帰の深さは \\(n\\) に達し、システムは \\(O(n)\\) のスタックフレーム空間を使用します。
","path":["第 7 章   木","7.2   二分木の走査"],"tags":[]},{"location":"chapter_tree/summary/","level":1,"title":"7.6   まとめ","text":"","path":["第 7 章   木","7.6   まとめ"],"tags":[]},{"location":"chapter_tree/summary/#1","level":3,"title":"1.   要点の振り返り","text":"
  • 二分木は非線形データ構造の一種であり、「二分する」分割統治の考え方を体現している。各二分木ノードは 1 つの値と 2 本のポインタを持ち、それぞれ左子ノードと右子ノードを指す。
  • 二分木のあるノードについて、その左(右)子ノードおよびその配下から構成される木を、そのノードの左(右)部分木と呼ぶ。
  • 二分木に関する用語には、根ノード、葉ノード、レベル、次数、辺、高さ、深さなどがある。
  • 二分木の初期化、ノードの挿入、ノードの削除は、連結リストの操作方法と似ている。
  • 一般的な二分木の種類には、perfect 二分木、complete 二分木、full 二分木、平衡二分木がある。perfect 二分木が最も理想的な状態であり、連結リストは退化後の最悪の状態である。
  • 二分木は配列で表現できる。方法としては、ノード値と空き位置をレベル順走査の順に並べ、親ノードと子ノードのインデックス対応関係に基づいてポインタを実現する。
  • 二分木のレベル順走査は幅優先探索の一種であり、「同心円状に外へ広がる」ような逐次的な走査方式を表しており、通常はキューによって実装される。
  • 前順、中順、後順走査はいずれも深さ優先探索に属し、「まず末端まで進み、その後バックトラックして続ける」という走査方式を体現しており、通常は再帰で実装される。
  • 二分探索木は効率的な要素探索データ構造であり、探索、挿入、削除の時間計算量はいずれも \\(O(\\log n)\\) である。二分探索木が連結リストへ退化すると、各操作の時間計算量は \\(O(n)\\) まで悪化する。
  • AVL 木は平衡二分探索木とも呼ばれ、回転操作によって、ノードの挿入と削除を繰り返した後も木が平衡を保つようにしている。
  • AVL 木の回転操作には、右回転、左回転、右回転してから左回転、左回転してから右回転がある。ノードの挿入または削除の後、AVL 木は下から上へ回転操作を行い、木を再び平衡状態に戻す。
","path":["第 7 章   木","7.6   まとめ"],"tags":[]},{"location":"chapter_tree/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:ノードが 1 つしかない二分木では、木の高さと根ノードの深さはどちらも \\(0\\) ですか?

はい。高さと深さは通常「通過した辺の本数」として定義されるからです。

Q:二分木における挿入と削除は通常一連の操作を組み合わせて完了しますが、ここでいう「一連の操作」とは何を指すのでしょうか?リソースの子ノードに対するリソース解放と理解できますか?

二分探索木を例にすると、ノード削除は 3 つのケースに分けて処理する必要があり、各ケースで複数段階のノード操作が必要になります。

Q:なぜ DFS による二分木走査には前順・中順・後順の 3 種類があり、それぞれどのような用途があるのですか?

配列の順方向走査と逆方向走査に似て、前順・中順・後順走査は二分木の 3 つの走査方法であり、特定の順序で走査結果を得るために使えます。たとえば二分探索木では、ノードの大小関係が 左子ノードの値 < 根ノードの値 < 右子ノードの値 を満たすため、「左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右」の優先順で木を走査すれば、整列済みのノード列を得られます。

Q:右回転操作は不平衡ノード nodechildgrand_child の関係を処理するものですが、node の親ノードと node の元の接続は維持しなくてよいのですか?右回転後に切れてしまいませんか?

この問題は再帰の視点から考える必要があります。右回転操作 right_rotate(root) に渡されるのは部分木の根ノードであり、最終的に return child によって回転後の部分木の根ノードを返します。部分木の根ノードとその親ノードの接続は、この関数の返却後に行われるため、右回転操作自身が管理する範囲には含まれません。

Q:C++ では関数を privatepublic に分けますが、この設計にはどのような考えがありますか?なぜ height() 関数と updateHeight() 関数をそれぞれ publicprivate に置くのですか?

主に、そのメソッドの利用範囲を見て決めます。メソッドがクラス内部でしか使われないなら、private に設計します。たとえば、利用者が updateHeight() を単独で呼び出しても意味はなく、これは挿入や削除の途中の 1 ステップにすぎません。一方で height() はノードの高さにアクセスするためのもので、vector.size() に似た役割を持つため、使いやすいように public に設定します。

Q:入力データの集合から二分探索木をどのように構築しますか?根ノードの選び方は重要ですか?

はい。木の構築方法は、二分探索木のコード中の build_tree() メソッドですでに示されています。根ノードの選択については、通常は入力データをソートし、その中央の要素を根ノードにしてから、左右の部分木を再帰的に構築します。こうすることで、木の平衡性を最大限に保てます。

Q:Java では、文字列比較には必ず equals() メソッドを使うべきですか?

Java では、基本データ型については == を使って 2 つの変数の値が等しいかどうかを比較します。参照型については、この 2 つの記法の働き方は異なります。

  • == :2 つの変数が同じオブジェクトを指しているか、つまりメモリ上の位置が同じかどうかを比較するために使います。
  • equals():2 つのオブジェクトの値が等しいかどうかを比較するために使います。

したがって、値を比較したい場合は equals() を使うべきです。ただし、String a = \"hi\"; String b = \"hi\"; によって初期化された文字列は文字列定数プールに格納され、同じオブジェクトを指すため、a == b でも 2 つの文字列の内容を比較できます。

Q:幅優先走査で最下層に到達する前、キュー内のノード数は \\(2^h\\) ですか?

はい。たとえば高さ \\(h = 2\\) の充足二分木では、ノード総数は \\(n = 7\\) であり、最下層のノード数は \\(4 = 2^h = (n + 1) / 2\\) です。

","path":["第 7 章   木","7.6   まとめ"],"tags":[]}]} \ No newline at end of file diff --git a/ja/stylesheets/extra.css b/ja/stylesheets/extra.css index ad77de182..68bd0fb96 100644 --- a/ja/stylesheets/extra.css +++ b/ja/stylesheets/extra.css @@ -1,4 +1,4 @@ -/* Color Settings */ +/* Theme tokens */ /* https://github.com/squidfunk/mkdocs-material/blob/6b5035f5580f97532d664e3d1babf5f320e88ee9/src/assets/stylesheets/main/_colors.scss */ /* https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#custom-colors */ :root>* { @@ -14,21 +14,23 @@ --md-code-fg-color: #1d1d20; --md-code-bg-color: #f5f5f5; - --md-accent-fg-color: #999; - --md-typeset-color: #1d1d20; --md-typeset-a-color: #349890; + --md-accent-fg-color: var(--md-typeset-a-color); + --md-typeset-btn-color: #55aea6; --md-typeset-btn-hover-color: #52bbb1; --md-admonition-icon--pythontutor: url('data:image/svg+xml;charset=utf-8,'); - --md-admonition-pythontutor-color: #eee; + --md-admonition-pythontutor-color: var(--md-code-bg-color); + + --hello-algo-sidebar-width: 13rem; } [data-md-color-scheme="slate"] { - --theme-dark-base: #1E1E1E; - --theme-dark-mantle: #1A1A1A; + --theme-dark-base: #1e1e1e; + --theme-dark-mantle: #1a1a1a; --theme-dark-crust: #171717; --hero-starfield-bg-color: var(--theme-dark-base); @@ -37,25 +39,25 @@ --md-default-fg-color: #adbac7; --md-default-bg-color: var(--theme-dark-base); + --md-default-bg-color--light: rgb(30 30 30 / 0.8); - --md-body-bg-color: var(--theme-dark-mantle); + --md-body-bg-color: var(--md-default-bg-color); --md-header-bg-color: rgba(26, 26, 26, 0.8); --md-code-fg-color: #adbac7; --md-code-bg-color: var(--theme-dark-crust); - --md-accent-fg-color: #aaa; - - --md-footer-fg-color: #adbac7; - --md-footer-bg-color: var(--theme-dark-mantle); - --md-typeset-color: #adbac7; --md-typeset-a-color: #52bbb1; + --md-accent-fg-color: var(--md-typeset-a-color); --md-typeset-btn-color: #52bbb1; --md-typeset-btn-hover-color: #55aea6; - --md-admonition-pythontutor-color: var(--theme-dark-crust); + --md-footer-fg-color: #adbac7; + --md-footer-bg-color: var(--theme-dark-mantle); + + --md-admonition-pythontutor-color: var(--md-code-bg-color); } [data-md-color-scheme="slate"][data-md-color-primary="black"], @@ -63,24 +65,82 @@ --md-typeset-a-color: #52bbb1; } +/* Base layout */ +body { + background-color: var(--md-default-bg-color); + --md-text-font-family: -apple-system, BlinkMacSystemFont, + var(--md-text-font, _), Helvetica, Arial, sans-serif; + --md-code-font-family: var(--md-code-font, _), SFMono-Regular, Consolas, Menlo, + -apple-system, BlinkMacSystemFont, var(--md-text-font, _), monospace; +} + +html:has(body[data-md-color-scheme="slate"]) { + background-color: #1e1e1e; +} + +html:has(body[data-md-color-scheme="default"]) { + background-color: #ffffff; +} + +@media screen and (min-width: 76.25em) { + .md-grid { + max-width: calc(61rem + 2 * (var(--hello-algo-sidebar-width) - 12.1rem)); + } + + .md-sidebar--primary, + .md-sidebar--secondary { + width: var(--hello-algo-sidebar-width); + } + + [dir="ltr"] .md-sidebar__inner { + padding-right: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); + } + + [dir="rtl"] .md-sidebar__inner { + padding-left: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); + } +} + +.md-sidebar--primary .md-sidebar__scrollwrap { + scrollbar-color: var(--md-default-fg-color--lighter) transparent; +} + +.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb, +.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: var(--md-default-fg-color--lighter); +} + +/* Banner and footer */ +.md-banner { + background-color: var(--md-default-bg-color); + color: var(--md-default-fg-color); + font-size: 0.75rem; +} + +.md-banner .banner-svg svg { + margin-right: 0.3rem; + height: 0.63rem; + fill: var(--md-default-fg-color); +} + +.md-footer, +.md-footer__inner, +.md-footer-meta, +.md-footer__link, +.md-footer__link:hover { + background-color: var(--md-default-bg-color); +} + +.md-footer { + border-top: 0.05rem solid var(--md-default-fg-color--lightest); +} + [data-md-color-scheme="slate"] .md-footer, -[data-md-color-scheme="slate"] .md-footer__inner { - background-color: var(--theme-dark-mantle); - color: var(--md-footer-fg-color); -} - -[data-md-color-scheme="slate"] .md-footer-meta { - background-color: var(--theme-dark-crust); - color: var(--md-footer-fg-color); -} - -[data-md-color-scheme="slate"] .md-footer__link { - background-color: var(--theme-dark-crust); - color: var(--md-footer-fg-color); -} - +[data-md-color-scheme="slate"] .md-footer__inner, +[data-md-color-scheme="slate"] .md-footer-meta, +[data-md-color-scheme="slate"] .md-footer__link, [data-md-color-scheme="slate"] .md-footer__link:hover { - background-color: var(--theme-dark-base); + color: var(--md-footer-fg-color); } [data-md-color-scheme="slate"] .md-footer__title, @@ -93,40 +153,31 @@ color: var(--md-footer-fg-color); } -/* https://github.com/squidfunk/mkdocs-material/issues/4832#issuecomment-1374891676 */ -.md-nav__link[for] { - color: var(--md-default-fg-color) !important; -} - -/* Figure class */ +/* Shared content elements */ .animation-figure { border-radius: 0.3rem; display: block; margin: 0 auto; - box-shadow: var(--md-shadow-z2); + box-shadow: 0 0.03rem 0.16rem rgb(0 0 0 / 0.07); } -/* Cover image class */ .cover-image { width: 28rem; height: auto; border-radius: 0.3rem; display: block; margin: 0 auto; - box-shadow: var(--md-shadow-z2); + box-shadow: 0 0.03rem 0.16rem rgb(0 0 0 / 0.07); } -/* Center Markdown Tables (requires md_in_html extension) */ .center-table { text-align: center; } -/* Reset alignment for table cells */ .md-typeset .center-table :is(td, th):not([align]) { text-align: initial; } -/* Font size */ .md-typeset { font-size: 0.75rem; line-height: 1.5; @@ -136,7 +187,6 @@ font-size: 0.95em; } -/* Markdown Header */ /* https://github.com/squidfunk/mkdocs-material/blob/dcab57dd1cced4b77875c1aa1b53467c62709d31/src/assets/stylesheets/main/_typeset.scss */ .md-typeset h1 { font-weight: 400; @@ -155,11 +205,6 @@ text-transform: none; } -.md-typeset a:hover { - color: var(--md-typeset-a-color); - text-decoration: underline; -} - .md-typeset code { border-radius: 0.2rem; } @@ -168,21 +213,11 @@ font-weight: normal; } -/* font-family setting for Win10 */ -body { - --md-text-font-family: -apple-system, BlinkMacSystemFont, - var(--md-text-font, _), Helvetica, Arial, sans-serif; - --md-code-font-family: var(--md-code-font, _), SFMono-Regular, Consolas, Menlo, - -apple-system, BlinkMacSystemFont, var(--md-text-font, _), monospace; -} - -/* max height of code block */ /* https://github.com/squidfunk/mkdocs-material/issues/3444 */ .md-typeset pre>code { max-height: 25rem; } -/* Keep code block scrollbar hover neutral instead of accent-colored */ .md-typeset pre>code:hover { scrollbar-color: var(--md-default-fg-color--lighter) transparent; } @@ -191,29 +226,48 @@ body { background-color: var(--md-default-fg-color--lighter); } -/* Make the picture not glare in dark theme */ [data-md-color-scheme="slate"] .md-typeset img, [data-md-color-scheme="slate"] .md-typeset svg, [data-md-color-scheme="slate"] .md-typeset video { filter: brightness(0.85) invert(0.05); } -/* landing page */ -.header-img-div { - display: flex; - align-items: center; - justify-content: center; - margin: 0 auto; - width: 100%; - /* Default to full width */ +.md-typeset a:not(.md-button) { + text-decoration: none; +} + +.md-typeset a:not(.md-button):hover, +.md-typeset a:not(.md-button):focus-visible { + color: var(--md-typeset-a-color); + text-decoration: underline; +} + +/* Admonitions and tabs */ +.md-typeset .admonition-title:before, +.md-typeset summary:before, +.md-typeset summary:after { + top: 50%; +} + +.md-typeset .admonition-title:before, +.md-typeset summary:before { + transform: translateY(-50%); +} + +.md-typeset summary:after { + transform: translateY(-50%) rotate(0deg); +} + +.md-typeset details[open]>summary:after { + transform: translateY(-50%) rotate(90deg); } -/* Admonition for python tutor */ .md-typeset .admonition.pythontutor, .md-typeset details.pythontutor { border-color: var(--md-default-fg-color--lightest); margin-top: 0; margin-bottom: 1.5625em; + background-color: var(--md-code-bg-color); } .md-typeset .pythontutor>.admonition-title, @@ -228,26 +282,18 @@ body { mask-image: var(--md-admonition-icon--pythontutor); } -/* code block tabs */ +[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary), +[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary) :is(p, li, strong, em, sub, sup, code, a) { + background-color: #f5f5f5; + color: #1d1d20; +} + .md-typeset .tabbed-labels>label { font-size: 0.61rem; } .md-typeset .tabbed-labels--linked>label>a { - padding: .78125em 1.0em .625em; -} - -/* header banner */ -.md-banner { - background-color: var(--md-code-bg-color); - color: var(--md-default-fg-color); - font-size: 0.75rem; -} - -.md-banner .banner-svg svg { - margin-right: 0.3rem; - height: 0.63rem; - fill: var(--md-default-fg-color); + padding: 0.78125em 1em 0.625em; } .pythontutor-iframe { @@ -260,115 +306,55 @@ body { border: none; } -/* landing page container */ +/* Landing page layout */ +.header-img-div { + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; + width: 100%; +} + .home-div { width: 100%; height: auto; display: flex; justify-content: center; align-items: center; + padding: 3em 2em; background-color: var(--md-default-bg-color); color: var(--md-default-fg-color); font-size: 0.9rem; - padding: 3em 2em; text-align: center; } +.home-div[data-md-color-scheme="default"], +.home-div[data-md-color-scheme="default"] h1, +.home-div[data-md-color-scheme="default"] h2, +.home-div[data-md-color-scheme="default"] h3, +.home-div[data-md-color-scheme="default"] h4, +.home-div[data-md-color-scheme="default"] h5, +.home-div[data-md-color-scheme="default"] h6, +.home-div[data-md-color-scheme="slate"], +.home-div[data-md-color-scheme="slate"] h1, +.home-div[data-md-color-scheme="slate"] h2, +.home-div[data-md-color-scheme="slate"] h3, +.home-div[data-md-color-scheme="slate"] h4, +.home-div[data-md-color-scheme="slate"] h5, +.home-div[data-md-color-scheme="slate"] h6 { + color: var(--md-default-fg-color); +} + .section-content { width: 100%; height: auto; max-width: 70vw; } -/* rounded button */ -.rounded-button { - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 10em; - margin: 0 0.1em; - padding: 0.6em 1.3em; - border: none; - background-color: var(--md-typeset-btn-color); - color: var(--md-primary-fg-color) !important; - text-align: center; - text-decoration: none; - cursor: pointer; -} - -.rounded-button:hover { - background-color: var(--md-typeset-btn-hover-color); -} - -.rounded-button span { - margin: 0; - margin-bottom: 0.07em; - white-space: nowrap; -} - -.rounded-button svg { - fill: var(--md-primary-fg-color); - width: auto; - height: 1.2em; - margin-right: 0.5em; -} - -/* device image */ -.device-on-hover { - width: auto; - transition: transform 0.3s ease-in-out, filter 0.3s ease-in-out; -} - -a:hover .device-on-hover { - filter: drop-shadow(0 0 0.2rem rgba(0, 0, 0, 0.15)); - transform: scale(1.01); -} - -/* text button */ -.reading-media { - display: flex; - justify-content: center; - align-items: flex-end; - height: 32vw; -} - -.media-block { - height: 100%; - margin: 0 0.2em; -} - -.text-button { - width: auto; - color: var(--md-typeset-btn-color); - text-decoration: none; - text-align: center; - margin: 2.7em auto; -} - -.text-button span { - white-space: nowrap; -} - -.text-button svg { - display: inline-block; - fill: var(--md-typeset-btn-color); - width: auto; - height: 0.9em; - background-size: cover; - padding-top: 0.17em; - margin-left: 0.15em; -} - -a:hover .text-button span { - text-decoration: underline; -} - -/* hero image */ .hero-div { height: min(84vh, 75vw); width: min(112vh, 100vw); - margin: 0 auto; - margin-top: -2.4rem; + margin: -2.4rem auto 0; padding: 0; position: relative; font-size: min(1.8vh, 2.5vw); @@ -384,13 +370,12 @@ a:hover .text-button span { } .hero-bg { - height: 100%; width: 100%; + height: 100%; object-fit: cover; position: absolute; } -/* hover on the planets */ .hero-div>a>img { width: auto; position: absolute; @@ -402,7 +387,6 @@ a:hover .text-button span { position: absolute; transform: translateX(-50%) translateY(-50%); white-space: nowrap; - /* prevent line breaks */ color: white; } @@ -412,21 +396,105 @@ a:hover .text-button span { } .hero-div>a:hover>span { - text-decoration: underline; color: var(--md-typeset-btn-color); + text-decoration: underline; } .heading-div { width: 100%; position: absolute; - transform: translateX(-50%); left: 50%; bottom: min(2vh, 3vw); + transform: translateX(-50%); pointer-events: none; color: #fff; } -/* code badge */ +/* Landing page CTAs */ +.rounded-button { + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0 0.1em; + padding: 0.6em 1.3em; + border: none; + border-radius: 10em; + background-color: var(--md-typeset-btn-color); + color: var(--md-primary-fg-color) !important; + text-align: center; + text-decoration: none; + cursor: pointer; +} + +.rounded-button:hover { + background-color: var(--md-typeset-btn-hover-color); +} + +.rounded-button span { + margin: 0 0 0.07em; + white-space: nowrap; +} + +.rounded-button svg { + width: auto; + height: 1.2em; + margin-right: 0.5em; + fill: var(--md-primary-fg-color); +} + +.reading-media { + display: flex; + justify-content: center; + align-items: flex-end; + height: 32vw; +} + +.reading-media+p { + margin-top: 1em !important; +} + +.media-block { + height: 100%; + margin: 0 0.2em; +} + +.text-button { + width: auto; + margin: 2.7em auto; + color: var(--md-typeset-btn-color); + text-align: center; + text-decoration: none; +} + +.text-button span { + white-space: nowrap; +} + +.text-button svg { + display: inline-block; + width: auto; + height: 0.9em; + margin-left: 0.15em; + padding-top: 0.17em; + fill: var(--md-typeset-btn-color); + background-size: cover; +} + +a:hover .text-button span { + text-decoration: underline; +} + +.device-on-hover { + width: auto; + transition: transform 0.3s ease-in-out, filter 0.3s ease-in-out; +} + +a:hover .device-on-hover { + filter: drop-shadow(0 0 0.2rem rgba(0, 0, 0, 0.15)); + transform: scale(1.01); +} + +/* Landing page content blocks */ .code-badge { width: 100%; height: auto; @@ -434,11 +502,10 @@ a:hover .text-button span { } .code-badge img { - height: 1.07em; width: auto; + height: 1.07em; } -/* brief intro */ .intro-container { display: flex; align-items: center; @@ -455,14 +522,13 @@ a:hover .text-button span { .intro-text { flex-grow: 1; - /* fill the space */ display: flex; flex-direction: column; justify-content: center; - text-align: left; align-items: flex-start; width: fit-content; margin: 2em; + text-align: left; } .intro-text>div { @@ -471,6 +537,10 @@ a:hover .text-button span { margin: 0 auto; } +.intro-text svg path { + fill: #3b3b3b; +} + .endor-text { width: 50%; } @@ -480,7 +550,10 @@ a:hover .text-button span { font-weight: bold; } -/* contributors table */ +.home-div .intro-quote { + color: var(--md-default-fg-color--light) !important; +} + .profile-div { display: flex; flex-wrap: wrap; @@ -495,6 +568,11 @@ a:hover .text-button span { text-align: center; } +.profile-cell a:hover b, +.profile-cell a:focus-visible b { + text-decoration: underline; +} + .profile-img { width: 5em; border-radius: 50%; @@ -517,7 +595,37 @@ a:hover .text-button span { margin: 0 auto; } -/* Hide navigation */ +/* Embedded media */ +.video-container { + position: relative; + padding-bottom: 56.25%; + height: 0; +} + +.video-container iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.starfield { + position: absolute; + width: 100%; + height: 100%; + z-index: 0; + background-color: var(--hero-starfield-bg-color, transparent); +} + +.starfield-origin { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +/* Responsive adjustments */ @media screen and (max-width: 76.25em) { .section-content { max-width: 95vw; @@ -532,7 +640,6 @@ a:hover .text-button span { } } -/* mobile devices */ @media screen and (max-width: 60em) { .home-div { font-size: 0.75rem; @@ -571,209 +678,4 @@ a:hover .text-button span { flex: 1 1 30%; } } - -.video-container { - position: relative; - padding-bottom: 56.25%; - /* 16:9 */ - height: 0; -} - -.video-container iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -/* starfield */ -.starfield { - position: absolute; - width: 100%; - height: 100%; - z-index: 0; - background-color: var(--hero-starfield-bg-color, transparent); -} - -.starfield-origin { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -/* Zensical-specific adjustments merged into the main stylesheet. */ -:root>* { - --md-accent-fg-color: var(--md-typeset-a-color); - --md-admonition-pythontutor-color: var(--md-code-bg-color); - --hello-algo-sidebar-width: 13rem; -} - -[data-md-color-scheme="slate"] { - --md-accent-fg-color: var(--md-typeset-a-color); - --md-admonition-pythontutor-color: var(--md-code-bg-color); - --md-body-bg-color: var(--md-default-bg-color); - --md-default-bg-color--light: rgb(30 30 30 / 0.8); -} - -[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary), -[data-md-color-scheme="slate"] .md-typeset details.pythontutor[open]> :not(summary) :is(p, li, strong, em, sub, sup, code, a) { - background-color: #f5f5f5; - color: #1d1d20; -} - -body { - background-color: var(--md-default-bg-color); -} - -html:has(body[data-md-color-scheme="slate"]) { - background-color: #1e1e1e; -} - -html:has(body[data-md-color-scheme="default"]) { - background-color: #ffffff; -} - -.home-div[data-md-color-scheme="default"], -.home-div[data-md-color-scheme="default"] h1, -.home-div[data-md-color-scheme="default"] h2, -.home-div[data-md-color-scheme="default"] h3, -.home-div[data-md-color-scheme="default"] h4, -.home-div[data-md-color-scheme="default"] h5, -.home-div[data-md-color-scheme="default"] h6 { - color: var(--md-default-fg-color); -} - -.home-div[data-md-color-scheme="slate"], -.home-div[data-md-color-scheme="slate"] h1, -.home-div[data-md-color-scheme="slate"] h2, -.home-div[data-md-color-scheme="slate"] h3, -.home-div[data-md-color-scheme="slate"] h4, -.home-div[data-md-color-scheme="slate"] h5, -.home-div[data-md-color-scheme="slate"] h6 { - color: var(--md-default-fg-color); -} - -.home-div .intro-quote { - color: var(--md-default-fg-color--light) !important; -} - -.reading-media+p { - margin-top: 1em !important; -} - -.md-typeset .admonition-title:before, -.md-typeset summary:before, -.md-typeset summary:after { - top: 50%; -} - -.md-typeset .admonition-title:before, -.md-typeset summary:before { - transform: translateY(-50%); -} - -.md-typeset summary:after { - transform: translateY(-50%) rotate(0deg); -} - -.md-typeset details[open]>summary:after { - transform: translateY(-50%) rotate(90deg); -} - -.md-nav__link[for] { - color: inherit !important; -} - -.md-nav__link[for].md-nav__link--active { - color: var(--md-accent-fg-color) !important; -} - -@media screen and (min-width: 76.25em) { - .md-grid { - max-width: calc(61rem + 2 * (var(--hello-algo-sidebar-width) - 12.1rem)); - } - - .md-sidebar--primary, - .md-sidebar--secondary { - width: var(--hello-algo-sidebar-width); - } - - [dir="ltr"] .md-sidebar__inner { - padding-right: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); - } - - [dir="rtl"] .md-sidebar__inner { - padding-left: calc(100% - (var(--hello-algo-sidebar-width) - 0.6rem)); - } -} - -.md-sidebar--primary .md-sidebar__scrollwrap { - scrollbar-color: var(--md-default-fg-color--lighter) transparent; -} - -.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb, -.md-sidebar--primary .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { - background-color: var(--md-default-fg-color--lighter); -} - -.md-footer, -.md-footer__inner, -.md-footer-meta, -.md-footer__link, -.md-footer__link:hover { - background-color: var(--md-default-bg-color); -} - -.md-footer { - border-top: 0.05rem solid var(--md-default-fg-color--lightest); -} - -[data-md-color-scheme="slate"] .md-footer, -[data-md-color-scheme="slate"] .md-footer__inner, -[data-md-color-scheme="slate"] .md-footer-meta, -[data-md-color-scheme="slate"] .md-footer__link, -[data-md-color-scheme="slate"] .md-footer__link:hover { - background-color: var(--md-default-bg-color); -} - -.md-banner { - background-color: var(--md-default-bg-color); -} - -.md-typeset .admonition.pythontutor, -.md-typeset details.pythontutor, -.md-typeset .pythontutor>.admonition-title, -.md-typeset .pythontutor>summary { - background-color: var(--md-code-bg-color); -} - -.md-typeset .pythontutor>.admonition-title::before, -.md-typeset .pythontutor>summary::before, -.md-typeset .pythontutor>summary::after { - top: 50%; -} - -.md-typeset .pythontutor>.admonition-title::before, -.md-typeset .pythontutor>summary::before { - transform: translateY(-50%); -} - -.md-typeset .pythontutor>summary::after { - transform: translateY(-50%) rotate(0deg); -} - -.md-typeset details[open].pythontutor>summary::after { - transform: translateY(-50%) rotate(90deg); -} - -.md-typeset a:not(.md-button) { - text-decoration: none; -} - -.md-typeset a:not(.md-button):hover, -.md-typeset a:not(.md-button):focus-visible { - text-decoration: underline; -} -/*! update cache: 20260331044524 */ +/*! update cache: 20260331053217 */ diff --git a/javascripts/katex.js b/javascripts/katex.js index 2e4c4d0af..a112d1080 100644 --- a/javascripts/katex.js +++ b/javascripts/katex.js @@ -8,4 +8,4 @@ document$.subscribe(({ body }) => { ], }); }); -/*! update cache: 20260331044451 */ +/*! update cache: 20260331053143 */ diff --git a/javascripts/mathjax.js b/javascripts/mathjax.js index c5f295fb0..f36d0163c 100644 --- a/javascripts/mathjax.js +++ b/javascripts/mathjax.js @@ -15,4 +15,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise(); }); -/*! update cache: 20260331044451 */ +/*! update cache: 20260331053143 */ diff --git a/javascripts/starfield.js b/javascripts/starfield.js index 4108751da..5fc0cc80a 100644 --- a/javascripts/starfield.js +++ b/javascripts/starfield.js @@ -469,4 +469,4 @@ return Starfield; }); -/*! update cache: 20260331044451 */ +/*! update cache: 20260331053143 */ diff --git a/ru/404.html b/ru/404.html index 2bde90cae..39734f6fe 100644 --- a/ru/404.html +++ b/ru/404.html @@ -567,7 +567,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -589,7 +589,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -639,7 +639,7 @@ - 1.2 Что такое структуры данных и алгоритмы + 1.2 Что такое алгоритм @@ -1174,7 +1174,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1196,7 +1196,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1302,7 +1302,7 @@ - 4.4 Память и кеш * + 4.4 Оперативная память и кэш * @@ -1584,7 +1584,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1606,7 +1606,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1684,7 +1684,7 @@ - 6.3 Хеш-алгоритмы + 6.3 Алгоритмы хеширования @@ -1779,7 +1779,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1801,7 +1801,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1879,7 +1879,7 @@ - 7.3 Представление дерева массивом + 7.3 Представление двоичного дерева массивом @@ -1963,7 +1963,7 @@ - 7.6 Резюме + 7.6 Краткие итоги @@ -2126,7 +2126,7 @@ - 8.3 Задача Top-K + 8.3 Задача Top-k @@ -2217,7 +2217,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2239,7 +2239,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2289,7 +2289,7 @@ - 9.2 Базовые операции над графами + 9.2 Базовые операции графа @@ -2345,7 +2345,7 @@ - 9.4 Резюме + 9.4 Краткие итоги @@ -2484,7 +2484,7 @@ - 10.2 Точка вставки двоичного поиска + 10.2 Двоичный поиск точки вставки @@ -2512,7 +2512,7 @@ - 10.3 Граничные случаи двоичного поиска + 10.3 Двоичный поиск границ @@ -2540,7 +2540,7 @@ - 10.4 Стратегия оптимизации через хеширование + 10.4 Стратегии оптимизации хеширования @@ -2568,7 +2568,7 @@ - 10.5 Алгоритмы поиска: новый взгляд + 10.5 Переосмысление алгоритмов поиска @@ -2773,7 +2773,7 @@ - 11.3 Пузырьковая сортировка + 11.3 Сортировка пузырьком @@ -2801,7 +2801,7 @@ - 11.4 Сортировка вставкой + 11.4 Сортировка вставками @@ -3106,7 +3106,7 @@ - 12.1 Алгоритмы разделяй и властвуй + 12.1 Стратегия разделяй и властвуй @@ -3134,7 +3134,7 @@ - 12.2 Стратегия поиска разделяй и властвуй + 12.2 Поисковая стратегия разделяй и властвуй @@ -3411,7 +3411,7 @@ - 13.4 Задача о $n$ ферзях + 13.4 Задача о n ферзях @@ -3552,7 +3552,7 @@ - 14.1 Введение в динамическое программирование + 14.1 Первое знакомство с динамическим программированием @@ -3664,7 +3664,7 @@ - 14.5 Задача о неограниченном рюкзаке + 14.5 Задача о полном рюкзаке diff --git a/ru/assets/covers/chapter_appendix.jpg b/ru/assets/covers/chapter_appendix.jpg index c35b7db9e..19c262cb1 100644 Binary files a/ru/assets/covers/chapter_appendix.jpg and b/ru/assets/covers/chapter_appendix.jpg differ diff --git a/ru/assets/covers/chapter_array_and_linkedlist.jpg b/ru/assets/covers/chapter_array_and_linkedlist.jpg index 8d03142ff..1c00293c1 100644 Binary files a/ru/assets/covers/chapter_array_and_linkedlist.jpg and b/ru/assets/covers/chapter_array_and_linkedlist.jpg differ diff --git a/ru/assets/covers/chapter_backtracking.jpg b/ru/assets/covers/chapter_backtracking.jpg index ea4aa1d11..50afbefc1 100644 Binary files a/ru/assets/covers/chapter_backtracking.jpg and b/ru/assets/covers/chapter_backtracking.jpg differ diff --git a/ru/assets/covers/chapter_complexity_analysis.jpg b/ru/assets/covers/chapter_complexity_analysis.jpg index 691523b9f..ed689d46e 100644 Binary files a/ru/assets/covers/chapter_complexity_analysis.jpg and b/ru/assets/covers/chapter_complexity_analysis.jpg differ diff --git a/ru/assets/covers/chapter_data_structure.jpg b/ru/assets/covers/chapter_data_structure.jpg index a6ee84428..d77329fc2 100644 Binary files a/ru/assets/covers/chapter_data_structure.jpg and b/ru/assets/covers/chapter_data_structure.jpg differ diff --git a/ru/assets/covers/chapter_dynamic_programming.jpg b/ru/assets/covers/chapter_dynamic_programming.jpg index 274e99157..c2b2d5ce5 100644 Binary files a/ru/assets/covers/chapter_dynamic_programming.jpg and b/ru/assets/covers/chapter_dynamic_programming.jpg differ diff --git a/ru/assets/covers/chapter_graph.jpg b/ru/assets/covers/chapter_graph.jpg index 0e39cccd0..eeefccce5 100644 Binary files a/ru/assets/covers/chapter_graph.jpg and b/ru/assets/covers/chapter_graph.jpg differ diff --git a/ru/assets/covers/chapter_hashing.jpg b/ru/assets/covers/chapter_hashing.jpg index 7a4c95a9c..12ad99151 100644 Binary files a/ru/assets/covers/chapter_hashing.jpg and b/ru/assets/covers/chapter_hashing.jpg differ diff --git a/ru/assets/covers/chapter_hello_algo.jpg b/ru/assets/covers/chapter_hello_algo.jpg index 2444c9551..86077958a 100644 Binary files a/ru/assets/covers/chapter_hello_algo.jpg and b/ru/assets/covers/chapter_hello_algo.jpg differ diff --git a/ru/assets/covers/chapter_introduction.jpg b/ru/assets/covers/chapter_introduction.jpg index b0933c489..5803e13c1 100644 Binary files a/ru/assets/covers/chapter_introduction.jpg and b/ru/assets/covers/chapter_introduction.jpg differ diff --git a/ru/assets/covers/chapter_preface.jpg b/ru/assets/covers/chapter_preface.jpg index b37f0c1b2..2fa1b63ec 100644 Binary files a/ru/assets/covers/chapter_preface.jpg and b/ru/assets/covers/chapter_preface.jpg differ diff --git a/ru/assets/covers/chapter_sorting.jpg b/ru/assets/covers/chapter_sorting.jpg index f89612ad7..46519e7e8 100644 Binary files a/ru/assets/covers/chapter_sorting.jpg and b/ru/assets/covers/chapter_sorting.jpg differ diff --git a/ru/assets/covers/chapter_stack_and_queue.jpg b/ru/assets/covers/chapter_stack_and_queue.jpg index e41820d18..ab4de4ca1 100644 Binary files a/ru/assets/covers/chapter_stack_and_queue.jpg and b/ru/assets/covers/chapter_stack_and_queue.jpg differ diff --git a/ru/assets/covers/chapter_tree.jpg b/ru/assets/covers/chapter_tree.jpg index b780bfe1d..4dff6c918 100644 Binary files a/ru/assets/covers/chapter_tree.jpg and b/ru/assets/covers/chapter_tree.jpg differ diff --git a/ru/assets/javascripts/bundle.c2b142ea.min.js b/ru/assets/javascripts/bundle.c2b142ea.min.js index d80d70325..8ed8f65f3 100644 --- a/ru/assets/javascripts/bundle.c2b142ea.min.js +++ b/ru/assets/javascripts/bundle.c2b142ea.min.js @@ -1,4 +1,4 @@ "use strict";(()=>{var xc=Object.create;var kn=Object.defineProperty,wc=Object.defineProperties,Ec=Object.getOwnPropertyDescriptor,Tc=Object.getOwnPropertyDescriptors,Sc=Object.getOwnPropertyNames,Dr=Object.getOwnPropertySymbols,Oc=Object.getPrototypeOf,An=Object.prototype.hasOwnProperty,Fo=Object.prototype.propertyIsEnumerable;var jo=(e,t,r)=>t in e?kn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,H=(e,t)=>{for(var r in t||(t={}))An.call(t,r)&&jo(e,r,t[r]);if(Dr)for(var r of Dr(t))Fo.call(t,r)&&jo(e,r,t[r]);return e},He=(e,t)=>wc(e,Tc(t));var gr=(e,t)=>{var r={};for(var n in e)An.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Dr)for(var n of Dr(e))t.indexOf(n)<0&&Fo.call(e,n)&&(r[n]=e[n]);return r};var Cn=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Lc=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Sc(t))!An.call(e,o)&&o!==r&&kn(e,o,{get:()=>t[o],enumerable:!(n=Ec(t,o))||n.enumerable});return e};var _r=(e,t,r)=>(r=e!=null?xc(Oc(e)):{},Lc(t||!e||!e.__esModule?kn(r,"default",{value:e,enumerable:!0}):r,e));var Uo=(e,t,r)=>new Promise((n,o)=>{var i=c=>{try{s(r.next(c))}catch(l){o(l)}},a=c=>{try{s(r.throw(c))}catch(l){o(l)}},s=c=>c.done?n(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var Do=Cn((Hn,No)=>{(function(e,t){typeof Hn=="object"&&typeof No!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Hn,(function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(_){return!!(_&&_!==document&&_.nodeName!=="HTML"&&_.nodeName!=="BODY"&&"classList"in _&&"contains"in _.classList)}function c(_){var de=_.type,be=_.tagName;return!!(be==="INPUT"&&a[de]&&!_.readOnly||be==="TEXTAREA"&&!_.readOnly||_.isContentEditable)}function l(_){_.classList.contains("focus-visible")||(_.classList.add("focus-visible"),_.setAttribute("data-focus-visible-added",""))}function u(_){_.hasAttribute("data-focus-visible-added")&&(_.classList.remove("focus-visible"),_.removeAttribute("data-focus-visible-added"))}function p(_){_.metaKey||_.altKey||_.ctrlKey||(s(r.activeElement)&&l(r.activeElement),n=!0)}function d(_){n=!1}function m(_){s(_.target)&&(n||c(_.target))&&l(_.target)}function h(_){s(_.target)&&(_.target.classList.contains("focus-visible")||_.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(_.target))}function v(_){document.visibilityState==="hidden"&&(o&&(n=!0),x())}function x(){document.addEventListener("mousemove",E),document.addEventListener("mousedown",E),document.addEventListener("mouseup",E),document.addEventListener("pointermove",E),document.addEventListener("pointerdown",E),document.addEventListener("pointerup",E),document.addEventListener("touchmove",E),document.addEventListener("touchstart",E),document.addEventListener("touchend",E)}function w(){document.removeEventListener("mousemove",E),document.removeEventListener("mousedown",E),document.removeEventListener("mouseup",E),document.removeEventListener("pointermove",E),document.removeEventListener("pointerdown",E),document.removeEventListener("pointerup",E),document.removeEventListener("touchmove",E),document.removeEventListener("touchstart",E),document.removeEventListener("touchend",E)}function E(_){_.target.nodeName&&_.target.nodeName.toLowerCase()==="html"||(n=!1,w())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",d,!0),document.addEventListener("pointerdown",d,!0),document.addEventListener("touchstart",d,!0),document.addEventListener("visibilitychange",v,!0),x(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var So=Cn((M0,vs)=>{"use strict";var Gu=/["'&<>]/;vs.exports=Ju;function Ju(e){var t=""+e,r=Gu.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i{(function(t,r){typeof jr=="object"&&typeof Lo=="object"?Lo.exports=r():typeof define=="function"&&define.amd?define([],r):typeof jr=="object"?jr.ClipboardJS=r():t.ClipboardJS=r()})(jr,function(){return(function(){var e={686:(function(n,o,i){"use strict";i.d(o,{default:function(){return vr}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),p=i.n(u);function d(B){try{return document.execCommand(B)}catch(C){return!1}}var m=function(C){var k=p()(C);return d("cut"),k},h=m;function v(B){var C=document.documentElement.getAttribute("dir")==="rtl",k=document.createElement("textarea");k.style.fontSize="12pt",k.style.border="0",k.style.padding="0",k.style.margin="0",k.style.position="absolute",k.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return k.style.top="".concat(D,"px"),k.setAttribute("readonly",""),k.value=B,k}var x=function(C,k){var D=v(C);k.container.appendChild(D);var W=p()(D);return d("copy"),D.remove(),W},w=function(C){var k=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=x(C,k):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=x(C.value,k):(D=p()(C),d("copy")),D},E=w;function _(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?_=function(k){return typeof k}:_=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},_(B)}var de=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},k=C.action,D=k===void 0?"copy":k,W=C.container,Z=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Z!==void 0)if(Z&&_(Z)==="object"&&Z.nodeType===1){if(D==="copy"&&Z.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(Z.hasAttribute("readonly")||Z.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return E(We,{container:W});if(Z)return D==="cut"?h(Z):E(Z,{container:W})},be=de;function M(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?M=function(k){return typeof k}:M=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},M(B)}function O(B,C){if(!(B instanceof C))throw new TypeError("Cannot call a class as a function")}function N(B,C){for(var k=0;k0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof W.action=="function"?W.action:this.defaultAction,this.target=typeof W.target=="function"?W.target:this.defaultTarget,this.text=typeof W.text=="function"?W.text:this.defaultText,this.container=M(W.container)==="object"?W.container:document.body}},{key:"listenClick",value:function(W){var Z=this;this.listener=l()(W,"click",function(We){return Z.onClick(We)})}},{key:"onClick",value:function(W){var Z=W.delegateTarget||W.currentTarget,We=this.action(Z)||"copy",Gt=be({action:We,container:this.container,target:this.target(Z),text:this.text(Z)});this.emit(Gt?"success":"error",{action:We,text:Gt,trigger:Z,clearSelection:function(){Z&&Z.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(W){return Yt("action",W)}},{key:"defaultTarget",value:function(W){var Z=Yt("target",W);if(Z)return document.querySelector(Z)}},{key:"defaultText",value:function(W){return Yt("text",W)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(W){var Z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return E(W,Z)}},{key:"cut",value:function(W){return h(W)}},{key:"isSupported",value:function(){var W=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Z=typeof W=="string"?[W]:W,We=!!document.queryCommandSupported;return Z.forEach(function(Gt){We=We&&!!document.queryCommandSupported(Gt)}),We}}]),k})(s()),vr=Mt}),828:(function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a}),438:(function(n,o,i){var a=i(828);function s(u,p,d,m,h){var v=l.apply(this,arguments);return u.addEventListener(d,v,h),{destroy:function(){u.removeEventListener(d,v,h)}}}function c(u,p,d,m,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof d=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,d,m,h)}))}function l(u,p,d,m){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&m.call(u,h)}}n.exports=c}),879:(function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}}),370:(function(n,o,i){var a=i(879),s=i(438);function c(d,m,h){if(!d&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(d))return l(d,m,h);if(a.nodeList(d))return u(d,m,h);if(a.string(d))return p(d,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(d,m,h){return d.addEventListener(m,h),{destroy:function(){d.removeEventListener(m,h)}}}function u(d,m,h){return Array.prototype.forEach.call(d,function(v){v.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(d,function(v){v.removeEventListener(m,h)})}}}function p(d,m,h){return s(document.body,d,m,h)}n.exports=c}),817:(function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}n.exports=o}),279:(function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c0&&i[i.length-1])&&(l[0]===6||l[0]===2)){r=0;continue}if(l[0]===3&&(!i||l[1]>i[0]&&l[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function te(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function ne(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||c(m,v)})},h&&(o[m]=h(o[m])))}function c(m,h){try{l(n[m](h))}catch(v){d(i[0][3],v)}}function l(m){m.value instanceof kt?Promise.resolve(m.value.v).then(u,p):d(i[0][2],m)}function u(m){c("next",m)}function p(m){c("throw",m)}function d(m,h){m(h),i.shift(),i.length&&c(i[0][0],i[0][1])}}function zo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof $e=="function"?$e(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),o(s,c,a.done,a.value)})}}function o(i,a,s,c){Promise.resolve(c).then(function(l){i({value:l,done:s})},a)}}function F(e){return typeof e=="function"}function Jt(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Vr=Jt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: `+r.map(function(n,o){return o+1+") "+n.toString()}).join(` `):"",this.name="UnsubscriptionError",this.errors=r}});function ct(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var rt=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=$e(a),c=s.next();!c.done;c=s.next()){var l=c.value;l.remove(this)}}catch(v){t={error:v}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(F(u))try{u()}catch(v){i=v instanceof Vr?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var d=$e(p),m=d.next();!m.done;m=d.next()){var h=m.value;try{qo(h)}catch(v){i=i!=null?i:[],v instanceof Vr?i=ne(ne([],te(i)),te(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{m&&!m.done&&(o=d.return)&&o.call(d)}finally{if(n)throw n.error}}}if(i)throw new Vr(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)qo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&ct(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&ct(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var Pn=rt.EMPTY;function zr(e){return e instanceof rt||e&&"closed"in e&&F(e.remove)&&F(e.add)&&F(e.unsubscribe)}function qo(e){F(e)?e():e.unsubscribe()}var Je={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Xt={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Pn:(this.currentObservers=null,s.push(r),new rt(function(){n.currentObservers=null,ct(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new Qo(r,n)},t})(U);var Qo=(function(e){ue(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Pn},t})(I);var Un=(function(e){ue(t,e);function t(r){var n=e.call(this)||this;return n._value=r,n}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var n=e.prototype._subscribe.call(this,r);return!n.closed&&r.next(this._value),n},t.prototype.getValue=function(){var r=this,n=r.hasError,o=r.thrownError,i=r._value;if(n)throw o;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(I);var xr={now:function(){return(xr.delegate||Date).now()},delegate:void 0};var wr=(function(e){ue(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=xr);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.schedule.call(this,r,n):(this.delay=n,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,n){return n>0||this.closed?e.prototype.execute.call(this,r,n):this._execute(r,n)},t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.flush(this),0)},t})(tr);var ri=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(rr);var Wn=new ri(ti);var ni=(function(e){ue(t,e);function t(r,n){var o=e.call(this,r,n)||this;return o.scheduler=r,o.work=n,o}return t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!==null&&o>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=er.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&n===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(er.cancelAnimationFrame(n),r._scheduled=void 0)},t})(tr);var oi=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n;r?n=r.id:(n=this._scheduled,this._scheduled=void 0);var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t})(rr);var je=new oi(ni);var y=new U(function(e){return e.complete()});function Br(e){return e&&F(e.schedule)}function Vn(e){return e[e.length-1]}function _t(e){return F(Vn(e))?e.pop():void 0}function qe(e){return Br(Vn(e))?e.pop():void 0}function Yr(e,t){return typeof Vn(e)=="number"?e.pop():t}var nr=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function Gr(e){return F(e==null?void 0:e.then)}function Jr(e){return F(e[Qt])}function Xr(e){return Symbol.asyncIterator&&F(e==null?void 0:e[Symbol.asyncIterator])}function Zr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Rc(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qr=Rc();function en(e){return F(e==null?void 0:e[Qr])}function tn(e){return Vo(this,arguments,function(){var r,n,o,i;return Wr(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,kt(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,kt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,kt(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function rn(e){return F(e==null?void 0:e.getReader)}function q(e){if(e instanceof U)return e;if(e!=null){if(Jr(e))return jc(e);if(nr(e))return Fc(e);if(Gr(e))return Uc(e);if(Xr(e))return ii(e);if(en(e))return Nc(e);if(rn(e))return Dc(e)}throw Zr(e)}function jc(e){return new U(function(t){var r=e[Qt]();if(F(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Fc(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?L(function(o,i){return e(o,i,n)}):Oe,Me(1),r?ot(t):wi(function(){return new on}))}}function Gn(e){return e<=0?function(){return y}:S(function(t,r){var n=[];t.subscribe(T(r,function(o){n.push(o),e=2,!0))}function xe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new I}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u,p,d,m=0,h=!1,v=!1,x=function(){p==null||p.unsubscribe(),p=void 0},w=function(){x(),u=d=void 0,h=v=!1},E=function(){var _=u;w(),_==null||_.unsubscribe()};return S(function(_,de){m++,!v&&!h&&x();var be=d=d!=null?d:r();de.add(function(){m--,m===0&&!v&&!h&&(p=Jn(E,c))}),be.subscribe(de),!u&&m>0&&(u=new Ct({next:function(M){return be.next(M)},error:function(M){v=!0,x(),p=Jn(w,o,M),be.error(M)},complete:function(){h=!0,x(),p=Jn(w,a),be.complete()}}),q(_).subscribe(u))})(l)}}function Jn(e,t){for(var r=[],n=2;ne.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function G(e,t=document){let r=Le(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function Le(e,t=document){return t.querySelector(e)||void 0}function xt(){var e,t,r,n;return(n=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?n:void 0}var il=R(b(document.body,"focusin"),b(document.body,"focusout")).pipe(Be(1),J(void 0),f(()=>xt()||document.body),se(1));function ir(e){return il.pipe(f(t=>e.contains(t)),ie())}function Ft(e,t){let{matches:r}=matchMedia("(hover)");return j(()=>(r?R(b(e,"mouseenter").pipe(f(()=>!0)),b(e,"mouseleave").pipe(f(()=>!1))):R(b(e,"touchstart").pipe(f(()=>!0)),b(e,"touchend").pipe(f(()=>!1)),b(e,"touchcancel").pipe(f(()=>!1)))).pipe(t?Tr(o=>Ve(+!o*t)):Oe,J(!0,e.matches(":hover"))))}function Oi(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Oi(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Oi(n,o);return n}function Li(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function ar(e){let t=A("script",{src:e});return j(()=>(document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(g(()=>zn(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(f(()=>{}),V(()=>document.head.removeChild(t)),Me(1))))}var Mi=new I,al=j(()=>typeof ResizeObserver=="undefined"?ar("https://unpkg.com/resize-observer-polyfill"):Y(void 0)).pipe(f(()=>new ResizeObserver(e=>e.forEach(t=>Mi.next(t)))),g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Ae(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Re(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return al.pipe($(r=>r.observe(t)),g(r=>Mi.pipe(L(n=>n.target===t),V(()=>r.unobserve(t)))),f(()=>Ae(e)),J(Ae(e)))}function Mr(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ki(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Ai(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function wt(e){return{x:e.offsetLeft,y:e.offsetTop}}function Ci(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Hi(e){return R(b(window,"load"),b(window,"resize")).pipe(Xe(0,je),f(()=>wt(e)),J(wt(e)))}function ln(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ut(e){return R(b(e,"scroll"),b(window,"scroll"),b(window,"resize")).pipe(Xe(0,je),f(()=>ln(e)),J(ln(e)))}var $i=new I,sl=j(()=>Y(new IntersectionObserver(e=>{for(let t of e)$i.next(t)},{threshold:0}))).pipe(g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Et(e){return sl.pipe($(t=>t.observe(e)),g(t=>$i.pipe(L(({target:r})=>r===e),V(()=>t.unobserve(e)),f(({isIntersecting:r})=>r))))}var cl=Object.create,la=Object.defineProperty,ll=Object.getOwnPropertyDescriptor,ul=Object.getOwnPropertyNames,pl=Object.getPrototypeOf,fl=Object.prototype.hasOwnProperty,ml=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),dl=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ul(t))!fl.call(e,o)&&o!==r&&la(e,o,{get:()=>t[o],enumerable:!(n=ll(t,o))||n.enumerable});return e},hl=(e,t,r)=>(r=e!=null?cl(pl(e)):{},dl(t||!e||!e.__esModule?la(r,"default",{value:e,enumerable:!0}):r,e)),vl=ml((e,t)=>{var r="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,c=/^0o[0-7]+$/i,l=parseInt,u=typeof global=="object"&&global&&global.Object===Object&&global,p=typeof self=="object"&&self&&self.Object===Object&&self,d=u||p||Function("return this")(),m=Object.prototype,h=m.toString,v=Math.max,x=Math.min,w=function(){return d.Date.now()};function E(O,N,ee){var le,ce,Ne,bt,De,st,tt=0,Yt=!1,Mt=!1,vr=!0;if(typeof O!="function")throw new TypeError(r);N=M(N)||0,_(ee)&&(Yt=!!ee.leading,Mt="maxWait"in ee,Ne=Mt?v(M(ee.maxWait)||0,N):Ne,vr="trailing"in ee?!!ee.trailing:vr);function B(Te){var gt=le,br=ce;return le=ce=void 0,tt=Te,bt=O.apply(br,gt),bt}function C(Te){return tt=Te,De=setTimeout(W,N),Yt?B(Te):bt}function k(Te){var gt=Te-st,br=Te-tt,Ro=N-gt;return Mt?x(Ro,Ne-br):Ro}function D(Te){var gt=Te-st,br=Te-tt;return st===void 0||gt>=N||gt<0||Mt&&br>=Ne}function W(){var Te=w();if(D(Te))return Z(Te);De=setTimeout(W,k(Te))}function Z(Te){return De=void 0,vr&&le?B(Te):(le=ce=void 0,bt)}function We(){De!==void 0&&clearTimeout(De),tt=0,le=st=ce=De=void 0}function Gt(){return De===void 0?bt:Z(w())}function Nr(){var Te=w(),gt=D(Te);if(le=arguments,ce=this,st=Te,gt){if(De===void 0)return C(st);if(Mt)return De=setTimeout(W,N),B(st)}return De===void 0&&(De=setTimeout(W,N)),bt}return Nr.cancel=We,Nr.flush=Gt,Nr}function _(O){var N=typeof O;return!!O&&(N=="object"||N=="function")}function de(O){return!!O&&typeof O=="object"}function be(O){return typeof O=="symbol"||de(O)&&h.call(O)==o}function M(O){if(typeof O=="number")return O;if(be(O))return n;if(_(O)){var N=typeof O.valueOf=="function"?O.valueOf():O;O=_(N)?N+"":N}if(typeof O!="string")return O===0?O:+O;O=O.replace(i,"");var ee=s.test(O);return ee||c.test(O)?l(O.slice(2),ee?2:8):a.test(O)?n:+O}t.exports=E}),yn,K,ua,pa,Nt,Pi,fa,ma,da,lo,to,ro,bl,Ar={},ha=[],gl=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Pr=Array.isArray;function pt(e,t){for(var r in t)e[r]=t[r];return e}function uo(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function Wt(e,t,r){var n,o,i,a={};for(i in t)i=="key"?n=t[i]:i=="ref"?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?yn.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return fn(e,a,n,o,null)}function fn(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o!=null?o:++ua,__i:-1,__u:0};return o==null&&K.vnode!=null&&K.vnode(i),i}function ft(e){return e.children}function at(e,t){this.props=e,this.context=t}function cr(e,t){if(t==null)return e.__?cr(e.__,e.__i+1):null;for(var r;ts&&Nt.sort(ma),e=Nt.shift(),s=Nt.length,e.__d&&(r=void 0,n=void 0,o=(n=(t=e).__v).__e,i=[],a=[],t.__P&&((r=pt({},n)).__v=n.__v+1,K.vnode&&K.vnode(r),po(t.__P,r,n,t.__n,t.__P.namespaceURI,32&n.__u?[o]:null,i,o!=null?o:cr(n),!!(32&n.__u),a),r.__v=n.__v,r.__.__k[r.__i]=r,_a(i,r,a),n.__e=n.__=null,r.__e!=o&&va(r)));vn.__r=0}function ba(e,t,r,n,o,i,a,s,c,l,u){var p,d,m,h,v,x,w,E=n&&n.__k||ha,_=t.length;for(c=_l(r,t,E,c,_),p=0;p<_;p++)(m=r.__k[p])!=null&&(d=m.__i==-1?Ar:E[m.__i]||Ar,m.__i=p,x=po(e,m,d,o,i,a,s,c,l,u),h=m.__e,m.ref&&d.ref!=m.ref&&(d.ref&&fo(d.ref,null,m),u.push(m.ref,m.__c||h,m)),v==null&&h!=null&&(v=h),(w=!!(4&m.__u))||d.__k===m.__k?c=ga(m,c,e,w):typeof m.type=="function"&&x!==void 0?c=x:h&&(c=h.nextSibling),m.__u&=-7);return r.__e=v,c}function _l(e,t,r,n,o){var i,a,s,c,l,u=r.length,p=u,d=0;for(e.__k=new Array(o),i=0;i0?fn(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):a).__=e,a.__b=e.__b+1,s=null,(l=a.__i=yl(a,r,c,p))!=-1&&(p--,(s=r[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(o>u?d--:oc?d--:d++,a.__u|=4))):e.__k[i]=null;if(p)for(i=0;i(u?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return a}return-1}function Ri(e,t,r){t[0]=="-"?e.setProperty(t,r!=null?r:""):e[t]=r==null?"":typeof r!="number"||gl.test(t)?r:r+"px"}function un(e,t,r,n,o){var i,a;e:if(t=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)r&&t in r||Ri(e.style,t,"");if(r)for(t in r)n&&r[t]==n[t]||Ri(e.style,t,r[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(da,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=r,r?n?r.u=n.u:(r.u=lo,e.addEventListener(t,i?ro:to,i)):e.removeEventListener(t,i?ro:to,i);else{if(o=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=r!=null?r:"";break e}catch(s){}typeof r=="function"||(r==null||r===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&r==1?"":r))}}function ji(e){return function(t){if(this.l){var r=this.l[t.type+e];if(t.t==null)t.t=lo++;else if(t.t0?e:Pr(e)?e.map(ya):pt({},e)}function xl(e,t,r,n,o,i,a,s,c){var l,u,p,d,m,h,v,x=r.props,w=t.props,E=t.type;if(E=="svg"?o="http://www.w3.org/2000/svg":E=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(l=0;l=r.__.length&&r.__.push({}),r.__[e]}function bn(e){return $r=1,Tl(Ta,e)}function Tl(e,t,r){var n=mo(Hr++,2);if(n.t=e,!n.__c&&(n.__=[r?r(t):Ta(void 0,t),function(s){var c=n.__N?n.__N[0]:n.__[0],l=n.t(c,s);c!==l&&(n.__N=[l,n.__[1]],n.__c.setState({}))}],n.__c=ve,!ve.__f)){var o=function(s,c,l){if(!n.__c.__H)return!0;var u=n.__c.__H.__.filter(function(d){return!!d.__c});if(u.every(function(d){return!d.__N}))return!i||i.call(this,s,c,l);var p=n.__c.props!==s;return u.forEach(function(d){if(d.__N){var m=d.__[0];d.__=d.__N,d.__N=void 0,m!==d.__[0]&&(p=!0)}}),i&&i.call(this,s,c,l)||p};ve.__f=!0;var i=ve.shouldComponentUpdate,a=ve.componentWillUpdate;ve.componentWillUpdate=function(s,c,l){if(this.__e){var u=i;i=void 0,o(s,c,l),i=u}a&&a.call(this,s,c,l)},ve.shouldComponentUpdate=o}return n.__N||n.__}function mt(e,t){var r=mo(Hr++,3);!we.__s&&Ea(r.__H,t)&&(r.__=e,r.u=t,ve.__H.__h.push(r))}function Vt(e){return $r=5,ur(function(){return{current:e}},[])}function ur(e,t){var r=mo(Hr++,7);return Ea(r.__H,t)&&(r.__=e(),r.__H=t,r.__h=e),r.__}function Sl(e,t){return $r=8,ur(function(){return e},t)}function Ol(){for(var e;e=wa.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(mn),e.__H.__h.forEach(oo),e.__H.__h=[]}catch(t){e.__H.__h=[],we.__e(t,e.__v)}}we.__b=function(e){ve=null,Ui&&Ui(e)},we.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),zi&&zi(e,t)},we.__r=function(e){Ni&&Ni(e),Hr=0;var t=(ve=e.__c).__H;t&&(Zn===ve?(t.__h=[],ve.__h=[],t.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(t.__h.forEach(mn),t.__h.forEach(oo),t.__h=[],Hr=0)),Zn=ve},we.diffed=function(e){Di&&Di(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(wa.push(t)!==1&&Fi===we.requestAnimationFrame||((Fi=we.requestAnimationFrame)||Ll)(Ol)),t.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),Zn=ve=null},we.__c=function(e,t){t.some(function(r){try{r.__h.forEach(mn),r.__h=r.__h.filter(function(n){return!n.__||oo(n)})}catch(n){t.some(function(o){o.__h&&(o.__h=[])}),t=[],we.__e(n,r.__v)}}),Wi&&Wi(e,t)},we.unmount=function(e){Vi&&Vi(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(n){try{mn(n)}catch(o){t=o}}),r.__H=void 0,t&&we.__e(t,r.__v))};var qi=typeof requestAnimationFrame=="function";function Ll(e){var t,r=function(){clearTimeout(n),qi&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,35);qi&&(t=requestAnimationFrame(r))}function mn(e){var t=ve,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),ve=t}function oo(e){var t=ve;e.__c=e.__(),ve=t}function Ea(e,t){return!e||e.length!==t.length||t.some(function(r,n){return r!==e[n]})}function Ta(e,t){return typeof t=="function"?t(e):t}function Ml(e,t){for(var r in t)e[r]=t[r];return e}function Ki(e,t){for(var r in e)if(r!=="__source"&&!(r in t))return!0;for(var n in t)if(n!=="__source"&&e[n]!==t[n])return!0;return!1}function Bi(e,t){this.props=e,this.context=t}(Bi.prototype=new at).isPureReactComponent=!0,Bi.prototype.shouldComponentUpdate=function(e,t){return Ki(this.props,e)||Ki(this.state,t)};var Yi=K.__b;K.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),Yi&&Yi(e)};var Yx=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,kl=K.__e;K.__e=function(e,t,r,n){if(e.then){for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return t.__e==null&&(t.__e=r.__e,t.__k=r.__k),o.__c(e,t)}kl(e,t,r,n)};var Gi=K.unmount;function Sa(e,t,r){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=Ml({},e)).__c!=null&&(e.__c.__P===r&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return Sa(n,t,r)})),e}function Oa(e,t,r){return e&&r&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return Oa(n,t,r)}),e.__c&&e.__c.__P===t&&(e.__e&&r.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=r)),e}function Qn(){this.__u=0,this.o=null,this.__b=null}function La(e){var t=e.__.__c;return t&&t.__a&&t.__a(e)}function pn(){this.i=null,this.l=null}K.unmount=function(e){var t=e.__c;t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),Gi&&Gi(e)},(Qn.prototype=new at).__c=function(e,t){var r=t.__c,n=this;n.o==null&&(n.o=[]),n.o.push(r);var o=La(n.__v),i=!1,a=function(){i||(i=!0,r.__R=null,o?o(s):s())};r.__R=a;var s=function(){if(!--n.__u){if(n.state.__a){var c=n.state.__a;n.__v.__k[0]=Oa(c,c.__c.__P,c.__c.__O)}var l;for(n.setState({__a:n.__b=null});l=n.o.pop();)l.forceUpdate()}};n.__u++||32&t.__u||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(a,a)},Qn.prototype.componentWillUnmount=function(){this.o=[]},Qn.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=Sa(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__a&&Wt(ft,null,e.fallback);return o&&(o.__u&=-33),[Wt(ft,null,t.__a?null:e.children),o]};var Ji=function(e,t,r){if(++r[1]===r[0]&&e.l.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.l.size))for(r=e.i;r;){for(;r.length>3;)r.pop()();if(r[1]Object.freeze({get current(){return t.current}}),[])}var Nl=typeof globalThis<"u"&&typeof navigator<"u"&&typeof document<"u";function Dl(e,...t){var r;(r=e==null?void 0:e.addEventListener)==null||r.call(e,...t)}function Wl(e,...t){var r;(r=e==null?void 0:e.removeEventListener)==null||r.call(e,...t)}var Vl=(e,t)=>Object.hasOwn(e,t),zl=()=>!0,ql=()=>!1;function Kl(e=!1){let t=Vt(e),r=Sl(()=>t.current,[]);return mt(()=>(t.current=!0,()=>{t.current=!1}),[]),r}function Bl(e,...t){let r=Kl(),n=ka(t[1]),o=ur(()=>function(...i){r()&&(typeof n.current=="function"?n.current.apply(this,i):typeof n.current.handleEvent=="function"&&n.current.handleEvent.apply(this,i))},[]);mt(()=>{let i=Yl(e)?e.current:e;if(!i)return;let a=t.slice(2);return Dl(i,t[0],o,...a),()=>{Wl(i,t[0],o,...a)}},[e,t[0]])}function Yl(e){return e!==null&&typeof e=="object"&&Vl(e,"current")}var Gl=e=>typeof e=="function"?e:typeof e=="string"?t=>t.key===e:e?zl:ql,Jl=Nl?globalThis:null;function Aa(e,t,r=[],n={}){let{event:o="keydown",target:i=Jl,eventOptions:a}=n,s=ka(t),c=ur(()=>{let l=Gl(e);return function(u){l(u)&&s.current.call(this,u)}},r);Bl(i,o,c,a)}function Ca(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t1)St--;else{for(var e,t=!1;kr!==void 0;){var r=kr;for(kr=void 0,io++;r!==void 0;){var n=r.o;if(r.o=void 0,r.f&=-3,!(8&r.f)&&Pa(r))try{r.c()}catch(o){t||(e=o,t=!0)}r=n}}if(io=0,St--,t)throw e}}function Ql(e){if(St>0)return e();St++;try{return e()}finally{xn()}}var ae=void 0;function Ha(e){var t=ae;ae=void 0;try{return e()}finally{ae=t}}var kr=void 0,St=0,io=0,gn=0;function $a(e){if(ae!==void 0){var t=e.n;if(t===void 0||t.t!==ae)return t={i:0,S:e,p:ae.s,n:void 0,t:ae,e:void 0,x:void 0,r:t},ae.s!==void 0&&(ae.s.n=t),ae.s=t,e.n=t,32&ae.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=ae.s,t.n=void 0,ae.s.n=t,ae.s=t),t}}function Ce(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Ce.prototype.brand=Zl;Ce.prototype.h=function(){return!0};Ce.prototype.S=function(e){var t=this,r=this.t;r!==e&&e.e===void 0&&(e.x=r,this.t=e,r!==void 0?r.e=e:Ha(function(){var n;(n=t.W)==null||n.call(t)}))};Ce.prototype.U=function(e){var t=this;if(this.t!==void 0){var r=e.e,n=e.x;r!==void 0&&(r.x=n,e.e=void 0),n!==void 0&&(n.e=r,e.x=void 0),e===this.t&&(this.t=n,n===void 0&&Ha(function(){var o;(o=t.Z)==null||o.call(t)}))}};Ce.prototype.subscribe=function(e){var t=this;return qt(function(){var r=t.value,n=ae;ae=void 0;try{e(r)}finally{ae=n}},{name:"sub"})};Ce.prototype.valueOf=function(){return this.value};Ce.prototype.toString=function(){return this.value+""};Ce.prototype.toJSON=function(){return this.value};Ce.prototype.peek=function(){var e=ae;ae=void 0;try{return this.value}finally{ae=e}};Object.defineProperty(Ce.prototype,"value",{get:function(){var e=$a(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(io>100)throw new Error("Cycle detected");this.v=e,this.i++,gn++,St++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{xn()}}}});function Ot(e,t){return new Ce(e,t)}function Pa(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function Ia(e){for(var t=e.s;t!==void 0;t=t.n){var r=t.S.n;if(r!==void 0&&(t.r=r),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function Ra(e){for(var t=e.s,r=void 0;t!==void 0;){var n=t.p;t.i===-1?(t.S.U(t),n!==void 0&&(n.n=t.n),t.n!==void 0&&(t.n.p=n)):r=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=n}e.s=r}function Kt(e,t){Ce.call(this,void 0),this.x=e,this.s=void 0,this.g=gn-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Kt.prototype=new Ce;Kt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===gn))return!0;if(this.g=gn,this.f|=1,this.i>0&&!Pa(this))return this.f&=-2,!0;var e=ae;try{Ia(this),ae=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(r){this.v=r,this.f|=16,this.i++}return ae=e,Ra(this),this.f&=-2,!0};Kt.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}Ce.prototype.S.call(this,e)};Kt.prototype.U=function(e){if(this.t!==void 0&&(Ce.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};Kt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(Kt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=$a(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function ta(e,t){return new Kt(e,t)}function ja(e){var t=e.u;if(e.u=void 0,typeof t=="function"){St++;var r=ae;ae=void 0;try{t()}catch(n){throw e.f&=-2,e.f|=8,ho(e),n}finally{ae=r,xn()}}}function ho(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,ja(e)}function eu(e){if(ae!==this)throw new Error("Out-of-order effect");Ra(this),ae=e,this.f&=-2,8&this.f&&ho(this),xn()}function pr(e,t){this.x=e,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32,this.name=t==null?void 0:t.name}pr.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.u=t)}finally{e()}};pr.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,ja(this),Ia(this),St++;var e=ae;return ae=this,eu.bind(this,e)};pr.prototype.N=function(){2&this.f||(this.f|=2,this.o=kr,kr=this)};pr.prototype.d=function(){this.f|=8,1&this.f||ho(this)};pr.prototype.dispose=function(){this.d()};function qt(e,t){var r=new pr(e,t);try{r.c()}catch(o){throw r.d(),o}var n=r.d.bind(r);return n[Symbol.dispose]=n,n}var Fa,vo,eo,Ua=[];qt(function(){Fa=this.N})();function fr(e,t){K[e]=t.bind(null,K[e]||function(){})}function _n(e){eo&&eo(),eo=e&&e.S()}function Na(e){var t=this,r=e.data,n=ru(r);n.value=r;var o=ur(function(){for(var s=t,c=t.__v;c=c.__;)if(c.__c){c.__c.__$f|=4;break}var l=ta(function(){var m=n.value.value;return m===0?0:m===!0?"":m||""}),u=ta(function(){return!Array.isArray(l.value)&&!pa(l.value)}),p=qt(function(){if(this.N=Da,u.value){var m=l.value;s.__v&&s.__v.__e&&s.__v.__e.nodeType===3&&(s.__v.__e.data=m)}}),d=t.__$u.d;return t.__$u.d=function(){p(),d.call(this)},[u,l]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}Na.displayName="ReactiveTextNode";Object.defineProperties(Ce.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Na},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});fr("__b",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),typeof t.type=="string"){var r,n=t.props;for(var o in n)if(o!=="children"){var i=n[o];i instanceof Ce&&(r||(t.__np=r={}),r[o]=i,n[o]=i.peek())}}e(t)});fr("__r",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(t),t.type!==ft){_n();var r,n=t.__c;n&&(n.__$f&=-2,(r=n.__$u)===void 0&&(n.__$u=r=(function(o){var i;return qt(function(){i=this}),i.c=function(){n.__$f|=1,n.setState({})},i})())),vo=n,_n(r)}e(t)});fr("__e",function(e,t,r,n){typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0,e(t,r,n)});fr("diffed",function(e,t){typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0;var r;if(typeof t.type=="string"&&(r=t.__e)){var n=t.__np,o=t.props;if(n){var i=r.U;if(i)for(var a in i){var s=i[a];s!==void 0&&!(a in n)&&(s.d(),i[a]=void 0)}else i={},r.U=i;for(var c in n){var l=i[c],u=n[c];l===void 0?(l=tu(r,c,u,o),i[c]=l):l.o(u,o)}}}e(t)});function tu(e,t,r,n){var o=t in e&&e.ownerSVGElement===void 0,i=Ot(r);return{o:function(a,s){i.value=a,n=s},d:qt(function(){this.N=Da;var a=i.value.value;n[t]!==a&&(n[t]=a,o?e[t]=a:a?e.setAttribute(t,a):e.removeAttribute(t))})}}fr("unmount",function(e,t){if(typeof t.type=="string"){var r=t.__e;if(r){var n=r.U;if(n){r.U=void 0;for(var o in n){var i=n[o];i&&i.d()}}}}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)});fr("__h",function(e,t,r,n){(n<3||n===9)&&(t.__$f|=2),e(t,r,n)});at.prototype.shouldComponentUpdate=function(e,t){var r=this.__$u,n=r&&r.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){var i=2&this.__$f;if(!(n||i||4&this.__$f)||1&this.__$f)return!0}else if(!(n||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};function ru(e,t){return bn(function(){return Ot(e,t)})[0]}var nu=function(e){queueMicrotask(function(){queueMicrotask(e)})};function ou(){Ql(function(){for(var e;e=Ua.shift();)Fa.call(e)})}function Da(){Ua.push(this)===1&&(K.requestAnimationFrame||nu)(ou)}var ao=[0];for(let e=0;e<32;e++)ao.push(ao[e]|1<>>5]>>>e&1}set(e){this.data[e>>>5]|=1<<(e&31)}forEach(e){let t=this.size&31;for(let r=0;r{var r;return(r=t.tags)==null?void 0:r.length})&&(matchMedia("(max-width: 768px)").matches||Wa())}function Dt(){Qe.value=He(H({},Qe.value),{hideSearch:!Qe.value.hideSearch})}function Wa(){Qe.value=He(H({},Qe.value),{hideFilters:!Qe.value.hideFilters})}function dn(){return Qe.value.selectedItem}function so(e){Qe.value=He(H({},Qe.value),{selectedItem:e})}function su(){var e,t;return(t=(e=lr.value)==null?void 0:e.items)!=null?t:[]}function wn(){return typeof Se.value.input=="string"?Se.value.input:""}function Va(e){let t=za();e.length&&!t.length?Se.value=He(H({},Se.value),{page:void 0,input:e}):!e.length&&t.length?Se.value=He(H({},Se.value),{page:void 0,input:{type:"operator",data:{operator:"not",operands:[]}}}):Se.value=He(H({},Se.value),{page:void 0,input:e})}function cu(){typeof it.value.pagination.next<"u"&&(Se.value=He(H({},Se.value),{page:it.value.pagination.next}))}function lu(e){let t=Se.value.filter.input;if("type"in t&&t.type==="operator"){for(let r of t.data.operands)if("type"in r&&r.type==="value"&&typeof r.data.value=="string"&&r.data.value===e)return!0}return!1}function za(){let e=Se.value.filter.input,t=[];if("type"in e&&e.type==="operator")for(let r of e.data.operands)"type"in r&&r.type==="value"&&typeof r.data.value=="string"&&t.push(r.data.value);return t}function uu(e){let t=Se.value.filter.input,r=[];if("type"in t&&t.type==="operator")for(let n of t.data.operands)"type"in n&&n.type==="value"&&typeof n.data.value=="string"&&r.push(n.data.value);if(r.includes(e)){let n=r.indexOf(e);n>-1&&r.splice(n,1)}else r.push(e);Se.value=He(H({},Se.value),{page:void 0,filter:He(H({},Se.value.filter),{input:{type:"operator",data:{operator:"and",operands:r.map(n=>({type:"value",data:{field:"tags",value:n}}))}}})}),Va(wn())}function pu(){return it.value.items}function fu(){return it.value.total}function mu(){var e;for(let t of(e=it.value.aggregations)!=null?e:[])if(t.type==="term")return t.data.value;return[]}function sr(){return Qe.value.hideSearch}function du(){return Qe.value.hideFilters}function qa(){var e;return(e=Ka.value.highlight)!=null?e:!1}var Qe=Ot({hideSearch:!0,hideFilters:!0,selectedItem:0}),Ka=Ot({}),lr=Ot(),na=Ot(),Se=Ot({input:"",filter:{input:{type:"operator",data:{operator:"and",operands:[]}},aggregation:{input:[{type:"term",data:{field:"tags"}}]}}}),it=Ot({items:[],query:{select:{documents:new ra(0),terms:new ra(0)},values:[]},pagination:{total:0}});function hu(e,t,r){for(let n=0;tr&&t(0,o,r,r=i);continue;case 62:e.charCodeAt(r+1)===47?t(2,--o,r,r=i+1):hu(e,r,n)?t(3,o,r,r=i+1):t(1,o++,r,r=i+1)}i>r&&t(0,o,r,i)}function bu(e,t=0,r=e.length){let n=++t;e:for(let l=0;n{let i=[],a=[],{onElement:s,onText:c=gu}=typeof r=="function"?{onElement:r}:r,l=0,u=0;return e(t,(p,d,m,h)=>{if(p===0)i[l++]=c(t,m,h),a[u++]={value:null,depth:d};else if(p&1&&(a[u++]={value:bu(t,m,h),depth:d}),p&2)for(let v=0;u>=0;v++){let{value:x,depth:w}=a[--u];if(w>d)continue;let E=i.slice(l-=v,l+v);i[l++]=s(x,E),u++;break}},n,o),i.slice(0,l)}}function yu(e){return e.replace(/[&<>]/g,t=>{switch(t.charCodeAt(0)){case 38:return"&";case 60:return"<";case 62:return">"}})}function hn(e){return e.replace(/&(amp|[lg]t);/g,t=>{switch(t.charCodeAt(1)){case 97:return"&";case 108:return"<";case 103:return">"}})}function xu(e,t){return{start:e.start+t,end:e.end+t,value:e.value}}function wu(e,t,r){return e.slice(t,r)}function Eu(e){let{onHighlight:t,onText:r=wu}=typeof e=="function"?{onHighlight:e}:e;return(n,o,i=0,a=n.length)=>{var l;let s=[],c=(l=o==null?void 0:o.ranges)!=null?l:[];for(let u=0,p=i;ua)break;let m=c[u].end;if(mi&&s.push(r(n,i,d));let{value:h}=c[u];s.push(t(n,{start:d,end:i=m,value:h}))}return i{let o=n.data;switch(o.type){case 1:na.value=!0;break;case 3:typeof o.data.pagination.prev<"u"?it.value=He(H({},it.value),{pagination:o.data.pagination,items:[...it.value.items,...o.data.items]}):(it.value=o.data,so(0));break}},qt(()=>{lr.value&&r.postMessage({type:0,data:lr.value})}),qt(()=>{na.value&&r.postMessage({type:2,data:Se.value})})}var oa={container:"p",hidden:"m"};function ku(e){return z("div",{class:zt(oa.container,{[oa.hidden]:e.hidden}),onClick:()=>Dt()})}var ia={container:"r",disabled:"c"};function co(e){return z("button",{class:zt(ia.container,{[ia.disabled]:!e.onClick}),onClick:e.onClick,children:e.children})}var aa=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Au=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),sa=e=>{let t=Au(e);return t.charAt(0).toUpperCase()+t.slice(1)},Cu=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),Hu={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},$u=c=>{var l=c,{color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,children:o,iconNode:i,class:a=""}=l,s=gr(l,["color","size","strokeWidth","absoluteStrokeWidth","children","iconNode","class"]);return Wt("svg",H(He(H({},Hu),{width:String(t),height:t,stroke:e,"stroke-width":n?Number(r)*24/Number(t):r,class:["lucide",a].join(" ")}),s),[...i.map(([u,p])=>Wt(u,p)),...Cr(o)])},bo=(e,t)=>{let r=a=>{var s=a,{class:n="",children:o}=s,i=gr(s,["class","children"]);return Wt($u,He(H({},i),{iconNode:t,class:Cu(`lucide-${aa(sa(e))}`,`lucide-${aa(e)}`,n)}),o)};return r.displayName=sa(e),r},Pu=bo("corner-down-left",[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4",key:"6o5b7l"}],["path",{d:"m9 10-5 5 5 5",key:"1kshq7"}]]),Iu=bo("list-filter",[["path",{d:"M2 5h20",key:"1fs1ex"}],["path",{d:"M6 12h12",key:"8npq4p"}],["path",{d:"M9 19h6",key:"456am0"}]]),Ru=bo("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Gx=hl(vl(),1);function ju({threshold:e=0,root:t=null,rootMargin:r="0%",freezeOnceVisible:n=!1,initialIsIntersecting:o=!1,onChange:i}={}){var a;let[s,c]=bn(null),[l,u]=bn(()=>({isIntersecting:o,entry:void 0})),p=Vt();p.current=i;let d=((a=l.entry)==null?void 0:a.isIntersecting)&&n;mt(()=>{if(!s||!("IntersectionObserver"in window)||d)return;let v,x=new IntersectionObserver(w=>{let E=Array.isArray(x.thresholds)?x.thresholds:[x.thresholds];w.forEach(_=>{let de=_.isIntersecting&&E.some(be=>_.intersectionRatio>=be);u({isIntersecting:de,entry:_}),p.current&&p.current(de,_),de&&n&&v&&(v(),v=void 0)})},{threshold:e,root:t,rootMargin:r});return x.observe(s),()=>{x.disconnect()}},[s,JSON.stringify(e),t,r,d,n]);let m=Vt(null);mt(()=>{var v;!s&&(v=l.entry)!=null&&v.target&&!n&&!d&&m.current!==l.entry.target&&(m.current=l.entry.target,u({isIntersecting:o,entry:void 0}))},[s,l.entry,n,d,o]);let h=[c,!!l.isIntersecting,l.entry];return h.ref=h[0],h.isIntersecting=h[1],h.entry=h[2],h}var lt={container:"n",hidden:"l",content:"u",pop:"d",badge:"y",sidebar:"i",controls:"w",results:"k",loadmore:"z"};function Fu(e){let{isIntersecting:t,ref:r}=ju({threshold:0});mt(()=>{t&&cu()},[t]);let n=Vt(null);mt(()=>{n.current&&typeof Se.value.page>"u"&&n.current.scrollTo({top:0,behavior:"smooth"})},[Se.value]);let o=za();return z("div",{class:zt(lt.container,{[lt.hidden]:e.hidden}),children:[z("div",{class:lt.content,children:[z("div",{class:lt.controls,children:[z(co,{onClick:Dt,children:z(Ru,{})}),z(Nu,{focus:!e.hidden}),z(co,{onClick:Wa,children:[z(Iu,{}),o.length>0&&z("span",{class:lt.badge,children:o.length})]})]}),z("div",{class:lt.results,ref:n,children:[z(Du,{keyboard:!e.hidden}),z("div",{class:lt.loadmore,ref:r})]})]}),z("div",{class:zt(lt.sidebar,{[lt.hidden]:du()}),children:z(Uu,{})})]})}var Tt={container:"X",list:"j",heading:"F",title:"I",item:"o",active:"g",value:"R",count:"q"};function Uu(e){let t=mu();return t.sort((r,n)=>n.node.count-r.node.count),z("div",{class:Tt.container,children:[z("h3",{class:Tt.heading,children:"Filters"}),z("h4",{class:Tt.title,children:"Tags"}),z("ol",{class:Tt.list,children:t.map(r=>z("li",{class:zt(Tt.item,{[Tt.active]:lu(r.node.value)}),onClick:()=>uu(r.node.value),children:[z("span",{class:Tt.value,children:r.node.value}),z("span",{class:Tt.count,children:r.node.count})]}))})]})}var ca={container:"f"};function Nu(e){let t=Vt(null);return mt(()=>{var r,n;e.focus?(r=t.current)==null||r.focus():(n=t.current)==null||n.blur()},[e.focus]),z("div",{class:ca.container,children:z("input",{ref:t,type:"text",class:ca.content,value:hn(wn()),onInput:r=>Va(yu(r.currentTarget.value)),autocapitalize:"off",autocomplete:"off",autocorrect:"off",placeholder:"Search",spellcheck:!1,role:"combobox"})})}var ut={container:"b",heading:"A",item:"a",active:"h",wrapper:"B",actions:"s",title:"x",path:"t"};function Ga(){let[e,t]=bn(!1);return mt(()=>{let r=()=>t(!0),n=()=>t(!1);return document.addEventListener("compositionstart",r),document.addEventListener("compositionend",n),()=>{document.removeEventListener("compositionstart",r),document.removeEventListener("compositionend",n)}},[]),e}function Du(e){var s;let t=su(),r=pu(),n=dn(),o=Vt([]),i=Ga();mt(()=>{let c=o.current[n];c&&c.scrollIntoView({block:"center",behavior:"smooth"})},[n]),Aa(e.keyboard,c=>{if(i)return;let l=dn();c.key==="ArrowDown"?(c.preventDefault(),so(Math.min(l+1,r.length-1))):c.key==="ArrowUp"&&(c.preventDefault(),so(Math.max(l-1,0)))},[e.keyboard,i]);let a=(s=fu())!=null?s:0;return z(ft,{children:[r.length>0&&z("h3",{class:ut.heading,children:[z("span",{class:ut.bubble,children:new Intl.NumberFormat("en-US").format(a)})," ","results"]}),z("ol",{class:ut.container,children:r.map((c,l)=>{var m;let u=Ba(t[c.id].title,c.matches.find(({field:h})=>h==="title")),p=Mu((m=t[c.id].path)!=null?m:[],c.matches.find(({field:h})=>h==="path")),d=t[c.id].location;if(qa()){let h=encodeURIComponent(wn()),[v,x]=d.split("#",2);d=`${v}?h=${h.replace(/%20/g,"+")}`,typeof x<"u"&&(d+=`#${x}`)}return z("li",{children:z("a",{ref:h=>{o.current[l]=h},href:d,onClick:()=>Dt(),class:zt(ut.item,{[ut.active]:l===dn()}),children:[z("div",{class:ut.wrapper,children:[z("h2",{class:ut.title,children:u}),z("menu",{class:ut.path,children:p.map(h=>z("li",{children:h}))})]}),z("nav",{class:ut.actions,children:z(co,{children:z(Pu,{})})})]})})})})]})}var Wu={container:"e"};function Vu(e){let t=Ga();return Aa(!0,r=>{var n,o,i,a,s;if(!t)if((r.metaKey||r.ctrlKey)&&r.key==="k")r.preventDefault(),Dt();else if((r.metaKey||r.ctrlKey)&&r.key==="j")document.body.classList.toggle("dark");else if(r.key==="Enter"&&!sr()){r.preventDefault();let c=dn(),l=(o=(n=it.value)==null?void 0:n.items[c])==null?void 0:o.id;if((a=(i=lr.value)==null?void 0:i.items[l])!=null&&a.location){Dt();let u=(s=lr.value)==null?void 0:s.items[l].location;if(qa()){let p=encodeURIComponent(wn()),[d,m]=u.split("#",2);u=`${d}?h=${p.replace(/%20/g,"+")}`,typeof m<"u"&&(u+=`#${m}`)}window.location.href=u}}else r.key==="Escape"&&!sr()&&(r.preventDefault(),Dt())},[t]),z("div",{class:Wu.container,children:[z(ku,{hidden:sr()}),z(Fu,{hidden:sr()})]})}function Ja(e,t){au(e),El(z(Vu,{}),t)}function go(){Dt()}function zu(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function qu(){return R(b(window,"compositionstart").pipe(f(()=>!0)),b(window,"compositionend").pipe(f(()=>!1))).pipe(J(!1))}function Xa(){let e=b(window,"keydown").pipe(f(t=>({mode:sr()?"global":"search",type:t.key,meta:t.ctrlKey||t.metaKey,claim(){t.preventDefault(),t.stopPropagation()}})),L(({mode:t,type:r})=>{if(t==="global"){let n=xt();if(typeof n!="undefined")return!zu(n,r)}return!0}),xe());return qu().pipe(g(t=>t?y:e))}function Ye(){return new URL(location.href)}function dt(e,t=!1){if(X("navigation.instant")&&!t){let r=A("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function Za(){return new I}function Qa(){return location.hash.slice(1)}function es(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function _o(e){return R(b(window,"hashchange"),e).pipe(f(Qa),J(Qa()),L(t=>t.length>0),se(1))}function ts(e){return _o(e).pipe(f(t=>Le(`[id="${t}"]`)),L(t=>typeof t!="undefined"))}function Ir(e){let t=matchMedia(e);return an(r=>t.addListener(()=>r(t.matches))).pipe(J(t.matches))}function rs(){let e=matchMedia("print");return R(b(window,"beforeprint").pipe(f(()=>!0)),b(window,"afterprint").pipe(f(()=>!1))).pipe(J(e.matches))}function yo(e,t){return e.pipe(g(r=>r?t():y))}function xo(e,t){return new U(r=>{let n=new XMLHttpRequest;return n.open("GET",`${e}`),n.responseType="blob",n.addEventListener("load",()=>{n.status>=200&&n.status<300?(r.next(n.response),r.complete()):r.error(new Error(n.statusText))}),n.addEventListener("error",()=>{r.error(new Error("Network error"))}),n.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(n.addEventListener("progress",o=>{var i;if(o.lengthComputable)t.progress$.next(o.loaded/o.total*100);else{let a=(i=n.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(o.loaded/+a*100)}}),t.progress$.next(5)),n.send(),()=>n.abort()})}function et(e,t){return xo(e,t).pipe(g(r=>r.text()),f(r=>JSON.parse(r)),se(1))}function En(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/html")),se(1))}function ns(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/xml")),se(1))}var wo={drawer:G("[data-md-toggle=drawer]"),search:G("[data-md-toggle=search]")};function Eo(e,t){wo[e].checked!==t&&wo[e].click()}function Tn(e){let t=wo[e];return b(t,"change").pipe(f(()=>t.checked),J(t.checked))}function os(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function is(){return R(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(f(os),J(os()))}function as(){return{width:innerWidth,height:innerHeight}}function ss(){return b(window,"resize",{passive:!0}).pipe(f(as),J(as()))}function cs(){return re([is(),ss()]).pipe(f(([e,t])=>({offset:e,size:t})),se(1))}function Sn(e,{viewport$:t,header$:r}){let n=t.pipe(fe("size")),o=re([n,r]).pipe(f(()=>wt(e)));return re([r,t,o]).pipe(f(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}var Ku=G("#__config"),mr=JSON.parse(Ku.textContent);mr.base=`${new URL(mr.base,Ye())}`;function Ue(){return mr}function X(e){return mr.features.includes(e)}function Bt(e,t){return typeof t!="undefined"?mr.translations[e].replace("#",t.toString()):mr.translations[e]}function ht(e,t=document){return G(`[data-md-component=${e}]`,t)}function Ee(e,t=document){return P(`[data-md-component=${e}]`,t)}function Bu(e){let t=G(".md-typeset > :first-child",e);return b(t,"click",{once:!0}).pipe(f(()=>G(".md-typeset",e)),f(r=>({hash:__md_hash(r.innerHTML)})))}function ls(e){if(!X("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=G(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return j(()=>{let t=new I;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Bu(e).pipe($(r=>t.next(r)),V(()=>t.complete()),f(r=>H({ref:e},r)))})}function Yu(e,{target$:t}){return t.pipe(f(r=>({hidden:r!==e})))}function us(e,t){let r=new I;return r.subscribe(({hidden:n})=>{e.hidden=n}),Yu(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))}function To(e,t){return t==="inline"?A("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})):A("div",{class:"md-tooltip",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"}))}function On(...e){return A("div",{class:"md-tooltip2",role:"dialog"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function ps(...e){return A("div",{class:"md-tooltip2",role:"tooltip"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function fs(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("a",{href:r,class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}else return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("span",{class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}function ms(e){return A("button",{class:"md-code__button",title:Bt("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function ds(){return A("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function hs(){return A("nav",{class:"md-code__nav"})}var Xu=_r(So());function bs(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Li(r):r)))}function Oo(e){let t=`tabbed-control tabbed-control--${e}`;return A("div",{class:t,hidden:!0},A("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function gs(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function Zu(e){var n;let t=Ue(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:`${r}`,class:"md-version__link"},e.title,((n=t.version)==null?void 0:n.alias)&&e.aliases.length>0&&A("span",{class:"md-version__alias"},e.aliases[0])))}function _s(e,t){var n;let r=Ue();return e=e.filter(o=>{var i;return!((i=o.properties)!=null&&i.hidden)}),A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":Bt("select.version")},t.title,((n=r.version)==null?void 0:n.alias)&&t.aliases.length>0&&A("span",{class:"md-version__alias"},t.aliases[0])),A("ul",{class:"md-version__list"},e.map(Zu)))}var Qu=0;function ep(e,t=250){let r=re([ir(e),Ft(e,t)]).pipe(f(([o,i])=>o||i),ie()),n=j(()=>Ai(e)).pipe(oe(Ut),Lr(1),Ze(r),f(()=>Ci(e)));return r.pipe(Sr(o=>o),g(()=>re([r,n])),f(([o,i])=>({active:o,offset:i})),xe())}function Rr(e,t,r=250){let{content$:n,viewport$:o}=t,i=`__tooltip2_${Qu++}`;return j(()=>{let a=new I,s=new Un(!1);a.pipe(he(),ye(!1)).subscribe(s);let c=s.pipe(Tr(u=>Ve(+!u*250,Wn)),ie(),g(u=>u?n:y),$(u=>u.id=i),xe());re([a.pipe(f(({active:u})=>u)),c.pipe(g(u=>Ft(u,250)),J(!1))]).pipe(f(u=>u.some(p=>p))).subscribe(s);let l=s.pipe(L(u=>u),pe(c,o),f(([u,p,{size:d}])=>{let m=e.getBoundingClientRect(),h=m.width/2;if(p.role==="tooltip")return{x:h,y:8+m.height};if(m.y>=d.height/2){let{height:v}=Ae(p);return{x:h,y:-16-v}}else return{x:h,y:16+m.height}}));return re([c,a,l]).subscribe(([u,{offset:p},d])=>{u.style.setProperty("--md-tooltip-host-x",`${p.x}px`),u.style.setProperty("--md-tooltip-host-y",`${p.y}px`),u.style.setProperty("--md-tooltip-x",`${d.x}px`),u.style.setProperty("--md-tooltip-y",`${d.y}px`),u.classList.toggle("md-tooltip2--top",d.y<0),u.classList.toggle("md-tooltip2--bottom",d.y>=0)}),s.pipe(L(u=>u),pe(c,(u,p)=>p),L(u=>u.role==="tooltip")).subscribe(u=>{let p=Ae(G(":scope > *",u));u.style.setProperty("--md-tooltip-width",`${p.width}px`),u.style.setProperty("--md-tooltip-tail","0px")}),s.pipe(ie(),Ie(je),pe(c)).subscribe(([u,p])=>{p.classList.toggle("md-tooltip2--active",u)}),re([s.pipe(L(u=>u)),c]).subscribe(([u,p])=>{p.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),s.pipe(L(u=>!u)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ep(e,r).pipe($(u=>a.next(u)),V(()=>a.complete()),f(u=>H({ref:e},u)))})}function Ge(e,{viewport$:t},r=document.body){return Rr(e,{content$:new U(n=>{let o=e.title,i=ps(o);return n.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",o)}}),viewport$:t},0)}function tp(e,t){let r=j(()=>re([Hi(e),Ut(t)])).pipe(f(([{x:n,y:o},i])=>{let{width:a,height:s}=Ae(e);return{x:n-i.x+a/2,y:o-i.y+s/2}}));return ir(e).pipe(g(n=>r.pipe(f(o=>({active:n,offset:o})),Me(+!n||1/0))))}function ys(e,t,{target$:r}){let[n,o]=Array.from(e.children);return j(()=>{let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),Et(e).pipe(Q(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),R(i.pipe(L(({active:s})=>s)),i.pipe(Be(250),L(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Xe(16,je)).subscribe(({active:s})=>{n.classList.toggle("md-tooltip--active",s)}),i.pipe(Lr(125,je),L(()=>!!e.offsetParent),f(()=>e.offsetParent.getBoundingClientRect()),f(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),b(o,"click").pipe(Q(a),L(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),b(o,"mousedown").pipe(Q(a),pe(i)).subscribe(([s,{active:c}])=>{var l;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(l=xt())==null||l.blur()}}),r.pipe(Q(a),L(s=>s===n),It(125)).subscribe(()=>e.focus()),tp(e,t).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function rp(e){let t=Ue();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate){let n=e.closest("[class|=language]");if(n)for(let o of Array.from(n.classList)){if(!o.startsWith("language-"))continue;let[,i]=o.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return P(r.join(", "),e)}function np(e){let t=[];for(let r of rp(e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let l=i.splitText(a.index);i=l.splitText(s.length),t.push(l)}else{i.textContent=s,t.push(i);break}}}}return t}function xs(e,t){t.append(...Array.from(e.childNodes))}function Ln(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let s of np(t)){let[,c]=s.textContent.match(/\((\d+)\)/);Le(`:scope > li:nth-child(${c})`,e)&&(a.set(c,fs(c,i)),s.replaceWith(a.get(c)))}return a.size===0?y:j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=[];for(let[u,p]of a)l.push([G(".md-typeset",p),G(`:scope > li:nth-child(${u})`,e)]);return n.pipe(Q(c)).subscribe(u=>{e.hidden=!u,e.classList.toggle("md-annotation-list",u);for(let[p,d]of l)u?xs(p,d):xs(d,p)}),R(...[...a].map(([,u])=>ys(u,t,{target$:r}))).pipe(V(()=>s.complete()),xe())})}function ws(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return ws(t)}}function Es(e,t){return j(()=>{let r=ws(e);return typeof r!="undefined"?Ln(r,e,t):y})}var Ss=_r(Mo());var op=0,Ts=R(b(window,"keydown").pipe(f(()=>!0)),R(b(window,"keyup"),b(window,"contextmenu")).pipe(f(()=>!1))).pipe(J(!1),se(1));function Os(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Os(t)}}function ip(e){return Re(e).pipe(f(({width:t})=>({scrollable:Mr(e).width>t})),fe("scrollable"))}function Ls(e,t){let{matches:r}=matchMedia("(hover)"),n=j(()=>{let o=new I,i=o.pipe(Gn(1));o.subscribe(({scrollable:m})=>{m&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[],s=e.closest("pre"),c=s.closest("[id]"),l=c?c.id:op++;s.id=`__code_${l}`;let u=[],p=e.closest(".highlight");if(p instanceof HTMLElement){let m=Os(p);if(typeof m!="undefined"&&(p.classList.contains("annotate")||X("content.code.annotate"))){let h=Ln(m,e,t);u.push(Re(p).pipe(Q(i),f(({width:v,height:x})=>v&&x),ie(),g(v=>v?h:y)))}}let d=P(":scope > span[id]",e);if(d.length&&(e.classList.add("md-code__content"),e.closest(".select")||X("content.code.select")&&!e.closest(".no-select"))){let m=+d[0].id.split("-").pop(),h=ds();a.push(h),X("content.tooltips")&&u.push(Ge(h,{viewport$}));let v=b(h,"click").pipe(Or(M=>!M,!1),$(()=>h.blur()),xe());v.subscribe(M=>{h.classList.toggle("md-code__button--active",M)});let x=me(d).pipe(oe(M=>Ft(M).pipe(f(O=>[M,O]))));v.pipe(g(M=>M?x:y)).subscribe(([M,O])=>{let N=Le(".hll.select",M);if(N&&!O)N.replaceWith(...Array.from(N.childNodes));else if(!N&&O){let ee=document.createElement("span");ee.className="hll select",ee.append(...Array.from(M.childNodes).slice(1)),M.append(ee)}});let w=me(d).pipe(oe(M=>b(M,"mousedown").pipe($(O=>O.preventDefault()),f(()=>M)))),E=v.pipe(g(M=>M?w:y),pe(Ts),f(([M,O])=>{var ee;let N=d.indexOf(M)+m;if(O===!1)return[N,N];{let le=P(".hll",e).map(ce=>d.indexOf(ce.parentElement)+m);return(ee=window.getSelection())==null||ee.removeAllRanges(),[Math.min(N,...le),Math.max(N,...le)]}})),_=_o(y).pipe(L(M=>M.startsWith(`__codelineno-${l}-`)));_.subscribe(M=>{let[,,O]=M.split("-"),N=O.split(":").map(le=>+le-m+1);N.length===1&&N.push(N[0]);for(let le of P(".hll:not(.select)",e))le.replaceWith(...Array.from(le.childNodes));let ee=d.slice(N[0]-1,N[1]);for(let le of ee){let ce=document.createElement("span");ce.className="hll",ce.append(...Array.from(le.childNodes).slice(1)),le.append(ce)}}),_.pipe(Me(1),Ie(ge)).subscribe(M=>{if(M.includes(":")){let O=document.getElementById(M.split(":")[0]);O&&setTimeout(()=>{let N=O,ee=-64;for(;N!==document.body;)ee+=N.offsetTop,N=N.offsetParent;window.scrollTo({top:ee})},1)}});let be=me(P('a[href^="#__codelineno"]',p)).pipe(oe(M=>b(M,"click").pipe($(O=>O.preventDefault()),f(()=>M)))).pipe(Q(i),pe(Ts),f(([M,O])=>{let ee=+G(`[id="${M.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(O===!1)return[ee,ee];{let le=P(".hll",e).map(ce=>+ce.parentElement.id.split("-").pop());return[Math.min(ee,...le),Math.max(ee,...le)]}}));R(E,be).subscribe(M=>{let O=`#__codelineno-${l}-`;M[0]===M[1]?O+=M[0]:O+=`${M[0]}:${M[1]}`,history.replaceState({},"",O),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+O,oldURL:window.location.href}))})}if(Ss.default.isSupported()&&(e.closest(".copy")||X("content.code.copy")&&!e.closest(".no-copy"))){let m=ms(s.id);a.push(m),X("content.tooltips")&&u.push(Ge(m,{viewport$}))}if(a.length){let m=hs();m.append(...a),s.insertBefore(m,e)}return ip(e).pipe($(m=>o.next(m)),V(()=>o.complete()),f(m=>H({ref:e},m)),Rt(R(...u).pipe(Q(i))))});return X("content.lazy")?Et(e).pipe(L(o=>o),Me(1),g(()=>n)):n}function ap(e,{target$:t,print$:r}){let n=!0;return R(t.pipe(f(o=>o.closest("details:not([open])")),L(o=>e===o),f(()=>({action:"open",reveal:!0}))),r.pipe(L(o=>o||!n),$(()=>n=e.open),f(o=>({action:o?"open":"close"}))))}function Ms(e,t){return j(()=>{let r=new I;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),ap(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}var ks=0,As=new Map;function sp(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],n=e.nextElementSibling;for(;n&&!(n instanceof HTMLHeadingElement);)r.push(n.cloneNode(!0)),n=n.nextElementSibling;return r}function cp(e,t){for(let r of P("[href], [src]",e))for(let n of["href","src"]){let o=r.getAttribute(n);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){r[n]=new URL(r.getAttribute(n),t).toString();break}}for(let r of P("[name^=__], [for]",e))for(let n of["id","for","name"]){let o=r.getAttribute(n);o&&r.setAttribute(n,`${o}$preview_${ks}`)}return ks++,Y(e)}function lp(e){let t=As.get(e.toString());return t?Y(t):En(e).pipe(g(r=>cp(r,e)),f(r=>(As.set(e.toString(),r),r)))}function Cs(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(X("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let n=re([ir(e),Ft(e).pipe(ke(1))]).pipe(f(([i,a])=>i||a),ie(),L(i=>i));return $t([r,n]).pipe(g(([i])=>{let a=new URL(e.href);return a.search=a.hash="",i.has(`${a}`)?Y(a):y}),g(i=>lp(i)),g(i=>{let a=e.hash?`article [id="${decodeURIComponent(e.hash.slice(1))}"]`:"article h1",s=Le(a,i);return typeof s=="undefined"?y:Y(sp(s))})).pipe(g(i=>{let a=new U(s=>{let c=On(...i);return s.next(c),document.body.append(c),()=>c.remove()});return Rr(e,H({content$:a},t))}))}var Hs=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var ko,pp=0;function fp(){return typeof mermaid=="undefined"||mermaid instanceof Element?ar("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):Y(void 0)}function $s(e){return e.classList.remove("mermaid"),ko||(ko=fp().pipe($(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Hs,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),f(()=>{}),se(1))),ko.subscribe(()=>Uo(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${pp++}`,r=A("div",{class:"mermaid"}),n=e.textContent,{svg:o,fn:i}=yield mermaid.render(t,n),a=r.attachShadow({mode:"closed"});a.innerHTML=o,e.replaceWith(r),i==null||i(a)})),ko.pipe(f(()=>({ref:e})))}var Ps=A("table");function Is(e){return e.replaceWith(Ps),Ps.replaceWith(gs(e)),Y({ref:e})}function mp(e){let t=e.find(r=>r.checked)||e[0];return R(...e.map(r=>b(r,"change").pipe(f(()=>G(`label[for="${r.id}"]`))))).pipe(J(G(`label[for="${t.id}"]`)),f(r=>({active:r})))}function Rs(e,{viewport$:t,target$:r}){let n=G(".tabbed-labels",e),o=P(":scope > input",e),i=Oo("prev");e.append(i);let a=Oo("next");return e.append(a),j(()=>{let s=new I,c=s.pipe(he(),ye(!0));re([s,Re(e),Et(e)]).pipe(Q(c),Xe(1,je)).subscribe({next([{active:l},u]){let p=wt(l),{width:d}=Ae(l);e.style.setProperty("--md-indicator-x",`${p.x}px`),e.style.setProperty("--md-indicator-width",`${d}px`);let m=ln(n);(p.xm.x+u.width)&&n.scrollTo({left:Math.max(0,p.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),re([Ut(n),Re(n)]).pipe(Q(c)).subscribe(([l,u])=>{let p=Mr(n);i.hidden=l.x<16,a.hidden=l.x>p.width-u.width-16}),R(b(i,"click").pipe(f(()=>-1)),b(a,"click").pipe(f(()=>1))).pipe(Q(c)).subscribe(l=>{let{width:u}=Ae(n);n.scrollBy({left:u*l,behavior:"smooth"})}),r.pipe(Q(c),L(l=>o.includes(l))).subscribe(l=>l.click()),n.classList.add("tabbed-labels--linked");for(let l of o){let u=G(`label[for="${l.id}"]`);u.replaceChildren(A("a",{href:`#${u.htmlFor}`,tabIndex:-1},...Array.from(u.childNodes))),b(u.firstElementChild,"click").pipe(Q(c),L(p=>!(p.metaKey||p.ctrlKey)),$(p=>{p.preventDefault(),p.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${u.htmlFor}`),u.click()})}return X("content.tabs.link")&&s.pipe(ke(1),pe(t)).subscribe(([{active:l},{offset:u}])=>{let p=l.innerText.trim();if(l.hasAttribute("data-md-switching"))l.removeAttribute("data-md-switching");else{let d=e.offsetTop-u.y;for(let h of P("[data-tabs]"))for(let v of P(":scope > input",h)){let x=G(`label[for="${v.id}"]`);if(x!==l&&x.innerText.trim()===p){x.setAttribute("data-md-switching",""),v.click();break}}window.scrollTo({top:e.offsetTop-d});let m=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([p,...m])])}}),s.pipe(Q(c)).subscribe(()=>{for(let l of P("audio, video",e))l.offsetWidth&&l.autoplay?l.play().catch(()=>{}):l.pause()}),mp(o).pipe($(l=>s.next(l)),V(()=>s.complete()),f(l=>H({ref:e},l)))}).pipe(Ht(ge))}function js(e,t){let{viewport$:r,target$:n,print$:o}=t;return R(...P(".annotate:not(.highlight)",e).map(i=>Es(i,{target$:n,print$:o})),...P("pre:not(.mermaid) > code",e).map(i=>Ls(i,{target$:n,print$:o})),...P("a",e).map(i=>Cs(i,t)),...P("pre.mermaid",e).map(i=>$s(i)),...P("table:not([class])",e).map(i=>Is(i)),...P("details",e).map(i=>Ms(i,{target$:n,print$:o})),...P("[data-tabs]",e).map(i=>Rs(i,{viewport$:r,target$:n})),...P("[title]:not([data-preview])",e).filter(()=>X("content.tooltips")).map(i=>Ge(i,{viewport$:r})),...P(".footnote-ref",e).filter(()=>X("content.footnote.tooltips")).map(i=>Rr(i,{content$:new U(a=>{let s=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(s).cloneNode(!0).children),l=On(...c);return a.next(l),document.body.append(l),()=>l.remove()}),viewport$:r})))}function dp(e,{alert$:t}){return t.pipe(g(r=>R(Y(!0),Y(!1).pipe(It(2e3))).pipe(f(n=>({message:r,active:n})))))}function Fs(e,t){let r=G(".md-typeset",e);return j(()=>{let n=new I;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),dp(e,t).pipe($(o=>n.next(o)),V(()=>n.complete()),f(o=>H({ref:e},o)))})}function hp({viewport$:e}){if(!X("header.autohide"))return Y(!1);let t=e.pipe(f(({offset:{y:o}})=>o),Pt(2,1),f(([o,i])=>[oMath.abs(i-o.y)>100),f(([,[o]])=>o),ie()),n=Tn("search");return re([e,n]).pipe(f(([{offset:o},i])=>o.y>400&&!i),ie(),g(o=>o?r:Y(!1)),J(!1))}function Us(e,t){return j(()=>re([Re(e),hp(t)])).pipe(f(([{height:r},n])=>({height:r,hidden:n})),ie((r,n)=>r.height===n.height&&r.hidden===n.hidden),se(1))}function Ns(e,{viewport$:t,header$:r,main$:n}){return j(()=>{let o=new I,i=o.pipe(he(),ye(!0));o.pipe(fe("active"),Ze(r)).subscribe(([{active:s},{hidden:c}])=>{e.classList.toggle("md-header--shadow",s&&!c),e.hidden=c});let a=me(P("[title]",e)).pipe(L(()=>X("content.tooltips")),oe(s=>Ge(s,{viewport$:t})));return n.subscribe(o),r.pipe(Q(i),f(s=>H({ref:e},s)),Rt(a.pipe(Q(i))))})}function vp(e,{viewport$:t,header$:r}){return Sn(e,{viewport$:t,header$:r}).pipe(f(({offset:{y:n}})=>{let{height:o}=Ae(e);return{active:o>0&&n>=o}}),fe("active"))}function Ds(e,t){return j(()=>{let r=new I;r.subscribe({next({active:o}){e.classList.toggle("md-header__title--active",o)},complete(){e.classList.remove("md-header__title--active")}});let n=Le(".md-content h1");return typeof n=="undefined"?y:vp(n,t).pipe($(o=>r.next(o)),V(()=>r.complete()),f(o=>H({ref:e},o)))})}function Ws(e,{viewport$:t,header$:r}){let n=r.pipe(f(({height:i})=>i),ie()),o=n.pipe(g(()=>Re(e).pipe(f(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),fe("bottom"))));return re([n,o,t]).pipe(f(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),ie((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function bp(e){let t=__md_get("__palette")||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return Y(...e).pipe(oe(n=>b(n,"change").pipe(f(()=>n))),J(e[r]),f(n=>({index:e.indexOf(n),color:{media:n.getAttribute("data-md-color-media"),scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),se(1))}function Vs(e){let t=P("input",e),r=A("meta",{name:"theme-color"});document.head.appendChild(r);let n=A("meta",{name:"color-scheme"});document.head.appendChild(n);let o=Ir("(prefers-color-scheme: light)");return j(()=>{let i=new I;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),pe(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(f(()=>{let a=ht("header"),s=window.getComputedStyle(a);return n.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Ie(ge)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),bp(t).pipe(Q(o.pipe(ke(1))),jt(),$(a=>i.next(a)),V(()=>i.complete()),f(a=>H({ref:e},a)))})}function zs(e,{progress$:t}){return j(()=>{let r=new I;return r.subscribe(({value:n})=>{e.style.setProperty("--md-progress-value",`${n}`)}),t.pipe($(n=>r.next({value:n})),V(()=>r.complete()),f(n=>({ref:e,value:n})))})}var qs='.v u{text-decoration:underline!important;text-decoration-style:wavy!important;text-decoration-thickness:1px!important}.p{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-backdrop)/var(--alpha-lighter));cursor:pointer;height:100%;pointer-events:auto;position:absolute;transition:opacity .25s;width:100%}.p.m{opacity:0;pointer-events:none;transition:opacity .35s}.r{align-items:center;background-color:initial;border:none;border-radius:var(--space-2);cursor:pointer;display:flex;flex-shrink:0;font-family:var(--font-family);height:36px;justify-content:center;outline:none;padding:0;position:relative;transition:background-color .25s,color .25s;width:36px;z-index:1}.r svg{stroke:rgb(var(--color-foreground));height:18px;opacity:.5;width:18px}.r:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.r:hover:before{opacity:1;transform:scale(1)}.r.c{cursor:auto}.r.c:before{display:none}.n{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background)/var(--alpha-light));border-radius:var(--space-3);box-shadow:0 0 60px #0000000d;display:flex;height:480px;overflow:hidden;pointer-events:auto;position:absolute;transition:transform .25s cubic-bezier(.16,1,.3,1),opacity .25s;width:640px}.n.l{opacity:0;pointer-events:none;transform:scale(1.1);transition:transform .25s .15s,opacity .15s}@media (max-width:680px){.n{border-radius:0;height:100%;width:100%}}.u{display:flex;flex-basis:min-content;flex-direction:column;flex-grow:1;flex-shrink:0}@keyframes d{0%{transform:scale(0)}50%{transform:scale(1.2)}to{transform:scale(1)}}.y{animation:d .25s ease-in-out;background:var(--color-highlight);border-radius:100%;color:#fff;font-size:8px;font-weight:700;height:12px;padding-top:1px;position:absolute;right:4px;top:4px;width:12px}.i{background-color:rgb(var(--color-background-subtle)/var(--alpha-lighter));flex-shrink:0;overflow:scroll;position:relative;transition:width .35s cubic-bezier(.16,1,.3,1),opacity .25s;width:200px}.i>*{transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.i.l{opacity:0;width:0}.i.l>*{transform:translate(-48px)}@media (max-width:680px){.i{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background-subtle)/var(--alpha-light));box-shadow:0 0 60px #00000026;height:100%;position:absolute;right:0;top:0}}.w{border-bottom:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));display:flex;gap:var(--space-1);padding:var(--space-2)}.k{-webkit-overflow-scrolling:touch;overflow:auto;overscroll-behavior:contain}.z{padding:8px 10px}.X{color:rgb(var(--color-foreground)/var(--alpha-light));padding:var(--space-2);position:absolute;width:200px}.X,.j{display:flex;flex-direction:column}.j{gap:2px;list-style:none;padding:0}.F,.j{margin:0}.F{font-size:16px;font-weight:400}.F,.I{padding:8px}.I{font-size:14px;margin:4px 0 0;opacity:.5}.I,.o{font-size:12px}.o{cursor:pointer;display:flex;padding:4px 8px;position:relative}.o:before{background-color:var(--color-highlight-transparent);border-radius:var(--space-1);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.o.g:before,.o:hover:before{opacity:1;transform:scale(1)}.o.g,.o:hover{color:var(--color-highlight)}.R{flex-grow:1}.R,.q{position:relative}.q{font-weight:700}.f{flex-grow:1}.f input{background:#0000;border:none;color:rgb(var(--color-foreground));font-family:var(--font-family);font-size:16px;height:100%;letter-spacing:-.25px;outline:none;width:100%}.b{color:rgb(var(--color-foreground)/var(--alpha-light));display:flex;flex-direction:column;gap:2px;line-height:1.3;list-style:none;margin:var(--space-2);margin-top:0;padding:0}.A,.b li{margin:0}.A{color:rgb(var(--color-foreground)/var(--alpha-lighter));font-size:12px;margin-top:var(--space-2);padding:0 18px}.a{border-radius:var(--space-2);color:inherit;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;padding:8px 10px;position:relative;text-decoration:none}.a:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";display:block;inset:0;opacity:0;position:absolute;transform:scale(.9);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.a.h:before,.a:hover:before{opacity:1;transform:scale(1)}}.a mark{background:#0000;color:var(--color-highlight)}.a u{background-color:var(--color-highlight-transparent);border-radius:2px;box-shadow:0 0 0 1px var(--color-highlight-transparent);text-decoration:none}.B{flex-grow:1}.s{margin-right:-8px;opacity:0;position:relative;transform:translate(-2px);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.h>.s,:hover>.s{opacity:1;transform:none}}.x{font-size:14px;margin:0;position:relative}.x code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);font-size:13px;padding:2px 4px}.t{color:rgb(var(--color-foreground)/var(--alpha-lighter));display:inline-flex;flex-wrap:wrap;font-size:12px;gap:var(--space-1);list-style:none;margin:0;padding:0;position:relative}.t li{white-space:nowrap}.t li:after{content:"/";display:inline;margin-left:var(--space-1)}.t li:last-child:after{content:"";display:none}.e{--space-1:4px;--space-2:calc(var(--space-1)*2);--space-3:calc(var(--space-2)*2);--space-4:calc(var(--space-3)*2);--space-5:calc(var(--space-4)*2);--alpha-light:.7;--alpha-lighter:.54;--alpha-lightest:.1;--color-highlight:var(--md-accent-fg-color,#526cfe);--color-highlight-transparent:var(--md-accent-fg-color--transparent,#526cfe1a);--border-radius-1:var(--space-1);--border-radius-2:var(--space-2);--border-radius-3:calc(var(--space-1) + var(--space-2));--font-family:var(--md-text-font-family,Inter,Roboto Flex,system-ui,sans-serif);--font-size:16px;--line-height:1.5;--letter-spacing:-.5px;-webkit-font-smoothing:antialiased;align-items:center;display:flex;font-family:var(--font-family);font-size:var(--font-size);height:100vh;justify-content:center;letter-spacing:var(--letter-spacing);line-height:var(--line-height);pointer-events:none;position:absolute;width:100vw}@media (pointer:coarse){.e{height:-webkit-fill-available}}.e *,.e :after,.e :before{box-sizing:border-box}';function Ks(e,{index$:t}){let r=Ue(),n=document.createElement("div");document.body.appendChild(n),n.style.position="fixed",n.style.height="100%",n.style.top="0",n.style.zIndex="4";let o=n.attachShadow({mode:"open"});o.appendChild(A("style",{},qs.toString()));try{Ya(r.search,{highlight:r.features.includes("search.highlight")}),me(t).subscribe(i=>{for(let a of i.items)a.location=new URL(a.location,r.base).toString();Ja(i,o)}),b(e,"click").subscribe(()=>{go()}),Tn("search").pipe(ke(1)).subscribe(()=>go())}catch(i){e.hidden=!0;let a=G("label[for=__search]");a.hidden=!0}return Ke}var Bs=_r(So());function Ys(e,{index$:t,location$:r}){return re([t,r.pipe(J(Ye()),L(n=>!!n.searchParams.get("h")))]).pipe(f(([n,o])=>_p(n.config)(o.searchParams.get("h"))),f(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,l=n(c);l.length>c.length&&o.set(s,l)}for(let[s,c]of o){let{childNodes:l}=A("span",null,c);s.replaceWith(...Array.from(l))}return{ref:e,nodes:o}}))}function _p(e){let t=e.separator.split("|").map(o=>o.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":o).join("|"),r=new RegExp(t,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/\s+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${o.split(r).map(a=>a.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&")).filter(a=>a.length>0).join("|")})`,"img");return a=>(0,Bs.default)(a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function yp(e,{viewport$:t,main$:r}){let n=e.closest(".md-grid"),o=n.offsetTop-n.parentElement.offsetTop;return re([r,t]).pipe(f(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),ie((i,a)=>i.height===a.height&&i.locked===a.locked))}function Ao(e,n){var o=n,{header$:t}=o,r=gr(o,["header$"]);let i=G(".md-sidebar__scrollwrap",e),{y:a}=wt(i);return j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=s.pipe(Xe(0,je));return l.pipe(pe(t)).subscribe({next([{height:u},{height:p}]){i.style.height=`${u-2*a}px`,e.style.top=`${p}px`},complete(){i.style.height="",e.style.top=""}}),l.pipe(Sr()).subscribe(()=>{for(let u of P(".md-nav__link--active[href]",e)){if(!u.clientHeight)continue;let p=u.closest(".md-sidebar__scrollwrap");if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2})}}}),me(P("label[tabindex]",e)).pipe(oe(u=>b(u,"click").pipe(Ie(ge),f(()=>u),Q(c)))).subscribe(u=>{let p=G(`[id="${u.htmlFor}"]`);G(`[aria-labelledby="${u.id}"]`).setAttribute("aria-expanded",`${p.checked}`)}),X("content.tooltips")&&me(P("abbr[title]",e)).pipe(oe(u=>Ge(u,{viewport$})),Q(c)).subscribe(),yp(e,r).pipe($(u=>s.next(u)),V(()=>s.complete()),f(u=>H({ref:e},u)))})}function Gs(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return $t(et(`${r}/releases/latest`).pipe(_e(()=>y),f(n=>({version:n.tag_name})),ot({})),et(r).pipe(_e(()=>y),f(n=>({stars:n.stargazers_count,forks:n.forks_count})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return et(r).pipe(f(n=>({repositories:n.public_repos})),ot({}))}}function Js(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return $t(et(`${r}/releases/permalink/latest`).pipe(_e(()=>y),f(({tag_name:n})=>({version:n})),ot({})),et(r).pipe(_e(()=>y),f(({star_count:n,forks_count:o})=>({stars:n,forks:o})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}function Xs(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,n]=t;return Gs(r,n)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,n]=t;return Js(r,n)}return y}var xp;function wp(e){return xp||(xp=j(()=>{let t=__md_get("__source",sessionStorage);if(t)return Y(t);if(Ee("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return y}return Xs(e.href).pipe($(n=>__md_set("__source",n,sessionStorage)))}).pipe(_e(()=>y),L(t=>Object.keys(t).length>0),f(t=>({facts:t})),se(1)))}function Zs(e){let t=G(":scope > :last-child",e);return j(()=>{let r=new I;return r.subscribe(({facts:n})=>{t.appendChild(bs(n)),t.classList.add("md-source__repository--active")}),wp(e).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Ep(e,{viewport$:t,header$:r}){return Re(document.body).pipe(g(()=>Sn(e,{header$:r,viewport$:t})),f(({offset:{y:n}})=>({hidden:n>=10})),fe("hidden"))}function Qs(e,t){return j(()=>{let r=new I;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(X("navigation.tabs.sticky")?Y({hidden:!1}):Ep(e,t)).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Tp(e,{viewport$:t,header$:r}){let n=new Map,o=P(".md-nav__link",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),l=Le(`[id="${c}"]`);typeof l!="undefined"&&n.set(s,l)}let i=r.pipe(fe("height"),f(({height:s})=>{let c=ht("main"),l=G(":scope > :first-child",c);return s+.9*(l.offsetTop-c.offsetTop)}),xe());return Re(document.body).pipe(fe("height"),g(s=>j(()=>{let c=[];return Y([...n].reduce((l,[u,p])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=p.tagName;)c.pop();let d=p.offsetTop;for(;!d&&p.parentElement;)p=p.parentElement,d=p.offsetTop;let m=p.offsetParent;for(;m;m=m.offsetParent)d+=m.offsetTop;return l.set([...c=[...c,u]].reverse(),d)},new Map))}).pipe(f(c=>new Map([...c].sort(([,l],[,u])=>l-u))),Ze(i),g(([c,l])=>t.pipe(Or(([u,p],{offset:{y:d},size:m})=>{let h=d+m.height>=Math.floor(s.height);for(;p.length;){let[,v]=p[0];if(v-l=d&&!h)p=[u.pop(),...p];else break}return[u,p]},[[],[...c]]),ie((u,p)=>u[0]===p[0]&&u[1]===p[1])))))).pipe(f(([s,c])=>({prev:s.map(([l])=>l),next:c.map(([l])=>l)})),J({prev:[],next:[]}),Pt(2,1),f(([s,c])=>s.prev.length{let i=new I,a=i.pipe(he(),ye(!0));if(i.subscribe(({prev:s,next:c})=>{for(let[l]of c)l.classList.remove("md-nav__link--passed"),l.classList.remove("md-nav__link--active");for(let[l,[u]]of s.entries())u.classList.add("md-nav__link--passed"),u.classList.toggle("md-nav__link--active",l===s.length-1)}),X("toc.follow")){let s=R(t.pipe(Be(1),f(()=>{})),t.pipe(Be(250),f(()=>"smooth")));i.pipe(L(({prev:c})=>c.length>0),Ze(n.pipe(Ie(ge))),pe(s)).subscribe(([[{prev:c}],l])=>{let[u]=c[c.length-1];if(u.offsetHeight){let p=ki(u);if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2,behavior:l})}}})}return X("navigation.tracking")&&t.pipe(Q(a),fe("offset"),Be(250),ke(1),Q(o.pipe(ke(1))),jt({delay:250}),pe(i)).subscribe(([,{prev:s}])=>{let c=Ye(),l=s[s.length-1];if(l&&l.length){let[u]=l,{hash:p}=new URL(u.href);c.hash!==p&&(c.hash=p,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),Tp(e,{viewport$:t,header$:r}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function Sp(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(f(({offset:{y:a}})=>a),Pt(2,1),f(([a,s])=>a>s&&s>0),ie()),i=r.pipe(f(({active:a})=>a));return re([i,o]).pipe(f(([a,s])=>!(a&&s)),ie(),Q(n.pipe(ke(1))),ye(!0),jt({delay:250}),f(a=>({hidden:a})))}function tc(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(Q(a),fe("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),b(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Sp(e,{viewport$:t,main$:n,target$:o}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))}function rc(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,t.port&&(e.port=t.port),e}function Op(e,t){let r=new Map;for(let n of P("url",e)){let o=G("loc",n),i=[rc(new URL(o.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",n)){let s=a.getAttribute("href");s!=null&&i.push(rc(new URL(s),t))}}return r}function dr(e){return ns(new URL("sitemap.xml",e)).pipe(f(t=>Op(t,new URL(e))),_e(()=>Y(new Map)),xe())}function nc({document$:e}){let t=new Map;e.pipe(g(()=>P("link[rel=alternate]")),f(r=>new URL(r.href)),L(r=>!t.has(r.toString())),oe(r=>dr(r).pipe(f(n=>[r,n]),_e(()=>y)))).subscribe(([r,n])=>{t.set(r.toString().replace(/\/$/,""),n)}),b(document.body,"click").pipe(L(r=>!r.metaKey&&!r.ctrlKey),g(r=>{if(r.target instanceof Element){let n=r.target.closest("a");if(n&&!n.target){let o=[...t].find(([p])=>n.href.startsWith(`${p}/`));if(typeof o=="undefined")return y;let[i,a]=o,s=Ye();if(s.href.startsWith(i))return y;let c=Ue(),l=s.href.replace(c.base,"");l=`${i}/${l}`;let u=a.has(l.split("#")[0])?new URL(l,c.base):new URL(i);return r.preventDefault(),Y(u)}}return y})).subscribe(r=>dt(r,!0))}var Co=_r(Mo());function Lp(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function oc({alert$:e}){Co.default.isSupported()&&new U(t=>{new Co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Lp(G(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe($(t=>{t.trigger.focus()}),f(()=>Bt("clipboard.copied"))).subscribe(e)}function ic(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let n=new URL(r.href);return n.search=n.hash="",t.has(`${n}`)?(e.preventDefault(),Y(r)):y}function ac(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function sc(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let n=t.getAttribute(r);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){t[r]=t[r];break}}return Y(e)}function Mp(e){for(let n of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...X("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let o=Le(n),i=Le(n,e);typeof o!="undefined"&&typeof i!="undefined"&&o.replaceWith(i)}let t=ac(document);for(let[n,o]of ac(e))t.has(n)?t.delete(n):document.head.appendChild(o);for(let n of t.values()){let o=n.getAttribute("name");o!=="theme-color"&&o!=="color-scheme"&&n.remove()}let r=ht("container");return nt(P("script",r)).pipe(g(n=>{let o=e.createElement("script");if(n.src){for(let i of n.getAttributeNames())o.setAttribute(i,n.getAttribute(i));return n.replaceWith(o),new U(i=>{o.onload=()=>i.complete()})}else return o.textContent=n.textContent,n.replaceWith(o),y}),he(),ye(document))}function cc({sitemap$:e,location$:t,viewport$:r,progress$:n}){if(location.protocol==="file:")return Ke;Y(document).subscribe(sc);let o=b(document.body,"click").pipe(Ze(e),g(([s,c])=>ic(s,c)),f(({href:s})=>new URL(s)),xe()),i=b(window,"popstate").pipe(f(Ye),xe());o.pipe(pe(r)).subscribe(([s,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",s)}),R(o,i).subscribe(t);let a=t.pipe(fe("pathname"),g(s=>En(s,{progress$:n}).pipe(_e(()=>(dt(s,!0),y)))),g(sc),g(Mp),xe());return R(a.pipe(pe(t,(s,c)=>c)),a.pipe(g(()=>t),fe("hash")),t.pipe(ie((s,c)=>s.pathname===c.pathname&&s.hash===c.hash),g(()=>o),$(()=>history.back()))).subscribe(s=>{var c,l;history.state!==null||!s.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",es(s.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),b(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(fe("offset"),Be(100)).subscribe(({offset:s})=>{history.replaceState(s,"")}),X("navigation.instant.prefetch")&&R(b(document.body,"mousemove"),b(document.body,"focusin")).pipe(Ze(e),g(([s,c])=>ic(s,c)),Be(25),Yn(({href:s})=>s),cn(s=>{let c=document.createElement("link");return c.rel="prefetch",c.href=s.toString(),document.head.appendChild(c),b(c,"load").pipe(f(()=>c),Me(1))})).subscribe(s=>s.remove()),a}function lc(e){var u;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:n,currentBaseURL:o}=e,i=(u=Ho(o))==null?void 0:u.pathname;if(i===void 0)return;let a=kp(n.pathname,i);if(a===void 0)return;let s=Cp(t.keys());if(!t.has(s))return;let c=Ho(a,s);if(!c||!t.has(c.href))return;let l=Ho(a,r);if(l)return l.hash=n.hash,l.search=n.search,l}function Ho(e,t){try{return new URL(e,t)}catch(r){return}}function kp(e,t){if(e.startsWith(t))return e.slice(t.length)}function Ap(e,t){let r=Math.min(e.length,t.length),n;for(n=0;ny)),n=r.pipe(f(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));r.pipe(f(o=>new Map(o.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),g(o=>b(document.body,"click").pipe(L(i=>!i.metaKey&&!i.ctrlKey),pe(n),g(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&o.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&o.get(c)===a?y:(i.preventDefault(),Y(new URL(c)))}}return y}),g(i=>dr(i).pipe(f(a=>{var s;return(s=lc({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:Ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(o=>dt(o,!0)),re([r,n]).subscribe(([o,i])=>{G(".md-header__topic").appendChild(_s(o,i))}),e.pipe(g(()=>n)).subscribe(o=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let c=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let l of c)for(let u of o.aliases.concat(o.version))if(new RegExp(l,"i").test(u)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let c of Ee("outdated"))c.hidden=!1})}function pc({document$:e,viewport$:t}){e.pipe(g(()=>P(".md-ellipsis")),oe(r=>Et(r).pipe(Q(e.pipe(ke(1))),L(n=>n),f(()=>r),Me(1))),L(r=>r.offsetWidth{let n=r.innerText,o=r.closest("a")||r;return o.title=n,X("content.tooltips")?Ge(o,{viewport$:t}).pipe(Q(e.pipe(ke(1))),V(()=>o.removeAttribute("title"))):y})).subscribe(),X("content.tooltips")&&e.pipe(g(()=>P(".md-status")),oe(r=>Ge(r,{viewport$:t}))).subscribe()}function fc({document$:e,tablet$:t}){e.pipe(g(()=>P(".md-toggle--indeterminate")),$(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>b(r,"change").pipe(Xn(()=>r.classList.contains("md-toggle--indeterminate")),f(()=>r))),pe(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function Hp(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function mc({document$:e}){e.pipe(g(()=>P("[data-md-scrollfix]")),$(t=>t.removeAttribute("data-md-scrollfix")),L(Hp),oe(t=>b(t,"touchstart").pipe(f(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));function $p(){return location.protocol==="file:"?ar(`${new URL("search.js",Mn.base)}`).pipe(f(()=>__index),_e(()=>Ke),se(1)):et(new URL("search.json",Mn.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var vt=Si(),Ur=Za(),hr=ts(Ur),hc=Xa(),ze=cs(),$o=Ir("(min-width: 60em)"),vc=Ir("(min-width: 76.25em)"),bc=rs(),Mn=Ue(),gc=Le(".md-search")?$p():Ke,Po=new I;oc({alert$:Po});nc({document$:vt});var Io=new I,_c=dr(Mn.base);X("navigation.instant")&&cc({sitemap$:_c,location$:Ur,viewport$:ze,progress$:Io}).subscribe(vt);var dc;((dc=Mn.version)==null?void 0:dc.provider)==="mike"&&uc({document$:vt});R(Ur,hr).pipe(It(125)).subscribe(()=>{Eo("drawer",!1),Eo("search",!1)});hc.pipe(L(({mode:e,meta:t})=>e==="global"&&!t)).subscribe(e=>{switch(e.type){case",":case"p":let t=document.querySelector("link[rel=prev]");t instanceof HTMLLinkElement&&dt(t);break;case".":case"n":let r=document.querySelector("link[rel=next]");r instanceof HTMLLinkElement&&dt(r);break;case"/":let n=document.querySelector("[data-md-component=search] button");n instanceof HTMLButtonElement&&n.click();break;case"Enter":let o=xt();o instanceof HTMLLabelElement&&o.click()}});pc({viewport$:ze,document$:vt});fc({document$:vt,tablet$:$o});mc({document$:vt});var Lt=Us(ht("header"),{viewport$:ze}),Fr=vt.pipe(f(()=>ht("main")),g(e=>Ws(e,{viewport$:ze,header$:Lt})),se(1)),Pp=R(...Ee("consent").map(e=>us(e,{target$:hr})),...Ee("dialog").map(e=>Fs(e,{alert$:Po})),...Ee("palette").map(e=>Vs(e)),...Ee("progress").map(e=>zs(e,{progress$:Io})),...Ee("search").map(e=>Ks(e,{index$:gc})),...Ee("source").map(e=>Zs(e))),Ip=j(()=>R(...Ee("announce").map(e=>ls(e)),...Ee("content").map(e=>js(e,{sitemap$:_c,viewport$:ze,target$:hr,print$:bc})),...Ee("content").map(e=>X("search.highlight")?Ys(e,{index$:gc,location$:Ur}):y),...Ee("header").map(e=>Ns(e,{viewport$:ze,header$:Lt,main$:Fr})),...Ee("header-title").map(e=>Ds(e,{viewport$:ze,header$:Lt})),...Ee("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?yo(vc,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr})):yo($o,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr}))),...Ee("tabs").map(e=>Qs(e,{viewport$:ze,header$:Lt})),...Ee("toc").map(e=>ec(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})),...Ee("top").map(e=>tc(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})))),yc=vt.pipe(g(()=>Ip),Rt(Pp),se(1));yc.subscribe();window.document$=vt;window.location$=Ur;window.target$=hr;window.keyboard$=hc;window.viewport$=ze;window.tablet$=$o;window.screen$=vc;window.print$=bc;window.alert$=Po;window.progress$=Io;window.component$=yc;})(); -/*! update cache: 20260331044536 */ +/*! update cache: 20260331053229 */ diff --git a/ru/chapter_appendix/contribution/index.html b/ru/chapter_appendix/contribution/index.html index 148b312fc..0df9cdd21 100644 --- a/ru/chapter_appendix/contribution/index.html +++ b/ru/chapter_appendix/contribution/index.html @@ -574,7 +574,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -596,7 +596,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -646,7 +646,7 @@ - 1.2 Что такое структуры данных и алгоритмы + 1.2 Что такое алгоритм @@ -1181,7 +1181,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1203,7 +1203,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1309,7 +1309,7 @@ - 4.4 Память и кеш * + 4.4 Оперативная память и кэш * @@ -1591,7 +1591,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1613,7 +1613,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1691,7 +1691,7 @@ - 6.3 Хеш-алгоритмы + 6.3 Алгоритмы хеширования @@ -1786,7 +1786,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1808,7 +1808,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1886,7 +1886,7 @@ - 7.3 Представление дерева массивом + 7.3 Представление двоичного дерева массивом @@ -1970,7 +1970,7 @@ - 7.6 Резюме + 7.6 Краткие итоги @@ -2133,7 +2133,7 @@ - 8.3 Задача Top-K + 8.3 Задача Top-k @@ -2224,7 +2224,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2246,7 +2246,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2296,7 +2296,7 @@ - 9.2 Базовые операции над графами + 9.2 Базовые операции графа @@ -2352,7 +2352,7 @@ - 9.4 Резюме + 9.4 Краткие итоги @@ -2491,7 +2491,7 @@ - 10.2 Точка вставки двоичного поиска + 10.2 Двоичный поиск точки вставки @@ -2519,7 +2519,7 @@ - 10.3 Граничные случаи двоичного поиска + 10.3 Двоичный поиск границ @@ -2547,7 +2547,7 @@ - 10.4 Стратегия оптимизации через хеширование + 10.4 Стратегии оптимизации хеширования @@ -2575,7 +2575,7 @@ - 10.5 Алгоритмы поиска: новый взгляд + 10.5 Переосмысление алгоритмов поиска @@ -2780,7 +2780,7 @@ - 11.3 Пузырьковая сортировка + 11.3 Сортировка пузырьком @@ -2808,7 +2808,7 @@ - 11.4 Сортировка вставкой + 11.4 Сортировка вставками @@ -3113,7 +3113,7 @@ - 12.1 Алгоритмы разделяй и властвуй + 12.1 Стратегия разделяй и властвуй @@ -3141,7 +3141,7 @@ - 12.2 Стратегия поиска разделяй и властвуй + 12.2 Поисковая стратегия разделяй и властвуй @@ -3418,7 +3418,7 @@ - 13.4 Задача о $n$ ферзях + 13.4 Задача о n ферзях @@ -3559,7 +3559,7 @@ - 14.1 Введение в динамическое программирование + 14.1 Первое знакомство с динамическим программированием @@ -3671,7 +3671,7 @@ - 14.5 Задача о неограниченном рюкзаке + 14.5 Задача о полном рюкзаке diff --git a/ru/chapter_appendix/index.html b/ru/chapter_appendix/index.html index 13731e1ed..a4febd6c9 100644 --- a/ru/chapter_appendix/index.html +++ b/ru/chapter_appendix/index.html @@ -574,7 +574,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -596,7 +596,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -646,7 +646,7 @@ - 1.2 Что такое структуры данных и алгоритмы + 1.2 Что такое алгоритм @@ -1181,7 +1181,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1203,7 +1203,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1309,7 +1309,7 @@ - 4.4 Память и кеш * + 4.4 Оперативная память и кэш * @@ -1591,7 +1591,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1613,7 +1613,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1691,7 +1691,7 @@ - 6.3 Хеш-алгоритмы + 6.3 Алгоритмы хеширования @@ -1786,7 +1786,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1808,7 +1808,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1886,7 +1886,7 @@ - 7.3 Представление дерева массивом + 7.3 Представление двоичного дерева массивом @@ -1970,7 +1970,7 @@ - 7.6 Резюме + 7.6 Краткие итоги @@ -2133,7 +2133,7 @@ - 8.3 Задача Top-K + 8.3 Задача Top-k @@ -2224,7 +2224,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2246,7 +2246,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2296,7 +2296,7 @@ - 9.2 Базовые операции над графами + 9.2 Базовые операции графа @@ -2352,7 +2352,7 @@ - 9.4 Резюме + 9.4 Краткие итоги @@ -2491,7 +2491,7 @@ - 10.2 Точка вставки двоичного поиска + 10.2 Двоичный поиск точки вставки @@ -2519,7 +2519,7 @@ - 10.3 Граничные случаи двоичного поиска + 10.3 Двоичный поиск границ @@ -2547,7 +2547,7 @@ - 10.4 Стратегия оптимизации через хеширование + 10.4 Стратегии оптимизации хеширования @@ -2575,7 +2575,7 @@ - 10.5 Алгоритмы поиска: новый взгляд + 10.5 Переосмысление алгоритмов поиска @@ -2780,7 +2780,7 @@ - 11.3 Пузырьковая сортировка + 11.3 Сортировка пузырьком @@ -2808,7 +2808,7 @@ - 11.4 Сортировка вставкой + 11.4 Сортировка вставками @@ -3113,7 +3113,7 @@ - 12.1 Алгоритмы разделяй и властвуй + 12.1 Стратегия разделяй и властвуй @@ -3141,7 +3141,7 @@ - 12.2 Стратегия поиска разделяй и властвуй + 12.2 Поисковая стратегия разделяй и властвуй @@ -3418,7 +3418,7 @@ - 13.4 Задача о $n$ ферзях + 13.4 Задача о n ферзях @@ -3559,7 +3559,7 @@ - 14.1 Введение в динамическое программирование + 14.1 Первое знакомство с динамическим программированием @@ -3671,7 +3671,7 @@ - 14.5 Задача о неограниченном рюкзаке + 14.5 Задача о полном рюкзаке diff --git a/ru/chapter_appendix/installation/index.html b/ru/chapter_appendix/installation/index.html index 60d54ae34..e8c77d7e7 100644 --- a/ru/chapter_appendix/installation/index.html +++ b/ru/chapter_appendix/installation/index.html @@ -574,7 +574,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -596,7 +596,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -646,7 +646,7 @@ - 1.2 Что такое структуры данных и алгоритмы + 1.2 Что такое алгоритм @@ -1181,7 +1181,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1203,7 +1203,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1309,7 +1309,7 @@ - 4.4 Память и кеш * + 4.4 Оперативная память и кэш * @@ -1591,7 +1591,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1613,7 +1613,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1691,7 +1691,7 @@ - 6.3 Хеш-алгоритмы + 6.3 Алгоритмы хеширования @@ -1786,7 +1786,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1808,7 +1808,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1886,7 +1886,7 @@ - 7.3 Представление дерева массивом + 7.3 Представление двоичного дерева массивом @@ -1970,7 +1970,7 @@ - 7.6 Резюме + 7.6 Краткие итоги @@ -2133,7 +2133,7 @@ - 8.3 Задача Top-K + 8.3 Задача Top-k @@ -2224,7 +2224,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2246,7 +2246,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2296,7 +2296,7 @@ - 9.2 Базовые операции над графами + 9.2 Базовые операции графа @@ -2352,7 +2352,7 @@ - 9.4 Резюме + 9.4 Краткие итоги @@ -2491,7 +2491,7 @@ - 10.2 Точка вставки двоичного поиска + 10.2 Двоичный поиск точки вставки @@ -2519,7 +2519,7 @@ - 10.3 Граничные случаи двоичного поиска + 10.3 Двоичный поиск границ @@ -2547,7 +2547,7 @@ - 10.4 Стратегия оптимизации через хеширование + 10.4 Стратегии оптимизации хеширования @@ -2575,7 +2575,7 @@ - 10.5 Алгоритмы поиска: новый взгляд + 10.5 Переосмысление алгоритмов поиска @@ -2780,7 +2780,7 @@ - 11.3 Пузырьковая сортировка + 11.3 Сортировка пузырьком @@ -2808,7 +2808,7 @@ - 11.4 Сортировка вставкой + 11.4 Сортировка вставками @@ -3113,7 +3113,7 @@ - 12.1 Алгоритмы разделяй и властвуй + 12.1 Стратегия разделяй и властвуй @@ -3141,7 +3141,7 @@ - 12.2 Стратегия поиска разделяй и властвуй + 12.2 Поисковая стратегия разделяй и властвуй @@ -3418,7 +3418,7 @@ - 13.4 Задача о $n$ ферзях + 13.4 Задача о n ферзях @@ -3559,7 +3559,7 @@ - 14.1 Введение в динамическое программирование + 14.1 Первое знакомство с динамическим программированием @@ -3671,7 +3671,7 @@ - 14.5 Задача о неограниченном рюкзаке + 14.5 Задача о полном рюкзаке diff --git a/ru/chapter_appendix/terminology/index.html b/ru/chapter_appendix/terminology/index.html index 937edd689..c797fba90 100644 --- a/ru/chapter_appendix/terminology/index.html +++ b/ru/chapter_appendix/terminology/index.html @@ -574,7 +574,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -596,7 +596,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -646,7 +646,7 @@ - 1.2 Что такое структуры данных и алгоритмы + 1.2 Что такое алгоритм @@ -1181,7 +1181,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1203,7 +1203,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1309,7 +1309,7 @@ - 4.4 Память и кеш * + 4.4 Оперативная память и кэш * @@ -1591,7 +1591,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1613,7 +1613,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1691,7 +1691,7 @@ - 6.3 Хеш-алгоритмы + 6.3 Алгоритмы хеширования @@ -1786,7 +1786,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1808,7 +1808,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1886,7 +1886,7 @@ - 7.3 Представление дерева массивом + 7.3 Представление двоичного дерева массивом @@ -1970,7 +1970,7 @@ - 7.6 Резюме + 7.6 Краткие итоги @@ -2133,7 +2133,7 @@ - 8.3 Задача Top-K + 8.3 Задача Top-k @@ -2224,7 +2224,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2246,7 +2246,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2296,7 +2296,7 @@ - 9.2 Базовые операции над графами + 9.2 Базовые операции графа @@ -2352,7 +2352,7 @@ - 9.4 Резюме + 9.4 Краткие итоги @@ -2491,7 +2491,7 @@ - 10.2 Точка вставки двоичного поиска + 10.2 Двоичный поиск точки вставки @@ -2519,7 +2519,7 @@ - 10.3 Граничные случаи двоичного поиска + 10.3 Двоичный поиск границ @@ -2547,7 +2547,7 @@ - 10.4 Стратегия оптимизации через хеширование + 10.4 Стратегии оптимизации хеширования @@ -2575,7 +2575,7 @@ - 10.5 Алгоритмы поиска: новый взгляд + 10.5 Переосмысление алгоритмов поиска @@ -2780,7 +2780,7 @@ - 11.3 Пузырьковая сортировка + 11.3 Сортировка пузырьком @@ -2808,7 +2808,7 @@ - 11.4 Сортировка вставкой + 11.4 Сортировка вставками @@ -3113,7 +3113,7 @@ - 12.1 Алгоритмы разделяй и властвуй + 12.1 Стратегия разделяй и властвуй @@ -3141,7 +3141,7 @@ - 12.2 Стратегия поиска разделяй и властвуй + 12.2 Поисковая стратегия разделяй и властвуй @@ -3418,7 +3418,7 @@ - 13.4 Задача о $n$ ферзях + 13.4 Задача о n ферзях @@ -3559,7 +3559,7 @@ - 14.1 Введение в динамическое программирование + 14.1 Первое знакомство с динамическим программированием @@ -3671,7 +3671,7 @@ - 14.5 Задача о неограниченном рюкзаке + 14.5 Задача о полном рюкзаке diff --git a/ru/chapter_array_and_linkedlist/array/index.html b/ru/chapter_array_and_linkedlist/array/index.html index 42c6e4304..1ff18a5db 100644 --- a/ru/chapter_array_and_linkedlist/array/index.html +++ b/ru/chapter_array_and_linkedlist/array/index.html @@ -574,7 +574,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -596,7 +596,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -646,7 +646,7 @@ - 1.2 Что такое структуры данных и алгоритмы + 1.2 Что такое алгоритм @@ -1183,7 +1183,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1205,7 +1205,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1475,7 +1475,7 @@ - 4.4 Память и кеш * + 4.4 Оперативная память и кэш * @@ -1757,7 +1757,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1779,7 +1779,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1857,7 +1857,7 @@ - 6.3 Хеш-алгоритмы + 6.3 Алгоритмы хеширования @@ -1952,7 +1952,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1974,7 +1974,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -2052,7 +2052,7 @@ - 7.3 Представление дерева массивом + 7.3 Представление двоичного дерева массивом @@ -2136,7 +2136,7 @@ - 7.6 Резюме + 7.6 Краткие итоги @@ -2299,7 +2299,7 @@ - 8.3 Задача Top-K + 8.3 Задача Top-k @@ -2390,7 +2390,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2412,7 +2412,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2462,7 +2462,7 @@ - 9.2 Базовые операции над графами + 9.2 Базовые операции графа @@ -2518,7 +2518,7 @@ - 9.4 Резюме + 9.4 Краткие итоги @@ -2657,7 +2657,7 @@ - 10.2 Точка вставки двоичного поиска + 10.2 Двоичный поиск точки вставки @@ -2685,7 +2685,7 @@ - 10.3 Граничные случаи двоичного поиска + 10.3 Двоичный поиск границ @@ -2713,7 +2713,7 @@ - 10.4 Стратегия оптимизации через хеширование + 10.4 Стратегии оптимизации хеширования @@ -2741,7 +2741,7 @@ - 10.5 Алгоритмы поиска: новый взгляд + 10.5 Переосмысление алгоритмов поиска @@ -2946,7 +2946,7 @@ - 11.3 Пузырьковая сортировка + 11.3 Сортировка пузырьком @@ -2974,7 +2974,7 @@ - 11.4 Сортировка вставкой + 11.4 Сортировка вставками @@ -3279,7 +3279,7 @@ - 12.1 Алгоритмы разделяй и властвуй + 12.1 Стратегия разделяй и властвуй @@ -3307,7 +3307,7 @@ - 12.2 Стратегия поиска разделяй и властвуй + 12.2 Поисковая стратегия разделяй и властвуй @@ -3584,7 +3584,7 @@ - 13.4 Задача о $n$ ферзях + 13.4 Задача о n ферзях @@ -3725,7 +3725,7 @@ - 14.1 Введение в динамическое программирование + 14.1 Первое знакомство с динамическим программированием @@ -3837,7 +3837,7 @@ - 14.5 Задача о неограниченном рюкзаке + 14.5 Задача о полном рюкзаке diff --git a/ru/chapter_array_and_linkedlist/index.html b/ru/chapter_array_and_linkedlist/index.html index 21db44401..875a52cb5 100644 --- a/ru/chapter_array_and_linkedlist/index.html +++ b/ru/chapter_array_and_linkedlist/index.html @@ -574,7 +574,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -596,7 +596,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -646,7 +646,7 @@ - 1.2 Что такое структуры данных и алгоритмы + 1.2 Что такое алгоритм @@ -1183,7 +1183,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1205,7 +1205,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1311,7 +1311,7 @@ - 4.4 Память и кеш * + 4.4 Оперативная память и кэш * @@ -1593,7 +1593,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1615,7 +1615,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1693,7 +1693,7 @@ - 6.3 Хеш-алгоритмы + 6.3 Алгоритмы хеширования @@ -1788,7 +1788,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1810,7 +1810,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1888,7 +1888,7 @@ - 7.3 Представление дерева массивом + 7.3 Представление двоичного дерева массивом @@ -1972,7 +1972,7 @@ - 7.6 Резюме + 7.6 Краткие итоги @@ -2135,7 +2135,7 @@ - 8.3 Задача Top-K + 8.3 Задача Top-k @@ -2226,7 +2226,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2248,7 +2248,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2298,7 +2298,7 @@ - 9.2 Базовые операции над графами + 9.2 Базовые операции графа @@ -2354,7 +2354,7 @@ - 9.4 Резюме + 9.4 Краткие итоги @@ -2493,7 +2493,7 @@ - 10.2 Точка вставки двоичного поиска + 10.2 Двоичный поиск точки вставки @@ -2521,7 +2521,7 @@ - 10.3 Граничные случаи двоичного поиска + 10.3 Двоичный поиск границ @@ -2549,7 +2549,7 @@ - 10.4 Стратегия оптимизации через хеширование + 10.4 Стратегии оптимизации хеширования @@ -2577,7 +2577,7 @@ - 10.5 Алгоритмы поиска: новый взгляд + 10.5 Переосмысление алгоритмов поиска @@ -2782,7 +2782,7 @@ - 11.3 Пузырьковая сортировка + 11.3 Сортировка пузырьком @@ -2810,7 +2810,7 @@ - 11.4 Сортировка вставкой + 11.4 Сортировка вставками @@ -3115,7 +3115,7 @@ - 12.1 Алгоритмы разделяй и властвуй + 12.1 Стратегия разделяй и властвуй @@ -3143,7 +3143,7 @@ - 12.2 Стратегия поиска разделяй и властвуй + 12.2 Поисковая стратегия разделяй и властвуй @@ -3420,7 +3420,7 @@ - 13.4 Задача о $n$ ферзях + 13.4 Задача о n ферзях @@ -3561,7 +3561,7 @@ - 14.1 Введение в динамическое программирование + 14.1 Первое знакомство с динамическим программированием @@ -3673,7 +3673,7 @@ - 14.5 Задача о неограниченном рюкзаке + 14.5 Задача о полном рюкзаке @@ -4285,7 +4285,7 @@
  • 4.1   Массив
  • 4.2   Связный список
  • 4.3   Список
  • -
  • 4.4   Память и кеш *
  • +
  • 4.4   Оперативная память и кэш *
  • 4.5   Резюме
  • diff --git a/ru/chapter_array_and_linkedlist/linked_list/index.html b/ru/chapter_array_and_linkedlist/linked_list/index.html index ae926a13f..40f492544 100644 --- a/ru/chapter_array_and_linkedlist/linked_list/index.html +++ b/ru/chapter_array_and_linkedlist/linked_list/index.html @@ -574,7 +574,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -596,7 +596,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -646,7 +646,7 @@ - 1.2 Что такое структуры данных и алгоритмы + 1.2 Что такое алгоритм @@ -1183,7 +1183,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1205,7 +1205,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1464,7 +1464,7 @@ - 4.4 Память и кеш * + 4.4 Оперативная память и кэш * @@ -1746,7 +1746,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1768,7 +1768,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1846,7 +1846,7 @@ - 6.3 Хеш-алгоритмы + 6.3 Алгоритмы хеширования @@ -1941,7 +1941,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1963,7 +1963,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -2041,7 +2041,7 @@ - 7.3 Представление дерева массивом + 7.3 Представление двоичного дерева массивом @@ -2125,7 +2125,7 @@ - 7.6 Резюме + 7.6 Краткие итоги @@ -2288,7 +2288,7 @@ - 8.3 Задача Top-K + 8.3 Задача Top-k @@ -2379,7 +2379,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2401,7 +2401,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2451,7 +2451,7 @@ - 9.2 Базовые операции над графами + 9.2 Базовые операции графа @@ -2507,7 +2507,7 @@ - 9.4 Резюме + 9.4 Краткие итоги @@ -2646,7 +2646,7 @@ - 10.2 Точка вставки двоичного поиска + 10.2 Двоичный поиск точки вставки @@ -2674,7 +2674,7 @@ - 10.3 Граничные случаи двоичного поиска + 10.3 Двоичный поиск границ @@ -2702,7 +2702,7 @@ - 10.4 Стратегия оптимизации через хеширование + 10.4 Стратегии оптимизации хеширования @@ -2730,7 +2730,7 @@ - 10.5 Алгоритмы поиска: новый взгляд + 10.5 Переосмысление алгоритмов поиска @@ -2935,7 +2935,7 @@ - 11.3 Пузырьковая сортировка + 11.3 Сортировка пузырьком @@ -2963,7 +2963,7 @@ - 11.4 Сортировка вставкой + 11.4 Сортировка вставками @@ -3268,7 +3268,7 @@ - 12.1 Алгоритмы разделяй и властвуй + 12.1 Стратегия разделяй и властвуй @@ -3296,7 +3296,7 @@ - 12.2 Стратегия поиска разделяй и властвуй + 12.2 Поисковая стратегия разделяй и властвуй @@ -3573,7 +3573,7 @@ - 13.4 Задача о $n$ ферзях + 13.4 Задача о n ферзях @@ -3714,7 +3714,7 @@ - 14.1 Введение в динамическое программирование + 14.1 Первое знакомство с динамическим программированием @@ -3826,7 +3826,7 @@ - 14.5 Задача о неограниченном рюкзаке + 14.5 Задача о полном рюкзаке diff --git a/ru/chapter_array_and_linkedlist/list/index.html b/ru/chapter_array_and_linkedlist/list/index.html index 84bd205c7..d7b1e2ca7 100644 --- a/ru/chapter_array_and_linkedlist/list/index.html +++ b/ru/chapter_array_and_linkedlist/list/index.html @@ -574,7 +574,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -596,7 +596,7 @@ - Глава 1. Знакомство с алгоритмами + Глава 1. Введение в алгоритмы @@ -646,7 +646,7 @@ - 1.2 Что такое структуры данных и алгоритмы + 1.2 Что такое алгоритм @@ -1183,7 +1183,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1205,7 +1205,7 @@ - Глава 4. Массив и связный список + Глава 4. Массивы и списки @@ -1453,7 +1453,7 @@ - 4.4 Память и кеш * + 4.4 Оперативная память и кэш * @@ -1735,7 +1735,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1757,7 +1757,7 @@ - Глава 6. Хеширование + Глава 6. Хеш-таблицы @@ -1835,7 +1835,7 @@ - 6.3 Хеш-алгоритмы + 6.3 Алгоритмы хеширования @@ -1930,7 +1930,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -1952,7 +1952,7 @@ - Глава 7. Дерево + Глава 7. Деревья @@ -2030,7 +2030,7 @@ - 7.3 Представление дерева массивом + 7.3 Представление двоичного дерева массивом @@ -2114,7 +2114,7 @@ - 7.6 Резюме + 7.6 Краткие итоги @@ -2277,7 +2277,7 @@ - 8.3 Задача Top-K + 8.3 Задача Top-k @@ -2368,7 +2368,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2390,7 +2390,7 @@ - Глава 9. Граф + Глава 9. Графы @@ -2440,7 +2440,7 @@ - 9.2 Базовые операции над графами + 9.2 Базовые операции графа @@ -2496,7 +2496,7 @@ - 9.4 Резюме + 9.4 Краткие итоги @@ -2635,7 +2635,7 @@ - 10.2 Точка вставки двоичного поиска + 10.2 Двоичный поиск точки вставки @@ -2663,7 +2663,7 @@ - 10.3 Граничные случаи двоичного поиска + 10.3 Двоичный поиск границ @@ -2691,7 +2691,7 @@ - 10.4 Стратегия оптимизации через хеширование + 10.4 Стратегии оптимизации хеширования @@ -2719,7 +2719,7 @@ - 10.5 Алгоритмы поиска: новый взгляд + 10.5 Переосмысление алгоритмов поиска @@ -2924,7 +2924,7 @@ - 11.3 Пузырьковая сортировка + 11.3 Сортировка пузырьком @@ -2952,7 +2952,7 @@ - 11.4 Сортировка вставкой + 11.4 Сортировка вставками @@ -3257,7 +3257,7 @@ - 12.1 Алгоритмы разделяй и властвуй + 12.1 Стратегия разделяй и властвуй @@ -3285,7 +3285,7 @@ - 12.2 Стратегия поиска разделяй и властвуй + 12.2 Поисковая стратегия разделяй и властвуй @@ -3562,7 +3562,7 @@ - 13.4 Задача о $n$ ферзях + 13.4 Задача о n ферзях @@ -3703,7 +3703,7 @@ - 14.1 Введение в динамическое программирование + 14.1 Первое знакомство с динамическим программированием @@ -3815,7 +3815,7 @@ - 14.5 Задача о неограниченном рюкзаке + 14.5 Задача о полном рюкзаке @@ -6663,7 +6663,7 @@ aria-label="Нижний колонтитул"