⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⡿⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀
⠀⠀⠀⢀⣠⣤⣤⣤⣀⣀⠈⠋⠉⣁⣠⣤⣬⣤⣀⡀⠀⠀
⠀⢠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀
⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠀⠀⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀
⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣤⣀
⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁
⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠁⠀
⠀⠀⠀⠈⠙⢿⣿⣿⣿⠿⠟⠛⠻⠿⣿⣿⣿⡿⠋⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⡿⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀
⠀⠀⠀⢀⣠⣤⣤⣤⣀⣀⠈⠋⠉⣁⣠⣤⣬⣤⣀⡀⠀⠀
⠀⢠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀
⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠀⠀⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀
⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣤⣀
⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁
⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠁⠀
⠀⠀⠀⠈⠙⢿⣿⣿⣿⠿⠟⠛⠻⠿⣿⣿⣿⡿⠋⠀⠀⠀
⢀⣤⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣤⡀
⢸⣿⣿⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⣠⣦⡀⠀⠀⢀⣿⡟⠀⢀⣴⣄⠀⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⣠⣾⡿⠋⠀⠀⠀⢸⣿⠇⠀⠀⠙⢿⣷⣄⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠘⢿⣿⣄⠀⠀⠀⢀⣿⡟⠀⠀⠀⠀⣠⣿⡿⠃⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠙⢿⣷⠄⠀⢸⣿⠇⠀⠀⠠⣾⡿⠋⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⠀⠁⠀⠀⠿⠟⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡇
⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇
⠀⠈⠉⠉⠉⠉⠉⠉⠉⠉⠉⢹⣿⣿⣿⣿⡏⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣤⣼⣿⣿⣿⣿⣧⣤⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣤⣶⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠈⣿⣿⣇⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢾⣿⣷⣄⠀⠀⢸⣿⣿⠀
⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠙⢿⣿⣧⡀⠈⣿⣿⣇
⠀⠀⠀⠀⠀⠀⢰⣿⣿⣦⣄⡀⠀⠻⣿⣿⣦⠸⠟⠛
⠀⠀⠀⠀⠀⠀⠀⠈⠛⠿⣿⣿⣷⣤⣈⠛⠁⠀⠀⠀
⠀⠀⠀⠀⠀⣿⣶⣶⣦⣤⣀⣙⡻⢿⠃⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠈⠉⠙⠛⠿⢿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀
⢠⣤⡄⠀⣿⣿⣿⣿⣿⣷⣶⣶⣿⠀⣤⣤⡄⠀⠀⠀
⢸⣿⡇⠀⣉⣉⣉⣉⣉⣛⣛⣛⣟⠀⣿⣿⡇⠀⠀⠀
⢸⣿⡇⠀⠿⠿⠿⠿⠿⠿⠿⠿⠿⠀⣿⣿⡇⠀⠀⠀
⢸⣿⣧⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣿⣿⡇⠀⠀⠀
⠸⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠇⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡘⢿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣶⣽⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⠿⠿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⡟⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀
⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⡷⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣷⣽⣛⠀⠀⠀⠀
⠀⠀⢠⣿⣿⣿⣿⣿⣿⠿⠛⠛⠀⠀⠀⠀⠀⠀⠛⠛⠿⣿⣿⣿⣿⣿⣶⡄⠀⠀
⠀⢰⣿⣿⠿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠿⣿⣿⡆⠀
⡰⠟⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠻⢆
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡘⢿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣶⣽⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⠿⠿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⡟⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀
⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⡷⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣷⣽⣛⠀⠀⠀⠀
⠀⠀⢠⣿⣿⣿⣿⣿⣿⠿⠛⠛⠀⠀⠀⠀⠀⠀⠛⠛⠿⣿⣿⣿⣿⣿⣶⡄⠀⠀
⠀⢰⣿⣿⠿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠿⣿⣿⡆⠀
⡰⠟⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠻⢆
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⠿⠿⠿⠿⠿⠿⠿⣿⣿⣿⡿⢿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣀⣀⠀⠀⢀⣀⣀⡿⠉⠀⣀⣀⣀⢸⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⢸⣿⣿⡇⠀⠈⠿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⢸⣿⣿⣿⣦⣄⠀⠀⠙⢻⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⢸⣿⣿⡿⠿⣿⣿⠆⠀⢸⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣀⣀⣸⣿⣿⣇⣀⣀⣀⣀⣠⣾⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⢀⣤⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣤⡀
⢸⣿⣿⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⣠⣦⡀⠀⠀⢀⣿⡟⠀⢀⣴⣄⠀⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⣠⣾⡿⠋⠀⠀⠀⢸⣿⠇⠀⠀⠙⢿⣷⣄⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠘⢿⣿⣄⠀⠀⠀⢀⣿⡟⠀⠀⠀⠀⣠⣿⡿⠃⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠙⢿⣷⠄⠀⢸⣿⠇⠀⠀⠠⣾⡿⠋⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⠀⠁⠀⠀⠿⠟⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡇
⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇
⠀⠈⠉⠉⠉⠉⠉⠉⠉⠉⠉⢹⣿⣿⣿⣿⡏⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣤⣼⣿⣿⣿⣿⣧⣤⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⡿⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀
⠀⠀⠀⢀⣠⣤⣤⣤⣀⣀⠈⠋⠉⣁⣠⣤⣬⣤⣀⡀⠀⠀
⠀⢠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀
⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠀⠀⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀
⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣤⣀
⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁
⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠁⠀
⠀⠀⠀⠈⠙⢿⣿⣿⣿⠿⠟⠛⠻⠿⣿⣿⣿⡿⠋⠀⠀⠀
⣠⣀⣄⣄⣤⣠⣠⣀⣄⣠⣀⣄⣠⣀⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢼⡹⣞⡼⣲⢧⡳⣝⢮⡳⣝⢮⡳⣭⢿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢺⡵⣫⡜⣧⢏⡷⣹⢎⡷⣹⢎⡷⣹⢾⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢣⣟⣱⢻⡜⣏⢾⡱⣏⢾⡱⣏⢾⡱⢿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⡹⣜⢧⡻⣼⡹⣎⢷⡹⣎⢷⡹⢮⡝⣯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣹⢎⡷⣹⢶⣹⡮⠷⣽⠮⠷⣝⢧⡻⣽⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⣀⠀⠀⢰⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⡸⣏⡾⣱⠏⠀⣀⣰⠇⠀⠀⢹⡎⣷⣹⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⣿⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣽⣲⡽⠷⢴⡯⢯⡝⡷⣤⣤⠋⢻⣖⣻⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⣿⠀⠀⣽⣧⣴⡶⢦⣄⠀⠀⠀⣦⠀⠀⠀⠀⣦⠀⠀⣤⣴⠶⡶⣤⡀⠀⠀⢸⣧⡦⡴⠆⠀⣦⠀⠀⠀⠀⣤
⣿⢿⠀⠀⢈⣟⢧⡻⢵⡳⢞⡇⢘⣮⢟⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⣿⠀⠀⣼⡇⠀⠀⠀⠙⣧⠀⢸⡧⠀⠀⠀⠀⣿⠀⠀⣿⠀⠀⠀⠈⣿⠀⠀⢼⡇⠀⠀⠀⠀⣿⠀⠀⠀⠀⣿
⣿⣫⢷⡒⠻⣝⡮⣝⡧⠿⢿⡀⣸⢧⡿⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⣿⠄⠀⣸⡇⠀⠀⠀⠀⣿⠀⢸⣏⠀⠀⠀⠀⣿⠀⠀⣿⠀⠀⠀⠀⢾⠀⠀⢹⡇⠀⠀⠀⠀⣿⠀⠀⠀⠀⣿
⣿⡱⣏⢷⣄⡈⠙⢻⡀⠀⠀⡿⣝⢮⣽⠀⠀⠀⠘⢿⣄⡀⠀⣀⣼⠟⠀⠀⣹⡇⠀⠀⣀⣴⠏⠀⠀⢿⣄⠀⠀⢀⣿⠀⠀⣿⡀⠀⠀⠀⣾⡄⠀⢸⣇⡀⠀⣀⠀⢻⣄⡀⠀⢀⣿
⣿⡵⣫⠾⣜⢯⡳⣟⢷⡲⢾⡹⣎⢷⣻⠀⠀⠀⠀⠀⠉⠙⠛⠉⠁⠀⠀⠀⠈⠉⠛⠛⠉⠁⠀⠀⠀⠀⠉⠛⠋⠉⠉⠀⠀⠉⠁⠀⠀⠀⠉⠁⠀⠀⠉⠙⠋⠉⠀⠀⠉⠙⠋⠉⠉
⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡘⢿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣶⣽⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⠿⠿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⡟⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀
⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⡷⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣷⣽⣛⠀⠀⠀⠀
⠀⠀⢠⣿⣿⣿⣿⣿⣿⠿⠛⠛⠀⠀⠀⠀⠀⠀⠛⠛⠿⣿⣿⣿⣿⣿⣶⡄⠀⠀
⠀⢰⣿⣿⠿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠿⣿⣿⡆⠀
⡰⠟⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠻⢆
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡘⢿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣶⣽⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⠿⠿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⡟⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀
⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⡷⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣷⣽⣛⠀⠀⠀⠀
⠀⠀⢠⣿⣿⣿⣿⣿⣿⠿⠛⠛⠀⠀⠀⠀⠀⠀⠛⠛⠿⣿⣿⣿⣿⣿⣶⡄⠀⠀
⠀⢰⣿⣿⠿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠿⣿⣿⡆⠀
⡰⠟⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠻⢆
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣤⣶⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠈⣿⣿⣇⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢾⣿⣷⣄⠀⠀⢸⣿⣿⠀
⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠙⢿⣿⣧⡀⠈⣿⣿⣇
⠀⠀⠀⠀⠀⠀⢰⣿⣿⣦⣄⡀⠀⠻⣿⣿⣦⠸⠟⠛
⠀⠀⠀⠀⠀⠀⠀⠈⠛⠿⣿⣿⣷⣤⣈⠛⠁⠀⠀⠀
⠀⠀⠀⠀⠀⣿⣶⣶⣦⣤⣀⣙⡻⢿⠃⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠈⠉⠙⠛⠿⢿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀
⢠⣤⡄⠀⣿⣿⣿⣿⣿⣷⣶⣶⣿⠀⣤⣤⡄⠀⠀⠀
⢸⣿⡇⠀⣉⣉⣉⣉⣉⣛⣛⣛⣟⠀⣿⣿⡇⠀⠀⠀
⢸⣿⡇⠀⠿⠿⠿⠿⠿⠿⠿⠿⠿⠀⣿⣿⡇⠀⠀⠀
⢸⣿⣧⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣿⣿⡇⠀⠀⠀
⠸⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠇⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢀⣤⣴⣶⣶⣶⣶⣶⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣾⠟⠛⢿⣿⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢸⣿⣄⣀⣼⣿⣿⣿⣿⣿⣿⣿⠀⢀⣀⣀⣀⡀⠀⠀
⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠉⠉⣿⣿⣿⣿⣿⠀⢸⣿⣿⣿⣿⣦⠀
⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⢸⣿⣿⣿⣿⣿⡇
⢰⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⠿⠿⠿⠿⠿⠋⠀⣼⣿⣿⣿⣿⣿⡇
⢸⣿⣿⣿⣿⣿⡿⠉⢀⣠⣤⣤⣤⣤⣤⣤⣤⣴⣾⣿⣿⣿⣿⣿⣿⡇
⢸⣿⣿⣿⣿⣿⡇⠀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀
⠘⣿⣿⣿⣿⣿⡇⠀⣿⣿⣿⣿⣿⠛⠛⠛⠛⠛⠛⠛⠛⠛⠋⠁⠀⠀
⠀⠈⠛⠻⠿⠿⠇⠀⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⣿⡇⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣧⣀⣀⣿⠇⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀
⢀⣤⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣤⡀
⢸⣿⣿⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⣠⣦⡀⠀⠀⢀⣿⡟⠀⢀⣴⣄⠀⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⣠⣾⡿⠋⠀⠀⠀⢸⣿⠇⠀⠀⠙⢿⣷⣄⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠘⢿⣿⣄⠀⠀⠀⢀⣿⡟⠀⠀⠀⠀⣠⣿⡿⠃⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠙⢿⣷⠄⠀⢸⣿⠇⠀⠀⠠⣾⡿⠋⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⠀⠁⠀⠀⠿⠟⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⣿⣿⡇
⢸⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡇
⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇
⠀⠈⠉⠉⠉⠉⠉⠉⠉⠉⠉⢹⣿⣿⣿⣿⡏⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣤⣼⣿⣿⣿⣿⣧⣤⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠀⠀⠀⠀⠀⠀⠀⠀⠀

Fundamentos de Estructuras de Datos y Algoritmos

Tipos de datos, gestión de memoria y complejidad algorítmica: los conceptos base que determinan el rendimiento de tu código

Publicado el 11/10/2025

Imagina que estás organizando tu biblioteca personal. Podrías simplemente apilar todos los libros en un rincón, esto es extremadamente rápido: apenas recibes un libro nuevo, lo lanzas sobre la pila en segundos sin pensar. Pero cuando necesites encontrar un título específico, tendrías que revisar cada libro uno por uno desde arriba hasta encontrarlo, potencialmente examinando cientos de libros. En cambio, si decides organizarlos alfabéticamente en estantes, por género, o por autor, el proceso inicial de colocar cada libro requiere más tiempo: debes buscar la ubicación correcta, quizás mover otros libros para hacer espacio. Sin embargo, una vez organizado, localizar cualquier libro se vuelve casi instantáneo, sabes exactamente dónde buscar.

Saber cuándo utilizar uno u otro enfoque es crucial para diseñar procesos eficientes. Si estás empacando libros para una mudanza, amontonarlos rápidamente en cajas tiene perfecto sentido: solo necesitas transportarlos de un lugar a otro, nadie va a buscar un título específico en medio del traslado. Pero si estás organizando una biblioteca pública donde miles de personas consultarán libros diariamente, invertir tiempo en catalogar y ordenar meticulosamente cada volumen es indispensable. Para tomar la decisión correcta, necesitamos dos cosas: entender el caso de uso del proceso que vamos a diseñar y conocer las diferentes estructuras disponibles para organizar la información.

En el mundo del software ocurre exactamente lo mismo. Cuando implementamos un algoritmo o construimos un sistema, necesitamos entender primero el caso de uso: ¿vamos a insertar datos constantemente y leerlos ocasionalmente? ¿O vamos a hacer miles de búsquedas por segundo? Pero también necesitamos dominar las estructuras de datos disponibles para organizar información en la memoria de una computadora. Existen muchos tipos de estructuras, cada una optimizada para tareas específicas: algunas son excepcionales para búsquedas rápidas pero lentas para inserciones, otras son lo opuesto. Sin embargo, antes de sumergirnos en listas enlazadas, árboles binarios o tablas hash, necesitamos entender tres pilares fundamentales: qué tipos de datos existen, dónde viven en la memoria, y por qué la forma en que los organizamos es crucial.

1. Tipos de Datos Primitivos y de Referencia

¿Qué es exactamente un tipo de dato? Es la clasificación fundamental que le dice a la computadora dos cosas críticas: cuánta memoria reservar y qué operaciones son válidas. Declarar una variable como int en lugar de string no es solo una formalidad sintáctica; define si ocupará 4 bytes u 8 bytes, y si puedes sumarla o concatenarla.

¿Qué es un byte? Un byte es la unidad básica de almacenamiento en memoria, compuesto por 8 bits (donde cada bit puede ser 0 o 1). Con un byte puedes representar 256 valores diferentes (2^8 = 256). Cuando decimos que un int ocupa 4 bytes, significa que usa 32 bits (4 × 8 = 32) para almacenar valores, lo que permite representar aproximadamente 4.3 mil millones de números diferentes (2^32 = 4,294,967,296). Más bytes = más rango de valores posibles.

Todos los lenguajes de programación se construyen sobre tipos de datos primitivos (los bloques atómicos) y tipos no primitivos (estructuras complejas construidas a partir de los primitivos).

Tipos de Datos Primitivos

Los primitivos son los tipos más básicos: números, caracteres, booleanos. Son la materia prima con la que se construye todo lo demás. Tienen tamaño fijo en memoria y representan un único valor.

// JavaScript tiene tipos primitivos dinámicos

// Enteros y decimales (Number)
let edad = 25; // entero
let precio = 19.99; // decimal

// Booleanos
let estaActivo = true;
let tienePermiso = false;

// Carácter y Cadenas (String)
let inicial = 'A';
let nombre = "Juan";

// Undefined y Null
let sinDefinir; // undefined
let valorNulo = null; // null

console.log(typeof edad); // "number"
console.log(typeof estaActivo); // "boolean"
console.log(typeof nombre); // "string"

Ejemplos de Tipos de Datos Primitivos

La diferencia entre int (32 bits, rango ±2 mil millones) y long (64 bits, rango ±9 quintillones) no es arbitraria. Si estás contando usuarios de una app, un int basta. Si estás calculando la deuda nacional, necesitas long. Elegir el tipo correcto no solo evita errores de desbordamiento, sino que optimiza el uso de memoria.

¿Qué es un error de desbordamiento (overflow)? Ocurre cuando intentas almacenar un valor que excede el rango máximo del tipo de dato. Por ejemplo, si un int (32 bits) tiene un valor máximo de 2,147,483,647 y le sumas 1, no obtienes 2,147,483,648 como esperarías. En su lugar, el valor "se desborda" y vuelve al valor mínimo negativo: -2,147,483,648. Es como un odómetro de auto que pasa de 999,999 a 000,000. Este tipo de bug es silencioso y peligroso: el programa no crashea, simplemente produce resultados completamente incorrectos. En casos críticos (sistemas financieros, aeroespaciales, médicos), un overflow no detectado puede ser catastrófico.

De manera similar, float vs double es una decisión entre precisión y memoria. Un float (32 bits) te da ~7 dígitos de precisión, suficiente para gráficos 3D. Un double (64 bits) te da ~15 dígitos, necesario para cálculos científicos. La diferencia en memoria puede parecer insignificante para una variable, pero multiplicada por millones en un array, se vuelve crítica.

Tipos de Datos No Primitivos (o de Referencia)

A diferencia de los primitivos que almacenan un valor directo, los tipos de referencia almacenan una dirección de memoria que apunta a donde realmente vive el dato. Esto permite estructuras complejas y de tamaño variable: arrays, strings, objetos personalizados.

// Cadenas (String) - aunque se comportan como primitivos,
// internamente son objetos
let mensaje = "Hola Mundo";
let longitud = mensaje.length;  // propiedad de objeto

// Arreglos (Array)
let numeros = [1, 2, 3, 4, 5];
let nombres = ["Ana", "Luis", "María"];
let mixto = [1, "texto", true, null];

// Acceso y modificación
console.log(numeros[0]); // 1
numeros.push(6); // añade al final
console.log(numeros.length); // 6

// Objetos
let persona = {
nombre: "Juan",
edad: 30,
activo: true
};
console.log(persona.nombre); // "Juan"

Ejemplos de Tipos de Datos No Primitivos

Aquí hay un detalle crucial: cuando pasas un primitivo a una función, se pasa por valor (una copia). Cuando pasas un objeto o array, se pasa por referencia (la dirección de memoria). Esto significa que modificar un array dentro de una función afecta al original, pero modificar un int no lo hace. Entender esta diferencia es fundamental para evitar bugs sutiles.

¿Por qué importa esto? Porque determina cómo se comporta tu código en memoria y afecta directamente el rendimiento. Copiar un int cuesta 4 bytes. Copiar un array de 1 millón de elementos cuesta 4 MB y tiempo de CPU. Pasar una referencia al array cuesta solo 8 bytes (el tamaño del puntero).

// PASO POR VALOR (primitivos)
function incrementar(numero) {
    numero = numero + 10;  // Modifica la COPIA local
    console.log("Dentro de la función:", numero);  // 15
}

let x = 5;
incrementar(x); // Se pasa una COPIA de x (4 bytes copiados)
console.log("Fuera de la función:", x); // 5 (sin cambios)

// PASO POR REFERENCIA (arrays/objetos)
function modificarArray(arr) {
arr[0] = 999; // Modifica el array ORIGINAL
console.log("Dentro de la función:", arr); // [999, 2, 3]
}

let numeros = [1, 2, 3];
modificarArray(numeros); // Se pasa la DIRECCIÓN (8 bytes copiados)
console.log("Fuera de la función:", numeros); // [999, 2, 3] (¡cambió!)

// Aunque el array tenga 1 millón de elementos,
// solo se copian 8 bytes (la dirección de memoria)

Diferencia entre Paso por Valor y Paso por Referencia

Las Operaciones Fundamentales

Ahora que entendemos qué tipos de datos existen y cómo se comportan en memoria (valor vs referencia), necesitamos establecer el vocabulario fundamental: ¿qué operaciones podemos realizar sobre cualquier colección de datos? Estas operaciones son universales para todas las estructuras. Más adelante tendremos publicaciones dedicadas a explorar en profundidad cada estructura (arrays, listas enlazadas, árboles, hash tables, etc.) con todas sus complejidades y casos de uso. Por ahora, veamos una introducción a las cinco operaciones esenciales:

1. Acceso (Access): Obtener un elemento en una posición específica.

  • Array: O(1) - Cálculo directo de dirección de memoria: dirección_base + (índice × tamaño_elemento). Acceder a arr[0] o arr[999999] toma el mismo tiempo.
  • Linked List: O(N) - Debes seguir punteros nodo por nodo desde el inicio. Para llegar al elemento 1000, haces 1000 saltos.

2. Búsqueda (Search): Encontrar un elemento con un valor específico.

  • Array desordenado: O(N) - Sin orden, debes revisar elemento por elemento. En el peor caso, revisas toda la colección.
  • Binary Search Tree: O(log N) - Cada comparación descarta la mitad del espacio de búsqueda. 1 millón de elementos requiere solo ~20 comparaciones (log₂(1,000,000) ≈ 20).

3. Inserción (Insertion): Agregar un nuevo elemento.

  • Array (en el medio): O(N) - Memoria contigua significa desplazar todos los elementos siguientes para hacer espacio. Insertar [10,20,30,40,50][10,20,99,30,40,50] requiere mover 30, 40, 50.
  • Linked List (si ya tienes la posición): O(1) - Solo cambias dos punteros. No importa el tamaño de la lista.

4. Eliminación (Deletion): Remover un elemento.

  • Array (del medio): O(N) - Dejas un "hueco" que debe llenarse desplazando elementos hacia atrás. [10,20,30,40,50][10,20,40,50] requiere mover 40 y 50.
  • Linked List (si ya tienes la posición): O(1) - Cambias un puntero para "saltarte" el nodo eliminado.

5. Recorrido (Traversal): Visitar todos los elementos secuencialmente.

  • Array: O(N) - Muy eficiente por cache locality. Elementos contiguos se cargan juntos en caché.
  • Linked List: O(N) - Menos eficiente. Cada nodo puede estar disperso en memoria, causando cache misses frecuentes. En la práctica puede ser 10-20x más lento que un array aunque ambos sean O(N).

Con este vocabulario establecido, ahora podemos sumergirnos en dónde viven estos datos en memoria y por qué eso importa tanto para el rendimiento de estas operaciones.

2. Gestión de Memoria: Stack y Heap

Ya sabemos qué tipos de datos existen, pero ¿dónde viven exactamente cuando ejecutas tu programa? La respuesta es más compleja de lo que parece. La memoria de tu computadora no es un espacio único y homogéneo; está dividida en regiones especializadas con diferentes características de velocidad, tamaño y gestión.

La Jerarquía de Memoria: De lo Más Rápido a lo Más Lento

Antes de hablar de Stack y Heap, necesitas entender que tu computadora tiene múltiples niveles de memoria, cada uno con un balance distinto entre velocidad, tamaño y costo. Es como tener diferentes tipos de almacenamiento en una cocina: algunos espacios son pequeños pero están al alcance de la mano (veloces), otros son amplios pero requieren caminar hasta ellos (lentos).

1. Registros del CPU

Los registros son las variables que viven dentro del procesador. Son extremadamente rápidos (1 ciclo de CPU, ~0.3 nanosegundos) pero increíblemente limitados: un procesador moderno tiene solo unas 16-32 ubicaciones de almacenamiento de propósito general. Cuando escribes int x = 5;, el compilador intenta mantener x en un registro mientras sea posible. No tienes control directo sobre esto, el compilador decide.

2. Memoria Caché (L1, L2, L3)

La caché es memoria ultrarrápida que vive junto al procesador y actúa como intermediario entre el CPU y la RAM. Es como un escritorio cerca de una biblioteca: guardas ahí los libros que estás usando constantemente para no tener que ir a los estantes cada vez.

  • L1: La más rápida (~1 nanosegundo), más pequeña (~32-64 KB), integrada en cada núcleo del CPU
  • L2: Intermedia (~3-10 nanosegundos), tamaño medio (~256 KB - 1 MB), también por núcleo
  • L3: Más lenta (~10-20 nanosegundos), más grande (~8-32 MB), compartida entre todos los núcleos

Cuando tu programa accede a array[0], el procesador carga no solo ese elemento sino un "bloque" completo de memoria cercana (típicamente 64 bytes) en la caché, apostando a que pronto accederás a array[1], array[2], etc.

¿Por qué importa cómo organizamos los datos? Cache Locality

Este comportamiento de prefetching de la caché explica por qué la organización física de los datos en memoria tiene un impacto masivo en el rendimiento. Hay dos formas fundamentales de organizar datos:

Datos contiguos (como arrays): Se almacenan en bloques consecutivos de memoria. Cuando accedes al primer elemento, la caché carga automáticamente los elementos cercanos. Si tu siguiente operación necesita array[1] o array[2], ya están en caché (acceso en ~1 nanosegundo en lugar de ~100 nanosegundos desde RAM). Este patrón de acceso se llama cache locality y es extremadamente eficiente.

Datos fragmentados (como listas enlazadas): Cada elemento puede estar en cualquier parte de la RAM. El elemento 0 podría estar en la dirección 0x1000, el elemento 1 en 0x9AF3C8, y el elemento 2 en 0x2B14. Cada nodo tiene un puntero que apunta al siguiente. Cuando accedes al primer elemento, la caché carga memoria cercana a 0x1000, pero el siguiente elemento está en 0x9AF3C8, completamente alejado. Cache miss. El procesador debe esperar ~100 nanosegundos para traer ese dato desde RAM. Esto se repite en cada acceso.

La diferencia es dramática. Procesar 1 millón de enteros en un array puede ser 10-20x más rápido que procesarlos en una lista enlazada, incluso si ambos algoritmos son O(N). La complejidad Big O no captura este detalle de hardware, pero en sistemas reales con millones de operaciones por segundo, puede significar la diferencia entre un servidor que responde en 50 ms o en 5 ms. Este es el motivo por el que arrays suelen ser la primera opción para almacenar colecciones de datos, a menos que necesites las ventajas específicas de otras estructuras (como inserciones rápidas al inicio con listas enlazadas).

3. Memoria RAM (Random Access Memory)

La RAM es la memoria principal donde viven tus programas en ejecución. Es mucho más grande (8 GB - 64 GB típicamente) pero significativamente más lenta que la caché (~100 nanosegundos). Cuando declaras int arr[1000000];, esos 4 MB se asignan en RAM.

Todo lo que discutiremos sobre Stack y Heap ocurre en RAM. La RAM es volátil: cuando apagas la computadora, todo se pierde. Por eso debes guardar archivos en disco.

4. Almacenamiento en Disco (SSD/HDD)

Los discos son masivos (256 GB - 4 TB) pero extremadamente lentos comparados con RAM (~100,000 nanosegundos para SSD, ~10,000,000 nanosegundos para HDD tradicional). No son parte de la memoria de trabajo de un programa, pero existen técnicas como "memoria virtual" donde el sistema operativo puede usar disco como extensión de RAM cuando se agota (esto se llama "swap" y es devastador para el rendimiento).

La diferencia en velocidad es brutal: Si un acceso a registro tomara 1 segundo, un acceso a caché tomaría 3 segundos, a RAM tomaría 5 minutos, y a disco tomaría 1 semana. Por eso el código que mantiene datos cercanos en memoria (cache-friendly) puede ser órdenes de magnitud más rápido.

Direcciones de Memoria y Punteros

Ahora que sabemos dónde vive la memoria, hablemos de cómo la identificamos.

¿Qué es una Dirección de Memoria?

La RAM es como un hotel gigante con millones de habitaciones. Cada byte en RAM tiene una dirección única, típicamente representada en hexadecimal: 0x7fff5fbff5c0. Esta dirección es simplemente un número que identifica dónde está ese byte en la memoria física.

Cuando declaras int x = 42;, el sistema operativo le asigna 4 bytes consecutivos en RAM, digamos en las direcciones 0x1000 a 0x1003. El valor 42 se almacena en esos 4 bytes.

¿Qué es un Puntero?

Un puntero es simplemente una variable que almacena una dirección de memoria. Siguiendo nuestra analogía del hotel, un puntero es como tener el número de habitación escrito en una nota: la nota no es la habitación en sí, pero te dice exactamente dónde encontrarla.

int x = 42;           // Variable normal en la dirección 0x1000
int* ptr = &x;        // Puntero que almacena 0x1000

En lenguajes de alto nivel (JavaScript, Python, Java), los punteros están "escondidos". Cuando haces let obj = {name: "Ana"};, internamente obj es una referencia (básicamente un puntero) a la dirección donde vive el objeto en memoria. El lenguaje no te deja manipular la dirección directamente por seguridad, pero existe.

¿Por qué son importantes los punteros? Porque permiten compartir datos sin copiarlos. Como vimos anteriormente en el ejemplo de paso por valor vs paso por referencia, si una función recibe un puntero a un array de 1 millón de elementos, solo copia 8 bytes (la dirección), no los 4 MB completos del array. Esta es la razón fundamental por la que pasar objetos y arrays por referencia es tan eficiente.

Dos Regiones de Memoria con Características Distintas

Con este contexto, ahora podemos hablar de cómo se organiza la RAM que tu programa usa. Tu programa no tiene acceso a toda la RAM, sino a un espacio virtual asignado por el sistema operativo, dividido en regiones especializadas. Las dos más importantes son Stack y Heap:

1. Memoria Stack (Pila)

La Stack es una región de memoria que funciona exactamente como una pila de platos en tu cocina. Solo puedes interactuar con el plato de arriba: añadir un plato nuevo encima (push), o quitar el plato superior (pop). No puedes sacar el plato del medio sin desmoronar todo. Este comportamiento se llama LIFO (Last-In-First-Out): el último que entra es el primero que sale.

¿Cómo funciona en tu programa?

Cada vez que llamas a una función, el sistema crea un "marco" o stack frame en la Stack. Imagina que cada marco es una caja que contiene:

  • Los parámetros que le pasaste a la función
  • Las variables locales que la función declara
  • La dirección de memoria donde debe regresar cuando termine

Cuando la función termina, su marco se elimina de la Stack automáticamente, liberando toda la memoria que usaba. Es como quitar el plato de arriba: rápido, simple, automático.

Ejemplo visual:

función main() llama a calcularArea(10, 5)
┌─────────────────────┐
│ calcularArea        │ ← Top de la Stack
│ ancho: 10          │
│ alto: 5            │
│ area: 50           │
├─────────────────────┤
│ main()             │
│ resultado: ?       │
└─────────────────────┘

calcularArea termina y retorna
┌─────────────────────┐
│ main()             │ ← Top de la Stack
│ resultado: 50      │
└─────────────────────┘

Características clave:

  • Rápida: Asignar memoria en la Stack es instantáneo. El sistema solo necesita "mover un puntero" hacia arriba (como apilar un plato). No hay búsqueda de espacio libre ni gestión compleja. Asignar 100 variables locales toma el mismo tiempo que asignar 1.

  • Automática: No necesitas "liberar" o "eliminar" nada manualmente. Cuando una función termina, su marco desaparece automáticamente de la Stack. Esto previene un tipo común de bug llamado "memory leak" (fuga de memoria).

  • Limitada: La Stack es pequeña, típicamente 1-8 MB según tu sistema operativo. ¿Por qué? Porque necesita ser rápida y predecible. Si declaras un array gigante como variable local (int arr[10000000];), podrías agotar la Stack y causar un stack overflow (desbordamiento de pila), que crashea tu programa. Es por esto que las recursiones muy profundas (una función que se llama a sí misma miles de veces) pueden causar stack overflow: cada llamada apila un nuevo marco.

  • Predecible: Todas las variables en la Stack deben tener un tamaño conocido antes de que el programa se ejecute (en tiempo de compilación). Cuando escribes int x = 5;, el compilador sabe que x ocupará exactamente 4 bytes. Esto permite al sistema reservar espacio instantáneamente. No puedes crear un array cuyo tamaño se decide durante la ejecución: int arr[n]; donde n se ingresa por el usuario solo funciona si tu compilador usa extensiones especiales, porque rompe la regla de predictibilidad.

// Las variables locales se almacenan en la Stack
function calcularArea(ancho, alto) {
    // 'ancho', 'alto' y 'area' están en la Stack
    let area = ancho * alto;
    return area;
    // Al salir de la función, estas variables se eliminan automáticamente
}

function procesarRectangulo() {
let resultado = calcularArea(10, 5); // 'resultado' en la Stack
console.log(resultado);
// 'resultado' se elimina al finalizar la función
}

Ejemplo de Uso de la Stack

2. Memoria Heap (Montículo)

Si la Stack es una pila de platos ordenada y predecible, el Heap es un almacén gigante y flexible. A diferencia de la Stack donde todo sigue reglas estrictas de entrada y salida, en el Heap puedes pedir espacio de cualquier tamaño, en cualquier momento, y ese espacio permanecerá disponible hasta que explícitamente lo liberes (o hasta que el garbage collector lo haga por ti en lenguajes modernos).

¿Cómo funciona en tu programa?

Cuando necesitas crear algo cuyo tamaño no conoces hasta que el programa se ejecuta, o algo que debe sobrevivir más allá de una función, usas el Heap. En lenguajes como JavaScript o Python, casi no piensas en esto, ocurre automáticamente. En C/C++, debes pedirlo explícitamente con palabras clave como new o malloc.

Imagina el Heap como un estacionamiento enorme. Cuando llegas (necesitas memoria), el encargado busca un espacio disponible que sea lo suficientemente grande para tu auto (tus datos). Te da el número del espacio (la dirección de memoria) y ahí estacionas. Cuando te vas (liberas la memoria), ese espacio queda disponible nuevamente. Pero a diferencia de la Stack, los espacios no se liberan automáticamente cuando sales de una función, tú decides cuándo.

Ejemplo visual:

Heap de memoria (representación simplificada)
┌──────────────────────────────────────┐
│ [libre]                              │
├──────────────────────────────────────┤
│ Array de 1000 enteros (4KB)         │ ← new int[1000]
│ Dirección: 0x10000                   │
├──────────────────────────────────────┤
│ [libre]                              │
├──────────────────────────────────────┤
│ Objeto Usuario {nombre, edad}       │ ← new Usuario("Ana", 25)
│ Dirección: 0x20000                   │
├──────────────────────────────────────┤
│ [libre]                              │
├──────────────────────────────────────┤
│ String de longitud variable (50B)   │ ← "Hola Mundo..."
│ Dirección: 0x30000                   │
├──────────────────────────────────────┤
│ [libre]                              │
└──────────────────────────────────────┘

Cada bloque asignado puede tener tamaño diferente, pueden estar dispersos, y persisten independientemente de qué función los creó.

Características clave:

  • Grande: El Heap es enorme comparado con la Stack. Está limitado solo por tu RAM disponible (típicamente gigabytes). Puedes crear un array de 100 millones de elementos en el Heap sin problema. La Stack solo tiene 1-8 MB, pero el Heap puede usar 8 GB, 16 GB, o lo que tu sistema tenga disponible. Esto lo hace ideal para estructuras de datos grandes: bases de datos en memoria, cachés gigantes, procesamiento de imágenes de alta resolución.

  • Flexible: Aquí está la magia del Heap: puedes crear estructuras cuyo tamaño no conoces hasta que el programa se ejecuta. ¿El usuario quiere subir una imagen? No sabes si será de 1 KB o 10 MB hasta que lo haga. En el Heap, simplemente pides la cantidad exacta de memoria que necesitas en ese momento: new byte[tamañoDeImagen]. En la Stack esto es imposible porque todo debe tener tamaño conocido en tiempo de compilación.

  • Más lenta: Pedir memoria en el Heap es más costoso que en la Stack. ¿Por qué? Porque el sistema operativo debe buscar un espacio libre que sea lo suficientemente grande. Imagina un estacionamiento casi lleno: el encargado debe recorrer espacios hasta encontrar uno que te quede. Peor aún, con el tiempo el Heap se fragmenta: tienes memoria libre dispersa en pedacitos entre bloques ocupados. Pides 1 MB continuo, pero solo hay 500 KB aquí y 600 KB allá. El sistema debe mover cosas o buscar más. Esto se llama gestión de fragmentación y consume tiempo de CPU. Asignar en el Heap puede ser 100-1000x más lento que en la Stack.

  • Manual o Automática (según el lenguaje):

    • En C/C++: Tú eres responsable de pedir y liberar memoria. Usas new/malloc para pedir y debes usar delete/free para liberar. Si olvidas liberar, causas un memory leak: memoria que ya no usas pero sigue ocupada. Con suficientes leaks, tu programa agota la RAM y el sistema operativo lo mata.
    • En Java, Python, JavaScript: Estos lenguajes tienen un Garbage Collector (recolector de basura). Es un proceso que corre en segundo plano, detectando qué objetos ya no tienen referencias activas, y automáticamente libera esa memoria. No necesitas pensar en delete. Pero el GC no es gratis: debe pausar tu programa brevemente para hacer limpieza. En aplicaciones normales esto es imperceptible (~milisegundos). En sistemas de alta frecuencia (videojuegos, trading), estas pausas pueden ser problemáticas.
  • Persistente: Esta es una diferencia crítica con la Stack. Los datos en el Heap no se eliminan automáticamente cuando una función termina. Si creas un objeto en el Heap dentro de una función y retornas un puntero/referencia a él, el objeto sobrevive después de que la función termine. Esto permite retornar estructuras complejas sin copiarlas. Es extremadamente útil pero también peligroso en C/C++: si pierdes todas las referencias a un objeto en el Heap sin liberarlo primero, ese memoria queda inaccesible pero ocupada (memory leak).

// Los objetos y arrays se almacenan en el Heap
function crearUsuario(nombre, edad) {
    // El objeto se crea en el Heap
    let usuario = {
        nombre: nombre,
        edad: edad,
        hobbies: []  // el array también está en el Heap
    };

    // 'usuario' (la referencia) está en la Stack,
    // pero el objeto al que apunta está en el Heap
    return usuario;

}

let persona = crearUsuario("Ana", 25);
// El objeto persiste en el Heap incluso después
// de que la función termine
console.log(persona.nombre); // "Ana"

// El Garbage Collector de JavaScript liberará
// la memoria cuando ya no haya referencias al objeto

Ejemplo de Uso del Heap

3. La Importancia de la Estructura de Datos

Ya entendemos qué tipos de datos existen y dónde viven en memoria. Ahora la pregunta crítica: ¿por qué importa cómo los organizamos?

El hardware no puede salvar código mal estructurado. Puedes tener el procesador más rápido del mundo, pero si organizas los datos incorrectamente, tu aplicación será lenta. La estructura de datos correcta puede hacer tu código 50,000x más rápido que la incorrecta, sin cambiar una sola línea de lógica de negocio.

El rendimiento real viene de la organización

Imagina un catálogo de productos con 1 millón de elementos. Un usuario busca uno específico:

  • Array desordenado con búsqueda lineal (O(N)): Peor caso = revisar 1,000,000 elementos → ~50 ms
  • Árbol binario de búsqueda (O(log N)): Peor caso = revisar ~20 niveles → ~0.001 ms

La diferencia: 50,000x más rápido. No por mejor hardware, sino por organizar los datos inteligentemente. Multiplica esto por miles de usuarios simultáneos y entiendes por qué empresas como Amazon invierten tanto en estructuras de datos eficientes.

No existe la estructura perfecta universal

No existe una estructura que sea O(1) para todas las operaciones. Cada estructura sacrifica eficiencia en algunas operaciones para ganarla en otras:

  • Arrays: Acceso O(1), pero inserción en el medio O(N)
  • Linked Lists: Inserción O(1) (si tienes la posición), pero acceso O(N)
  • Hash Tables: Búsqueda O(1), pero sin orden garantizado
  • Binary Search Trees: Balance entre búsqueda, inserción y eliminación, todas O(log N)

La pregunta correcta no es "¿cuál es la mejor estructura de datos?", sino "¿cuál es la mejor para este problema específico?" Si tu aplicación hace 90% lecturas, optimiza para lecturas (arrays, árboles). Si hace inserciones constantes al inicio, optimiza para eso (linked lists). Estas decisiones se basan en las cinco operaciones fundamentales que vimos en la primera parte: acceso, búsqueda, inserción, eliminación y recorrido.

La escala magnifica las decisiones

Con 100 elementos, casi cualquier estructura funciona. La diferencia entre O(N) y O(log N) es imperceptible (microsegundos). Pero con 10 millones de elementos, esa diferencia puede ser segundos vs milisegundos. En producción con miles de usuarios concurrentes, es la diferencia entre un sistema que responde y uno que colapsa.

Este es el motivo por el cual entender estructuras de datos no es académico, es existencial para sistemas reales. Un sistema de streaming con millones de usuarios no puede permitirse estructuras ineficientes. Un motor de búsqueda procesando billones de documentos no puede usar búsqueda lineal. Una base de datos con terabytes de información debe organizar datos para acceso eficiente.

El pragmatismo también importa

No optimices prematuramente. La estructura más simple que resuelva tu problema es a menudo la correcta. Un array desordenado con búsqueda lineal es perfectamente válido si procesas 50 elementos una vez al día. El código legible y mantenible puede ahorrarte más tiempo que el código ultra-optimizado.

Primero haz que funcione correctamente, luego mide dónde están los cuellos de botella reales (con profilers, métricas de producción), y solo entonces optimiza. Como dice Donald Knuth: "La optimización prematura es la raíz de todos los males".

Conclusión

Hemos cubierto los tres pilares fundamentales que determinan cómo tu código se comporta en producción:

1. Tipos de datos: No son solo etiquetas sintácticas. Determinan consumo de memoria, rendimiento, y comportamiento (paso por valor vs referencia). La diferencia entre int y long, o entre primitivos y objetos, tiene consecuencias reales.

2. Gestión de memoria: La Stack y el Heap no son abstracciones académicas. Son regiones físicas con características radicalmente diferentes. Entender la jerarquía completa (registros → caché → RAM → disco) y conceptos como cache locality explica por qué código que es O(N) en teoría puede ser 10-20x más rápido en la práctica.

3. Estructuras de datos: El hardware no puede compensar mala organización. La estructura correcta puede hacer tu código 50,000x más rápido. No existe una solución universal, cada estructura está optimizada para operaciones específicas.

Lo más importante: estos tres pilares están profundamente conectados. Los tipos de datos que eliges determinan dónde viven en memoria. Dónde viven en memoria afecta el rendimiento. Y cómo los organizas (la estructura de datos) amplifica o destruye ese rendimiento.

Estos conceptos no son opcionales para software profesional. Son el lenguaje común entre desarrolladores competentes. Cuando un ingeniero senior habla de "evitar allocations en el Heap dentro del hot path", está aplicando exactamente estos principios.

Ahora tienes el mapa conceptual. En las próximas publicaciones nos sumergiremos en las estructuras concretas: arrays, listas enlazadas, pilas, colas, árboles y hash tables. Veremos implementaciones con código, trade-offs específicos, y casos de uso reales de producción. El conocimiento teórico que tienes ahora se convertirá en herramientas prácticas que usarás diariamente.