14 marzo 2025

Esplorazione dei Pattern di Programmazione Essenziali

Nell’informatica, padroneggiare i principali pattern di programmazione può migliorare notevolmente le tue capacità di problem-solving. Questo articolo esplora una varietà di pattern, dalle tecniche più semplici come la somma prefixata fino a metodi più complessi come il backtracking, ciascuno utile per affrontare diverse tipologie di sfide.

1. Somma Prefixata (Prefix Sum)

Panoramica: La tecnica della somma prefixata consiste nel calcolare la somma cumulativa di un array (o di un’altra sequenza), permettendo di rispondere in modo efficiente a query di somma su intervalli.

Casi d’Uso:

Esempio (JavaScript):

function prefixSums(arr) {
  const prefix = [0];
  for (let i = 0; i < arr.length; i++) {
    prefix.push(prefix[i] + arr[i]);
  }
  return prefix;
}

const arr = [1, 2, 3, 4];
console.log(prefixSums(arr)); // [0, 1, 3, 6, 10]

2. Tecnica dei Due Puntatori (Two Pointers)

Panoramica: La tecnica dei due puntatori utilizza due indici per attraversare strutture dati, spesso partendo da estremità opposte o muovendosi a velocità diverse.

Casi d’Uso:

Esempio (JavaScript):

function twoSumSorted(arr, target) {
  let left = 0,
    right = arr.length - 1;
  while (left < right) {
    const sum = arr[left] + arr[right];
    if (sum === target) return [left, right];
    else if (sum < target) left++;
    else right--;
  }
  return [];
}

console.log(twoSumSorted([1, 2, 3, 4, 6], 7)); // [0, 3]

3. Finestra Scorrevole (Sliding Window)

Panoramica: Questo pattern prevede il mantenimento di una finestra che scorre sui dati per calcolare dinamicamente i valori su sottoarray o sottostringhe.

Casi d’Uso:

Esempio (JavaScript):

function maxSubarraySum(arr, k) {
  let maxSum = 0,
    currentSum = 0;
  for (let i = 0; i < arr.length; i++) {
    currentSum += arr[i];
    if (i >= k - 1) {
      maxSum = Math.max(maxSum, currentSum);
      currentSum -= arr[i - (k - 1)];
    }
  }
  return maxSum;
}

console.log(maxSubarraySum([1, 4, 2, 10, 23, 3, 1, 0, 20], 4)); // 39

4. Inversione di una Lista Collegata (Linked List Reversal)

Panoramica: L’inversione di una lista collegata è un pattern comune che consiste nel invertire i puntatori di una lista per modificarne l’ordine dei nodi.

Casi d’Uso:

Esempio (JavaScript):

function reverseLinkedList(head) {
  let prev = null;
  let current = head;
  while (current !== null) {
    let next = current.next;
    current.next = prev;
    prev = current;
    current = next;
  }
  return prev;
}

5. Stack Monotonico (Monotonic Stack)

Panoramica: Uno stack monotonico viene mantenuto in ordine crescente o decrescente. È utile per risolvere problemi che richiedono di trovare l’elemento successivo maggiore o minore.

Casi d’Uso:

Esempio (JavaScript):

function nextGreaterElements(arr) {
  const result = new Array(arr.length).fill(-1);
  const stack = [];
  for (let i = 0; i < arr.length; i++) {
    while (stack.length && arr[i] > arr[stack[stack.length - 1]]) {
      result[stack.pop()] = arr[i];
    }
    stack.push(i);
  }
  return result;
}

console.log(nextGreaterElements([2, 1, 2, 4, 3])); // [4, 2, 4, -1, -1]

6. Primi K Elementi (Top K Elements)

Panoramica: Questo pattern consiste nel trovare i primi K elementi in un dataset, spesso utilizzando heap.

Casi d’Uso:

Esempio (JavaScript con un Min-Heap):

class MinHeap {
  constructor() {
    this.heap = [];
  }

  insert(value) {
    this.heap.push(value);
    this.bubbleUp();
  }

  bubbleUp() {
    let index = this.heap.length - 1;
    while (index > 0) {
      let parentIndex = Math.floor((index - 1) / 2);
      if (this.heap[parentIndex] <= this.heap[index]) break;
      [this.heap[parentIndex], this.heap[index]] = [
        this.heap[index],
        this.heap[parentIndex],
      ];
      index = parentIndex;
    }
  }

  extractMin() {
    if (this.heap.length === 1) return this.heap.pop();
    const min = this.heap[0];
    this.heap[0] = this.heap.pop();
    this.sinkDown(0);
    return min;
  }

  sinkDown(index) {
    const length = this.heap.length;
    while (true) {
      let left = 2 * index + 1;
      let right = 2 * index + 2;
      let smallest = index;

      if (left < length && this.heap[left] < this.heap[smallest])
        smallest = left;
      if (right < length && this.heap[right] < this.heap[smallest])
        smallest = right;
      if (smallest === index) break;

      [this.heap[index], this.heap[smallest]] = [
        this.heap[smallest],
        this.heap[index],
      ];
      index = smallest;
    }
  }
}

function findTopKElements(nums, k) {
  let minHeap = new MinHeap();
  for (let num of nums) {
    minHeap.insert(num);
    if (minHeap.heap.length > k) {
      minHeap.extractMin();
    }
  }
  return minHeap.heap;
}

console.log(findTopKElements([3, 1, 5, 12, 2, 11, 7], 3)); // Output: Top 3 elements

7. Intervalli Sovrapposti

Panoramica: Questo modello riguarda la fusione o l’elaborazione di intervalli che si sovrappongono, un compito comune nei problemi di pianificazione.

Casi d’uso:

Esempio (JavaScript):

function mergeIntervals(intervals) {
  if (!intervals.length) return intervals;
  intervals.sort((a, b) => a[0] - b[0]);
  const merged = [intervals[0]];
  for (let i = 1; i < intervals.length; i++) {
    const last = merged[merged.length - 1];
    if (intervals[i][0] <= last[1]) {
      last[1] = Math.max(last[1], intervals[i][1]);
    } else {
      merged.push(intervals[i]);
    }
  }
  return merged;
}

console.log(
  mergeIntervals([
    [1, 3],
    [2, 6],
    [8, 10],
    [15, 18],
  ]),
); // [[1,6],[8,10],[15,18]]

8. Ricerca Binaria Modificata

Panoramica: Una variante della classica ricerca binaria, questo modello è adattato per trovare risposte in uno spazio di ricerca modificato o vincolato.

Casi d’uso:

Esempio (JavaScript - Trovare un elemento di picco):

function findPeakElement(nums) {
  let left = 0,
    right = nums.length - 1;
  while (left < right) {
    let mid = Math.floor((left + right) / 2);
    if (nums[mid] < nums[mid + 1]) left = mid + 1;
    else right = mid;
  }
  return left;
}

console.log(findPeakElement([1, 2, 3, 1])); // 2

9. Ricerca in Profondità (DFS)

Panoramica: DFS è una tecnica di traversamento di grafi che esplora il più lontano possibile lungo ogni ramo prima di fare marcia indietro.

Casi d’uso:

Esempio (JavaScript):

function dfs(graph, start, visited = new Set()) {
  visited.add(start);
  console.log(start);
  for (let neighbor of graph[start]) {
    if (!visited.has(neighbor)) {
      dfs(graph, neighbor, visited);
    }
  }
}

const graph = {
  A: ["B", "C"],
  B: ["D", "E"],
  C: ["F"],
  D: [],
  E: ["F"],
  F: [],
};

dfs(graph, "A");

10. Ricerca in Ampiezza (BFS)

Panoramica: BFS esplora un grafo livello per livello, rendendolo ideale per trovare il percorso più breve nei grafi non ponderati.

Casi d’uso:

Esempio (JavaScript):

function bfs(graph, start) {
  const queue = [start];
  const visited = new Set([start]);
  while (queue.length) {
    const vertex = queue.shift();
    console.log(vertex);
    for (let neighbor of graph[vertex]) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        queue.push(neighbor);
      }
    }
  }
}

bfs(graph, "A");

11. Traversamento di Matrice

Panoramica: Il traversamento di matrice consiste nel navigare attraverso un array 2D, un’operazione comune nell’elaborazione delle immagini e nello sviluppo di giochi.

Casi d’uso:

Esempio (JavaScript):

function traverseMatrix(matrix) {
  for (let i = 0; i < matrix.length; i++) {
    for (let j = 0; j < matrix[i].length; j++) {
      console.log(matrix[i][j]);
    }
  }
}

const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];
traverseMatrix(matrix);

12. Backtracking

Panoramica: Il backtracking è una tecnica ricorsiva utilizzata per risolvere i problemi in modo incrementale, abbandonando le soluzioni che non soddisfano i criteri.

Casi d’uso:

Esempio (JavaScript - N-Queens):

function solveNQueens(n) {
  function isSafe(board, row, col) {
    for (let i = 0; i < row; i++) {
      if (board[i] === col || Math.abs(board[i] - col) === row - i) {
        return false;
      }
    }
    return true;
  }

  function backtrack(board, row) {
    if (row === n) {
      solutions.push([...board]);
      return;
    }
    for (let col = 0; col < n; col++) {
      if (isSafe(board, row, col)) {
        board[row] = col;
        backtrack(board, row + 1);
        board[row] = -1;
      }
    }
  }

  let solutions = [];
  backtrack(new Array(n).fill(-1), 0);
  return solutions;
}

console.log(solveNQueens(4));

Conclusione

Ognuno di questi modelli di programmazione è uno strumento potente nel kit di un sviluppatore. Che tu stia ottimizzando le prestazioni del tuo codice con somme prefissate o risolvendo puzzle complessi con il backtracking, comprendere queste tecniche ti aiuterà ad affrontare una vasta gamma di problemi in modo efficace. Sperimenta con questi modelli, integrali nei tuoi progetti e migliora le tue capacità di problem solving!